跳到主要内容

C++ 指针详解:传统指针与智能指针

· 阅读需 7 分钟
XingHunm
Tech Enthusiast

指针是 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++ 创建并传递给 QMLC++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

✅ 正确做法:

  1. 有 parent 的 QObject:用原始指针,交给 Qt 管理
  2. 无 parent 的 QObject:可用 std::unique_ptrQScopedPointer
  3. 非 QObject 类:优先用 std::unique_ptr / std::shared_ptr

五、指针选择速查表

场景推荐类型理由
标准 C++std::unique_ptr默认首选,安全高效
多处共享std::shared_ptr真正需要共享所有权时使用
观察者模式std::weak_ptr不影响生命周期的弱引用
Qt 有 parentT* (原始指针)Qt 自动管理,不要干扰
Qt 无 parentQScopedPointer / unique_ptr防止忘记释放
Qt 弱引用QPointer<T>自动检测 Qt 对象是否被销毁
函数参数T& / T*不转移所有权,仅观察

六、最佳实践

  1. 优先使用 make_unique / make_shared:更安全、更高效
  2. 原始指针仅用于观察:不拥有所有权,不负责 delete
  3. 明确所有权语义:
    • 函数参数用引用 (T&) 或原始指针 (T*)
    • 需要转移所有权时用 unique_ptr
    • 需要共享所有权时用 shared_ptr
  4. Qt 开发特例:有 parent 的 QObject 只用原始指针
  5. 初始化原始指针:务必初始化为 nullptr

现代 C++ 原则 除非别无选择,否则不要手动调用 delete

在 Qt 中,相信 Parent 机制