C++ 指针详解:传统指针与智能指针
· 阅读需 7 分钟
指针是 C++ 的核心特性。本文精简介绍了传统指针与智能指针(C++11+)的用法与最佳实践。
一、传统指针基础
基本概念
指针存储内存地址,通过 * 解引用,& 取地址。
int val = 42;
int* ptr = &val; // ptr 存储 val 的地址
*ptr = 100; // 通过指针修改 val 的值
// 指针的 const 修饰
const int* c_ptr = &val; // 指向常量:不能通过指针修改值
int* const ptr_c = &val; // 常量指针:指针本身不能改变指向
动态内存管理
使用 new/delete 在堆上分配和释放内存,必须成对使用。
void dynamicMemory() {
int* obj = new int(42); // 分配单个对象
int* arr = new int[5]{0}; // 分配数组
// 使用完毕后释放
delete obj; obj = nullptr;
delete[] arr; arr = nullptr; // 数组必须用 delete[]
}
常见陷阱
| 问题 | 错误示例 | 正确做法 |
|---|---|---|
| 内存泄漏 | int* p = new int; (忘记释放) | 用完后 delete p; |
| 悬空指针 | delete p; *p = 1; | delete p; p = nullptr; |
| 重复释放 | delete p; delete p; | 删除后立即设为 nullptr |
| 数组释放 | int* arr = new int[5]; delete arr; | 必须用 delete[] arr; |
传统指针的问题
手动管理内存容易出错,异常安全性差。现代 C++ 推荐使用智能指针自动管理生命周期。
二、智能指针:自动内存管理
C++11 引入 <memory> 头文件,提供三种智能指针,利用 RAII 机制自动管理资源。
std::unique_ptr - 独占所有权
最常用的智能指针,零开销,确保资源只有一个拥有者。
#include <memory>
void uniquePtrDemo() {
// 推荐使用 make_unique 创建
auto ptr = std::make_unique<int>(100);
// 独占特性:不可复制,只能移动
// auto p2 = ptr; // ❌ 编译错误
auto p2 = std::move(ptr); // ✅ 转移所有权,ptr 变为 nullptr
// 数组支持
auto arr = std::make_unique<int[]>(5);
// 自定义删除器(如关闭文件句柄)
auto file = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("test.txt", "w"), &fclose
);
} // 离开作用域自动释放
std::shared_ptr - 共享所有权
使用引用计数,允许多个指针共享同一资源,最后一个持有者销毁时释放资源。
void sharedPtrDemo() {
auto p1 = std::make_shared<int>(42); // 引用计数 = 1
{
auto p2 = p1; // 引用计数 = 2
std::cout << *p2;
} // p2 销毁,引用计数 = 1
// p1 依然有效
} // p1 销毁,引用计数 = 0,资源释放
为什么用 make_shared?
make_shared 只分配一次内存(对象 + 控制块),而 shared_ptr(new T) 需要两次分配,效率更低。
std::weak_ptr - 弱引用
不增加引用计数,用于打破循环引用或实现观察者模式。
解决循环引用:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // ✅ 用 weak_ptr 打破循环
~Node() { std::cout << "Node 析构\n"; }
};
void noCycle() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1; // 如果 prev 是 shared_ptr,会导致循环引用
} // 正常析构
安全访问:
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(42);
wp = sp;
if (auto locked = wp.lock()) { // 尝试升级为 shared_ptr
std::cout << *locked; // 对象存在,可以访问
}
}
// sp 销毁后,wp.lock() 返回 nullptr
三、实战:成员变量设计
默认选择:直接实例
原则:能用值类型就用值类型,简单可靠。
class Car {
Engine engine; // ✅ 推荐:生命周期自动绑定
public:
Car() : engine(2.0) {} // 初始化列表构造
};
优点:
- 内存连续,缓存友好
- 生命周期自动管理
- 无空指针风险
何时使用指针?
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 多态 | unique_ptr<Base> | 基类指针指向不同派生类对象 |
| 可选成员 | unique_ptr<T> / optional<T> | 成员可能不存在 |
| 延迟初始化 | unique_ptr<T> | 构造时不创建,稍后按需创建 |
| 共享成员 | shared_ptr<T> | 多个对象共享同一实例 |
| PIMPL 模式 | unique_ptr<Impl> | 隐藏实现,减少编译依赖 |
| 循环依赖 | weak_ptr<T> | 打破循环引用 |
| Qt 对象 | T* (原始指针) | Qt 父子机制自动管理,不要用智能指针 |
示例:多态容器
class Zoo {
std::vector<std::unique_ptr<Animal>> animals;
public:
void add(std::unique_ptr<Animal> animal) {
animals.push_back(std::move(animal));
}
};
四、高级:混用场景与陷阱
在实际项目中,常遇到原始指针、智能指针、Qt 对象混用,这是最容易出错的地方。
核心原则
谁创建谁拥有,谁拥有谁删除。一个资源只能有一个"主人"。
场景 1:标准 C++ - 智能指针与原始指针
规则:智能指针拥有所有权,原始指针仅用于观察。
void process(MyClass* ptr) { // 接收原始指针
if (ptr) ptr->doSomething();
// ❌ 绝对不要 delete ptr
}
void caller() {
auto owner = std::make_unique<MyClass>();
process(owner.get()); // 借用给函数
} // owner 离开作用域,自动释放
场景 2:Qt 父子对象机制
Qt 使用对象树自动管理 QObject 及其派生类的内存。
规则:
- 有
parent的对象由 parent 负责释放 - 使用原始指针,不要
delete,不要用智能指针
void createWidget() {
QWidget* parent = new QWidget();
QPushButton* btn = new QPushButton("Click", parent); // 指定 parent
// 不需要 delete btn,parent 析构时会自动释放
}
场景 3:纯 C++ 对象在 Qt 项目中
不继承 QObject 的类不享受 Qt 内存管理,必须手动管理。
class MyData { /* 普通 C++ 类 */ };
void qtFunction() {
// ❌ 错误:内存泄漏!
MyData* data = new MyData();
// ✅ 正确:使用智能指针
auto safeData = std::make_unique<MyData>();
}
场景 4:QML 与 C++ 混合开发
所有权取决于对象的创建方式:
| 创建方式 | 所有权归属 | 生命周期管理 |
|---|---|---|
| QML 声明式创建 | QML 引擎 | QML 自动管理 |
| C++ 创建并传递给 QML | C++ | C++ 负责释放 |
| QML 动态创建 (createObject) | QML 引擎 | QML 垃圾回收 |
示例:QML 声明式创建
Rectangle {
CircuitPainter { // C++ 注册的类
id: painter
}
}
class CircuitPainter : public QQuickPaintedItem {
QVector<Wire*> m_wires; // Wire 对象没有 parent
public:
~CircuitPainter() {
// CircuitPainter 自身由 QML 管理(有父对象 Rectangle)
// 但 m_wires 里的 Wire* 对象没有 parent,必须手动清理
qDeleteAll(m_wires);
}
};
:::tip 关键区分
- CircuitPainter 自身:由 QML 父对象(Rectangle)管理,QML 会自动释放
- m_wires 成员:里面的
Wire*对象没有指定 parent,必须在析构函数中手动清理 :::
致命陷阱:Qt 与智能指针混用
❌ 错误:智能指针管理 Qt 子对象
{
QWidget* parent = new QWidget();
// 错误!unique_ptr 和 parent 都会尝试 delete btn
std::unique_ptr<QPushButton> btn(new QPushButton(parent));
} // CRASH! Double Free
✅ 正确做法:
- 有 parent 的 QObject:用原始指针,交给 Qt 管理
- 无 parent 的 QObject:可用
std::unique_ptr或QScopedPointer - 非 QObject 类:优先用
std::unique_ptr/std::shared_ptr
五、指针选择速查表
| 场景 | 推荐类型 | 理由 |
|---|---|---|
| 标准 C++ | std::unique_ptr | 默认首选,安全高效 |
| 多处共享 | std::shared_ptr | 真正需要共享所有权时使用 |
| 观察者模式 | std::weak_ptr | 不影响生命周期的弱引用 |
| Qt 有 parent | T* (原始指针) | Qt 自动管理,不要干扰 |
| Qt 无 parent | QScopedPointer / unique_ptr | 防止忘记释放 |
| Qt 弱引用 | QPointer<T> | 自动检测 Qt 对象是否被销毁 |
| 函数参数 | T& / T* | 不转移所有权,仅观察 |
六、最佳实践
- 优先使用
make_unique/make_shared:更安全、更高效 - 原始指针仅用于观察:不拥有所有权,不负责
delete - 明确所有权语义:
- 函数参数用引用 (
T&) 或原始指针 (T*) - 需要转移所有权时用
unique_ptr - 需要共享所有权时用
shared_ptr
- 函数参数用引用 (
- Qt 开发特例:有 parent 的
QObject只用原始指针 - 初始化原始指针:务必初始化为
nullptr
现代 C++ 原则
除非别无选择,否则不要手动调用 delete。
在 Qt 中,相信 Parent 机制。