学习定位:现代 C++ 核心知识。智能指针用于表达动态对象的所有权,日常开发应优先掌握 std::unique_ptr,再理解 std::shared_ptr / std::weak_ptr 的共享所有权场景。

智能指针是 C++11 引入的 RAII 类模板,用于自动管理动态内存,避免手动 new/delete 导致的内存泄漏和悬挂指针。

1. 智能指针

传统裸指针的痛点

  • 需要手动 delete,容易遗忘 → 内存泄漏
  • 不确定何时释放 → 悬挂指针(访问已释放的内存)
  • 类内指针必须在析构函数中释放,若有多处 return 或异常,极易遗漏
  • 所有权不明确:调用者是否应该 delete 返回的指针?

智能指针的解决方案

  • 在栈上创建智能指针对象,管理堆上的裸指针
  • 智能指针过期时,自动调用析构函数释放所管理的对象
  • 无需手动 delete,异常安全,所有权明确

2. 智能指针类型

头文件:#include <memory>

智能指针C++标准所有权模型特点
auto_ptrC++98独占(转移)已弃用(C++11起),存在语义缺陷
unique_ptrC++11独占轻量、零开销,不可拷贝,可移动
shared_ptrC++11共享引用计数,最后一个所有者释放对象
weak_ptrC++11弱引用配合 shared_ptr 打破循环引用,不增加引用计数

现代 C++ 中,永远不要使用 auto_ptr,使用 unique_ptr 代替。


3.unique_ptr(独占所有权)

unique_ptr 独占其所管理的对象,同一时间只能有一个 unique_ptr 指向该对象。当 unique_ptr 被销毁时,所管理的对象也被销毁。

3.1 初始化方法

#include <memory>
 
// 方法1:直接使用 new(C++11)
std::unique_ptr<Person> up1(new Person);
 
// 方法2:使用 make_unique(C++14 起,推荐)
std::unique_ptr<Person> up2 = std::make_unique<Person>();
 
// 方法3:先创建裸指针,再交给 unique_ptr(不推荐)
Person* p = new Person;
std::unique_ptr<Person> up3(p);

推荐:始终使用 std::make_unique(C++14 及以上)。它更安全(避免内存泄漏)、更高效(一次分配)。

3.2 基本用法

class Person {
public:
    int age;
    Person() { std::cout << "构造\n"; }
    ~Person() { std::cout << "析构\n"; }
};
 
int main() {
    auto up = std::make_unique<Person>();
    up->age = 25;                 // 使用 ->
    (*up).age = 26;               // 使用 *
    Person* raw = up.get();       // 获取裸指针(不转移所有权)
    // 无需 delete
}   // 自动析构

3.3 注意事项(unique_ptr

title: unique_ptr 注意事项
- 不能将普通指针赋值给 `unique_ptr`(必须通过构造函数或 `reset`)
- **不能拷贝构造**:`std::unique_ptr<Person> up2(up1);` 错误
- **不能赋值**:`up2 = up1;` 错误
- 不要用同一个裸指针初始化多个 `unique_ptr`(会导致重复释放)
- 不要用 `unique_ptr` 管理非 `new` 分配的内存(如栈变量、 `malloc` 分配的)
- 不支持指针运算(`++`、`+` 等)
- 函数传参时建议传递引用或裸指针(值传递会调用拷贝构造,被禁止)

3.4 常用成员函数

函数说明
get()返回裸指针,不转移所有权
reset()释放当前管理的对象,可以替换为新对象
release()释放所有权,返回裸指针,unique_ptr 变为空
operator bool()判断是否非空
auto up = std::make_unique<Person>();
Person* p = up.release();   // up 放弃所有权,变为空,需手动 delete p
up.reset(new Person);       // 释放原对象,接管新对象
if (up) { /* 非空 */ }

3.5 转移所有权(移动语义)

unique_ptr 不可拷贝,但可以移动(转移所有权)。

std::unique_ptr<Person> up1 = std::make_unique<Person>();
std::unique_ptr<Person> up2 = std::move(up1);   // 所有权转移,up1 变空

函数返回值:编译器会自动应用移动语义(或 RVO)。

std::unique_ptr<Person> createPerson() {
    return std::make_unique<Person>();   // 正确,不会调用拷贝
}
 
int main() {
    auto up = createPerson();   // 移动构造(或 RVO)
}

3.6 函数参数传递

// 推荐:传递引用(不转移所有权)
void usePerson(const std::unique_ptr<Person>& up) {
    up->doSomething();
}
 
// 推荐:传递裸指针(更通用,不依赖智能指针类型)
void usePersonRaw(Person* p) {
    if (p) p->doSomething();
}
 
// 转移所有权:传递 unique_ptr 值(调用者需 std::move)
void takeOwnership(std::unique_ptr<Person> up) {
    // 函数结束时 up 析构,对象被释放
}
 
int main() {
    auto up = std::make_unique<Person>();
    takeOwnership(std::move(up));   // 转移所有权
}

3.7 自定义删除器

对于非 new 分配的资源(如 mallocfopen),可以自定义删除器。

// 管理 malloc 分配的内存
auto deleter = [](int* p) { std::free(p); };
std::unique_ptr<int, decltype(deleter)> up(static_cast<int*>(std::malloc(sizeof(int))), deleter);
 
// 管理 FILE*
auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen("test.txt", "r"), fileDeleter);

C++17 起,可以使用 std::unique_ptr<T[], CustomDeleter> 管理数组,但更推荐使用 std::vectorstd::array

3.8 使用建议

title: unique_ptr 使用建议
- **默认使用 `unique_ptr`**,除非确实需要共享所有权。
- 始终使用 `std::make_unique`(C++14+)创建。
- 函数参数传递:仅观察时传递裸指针或引用;需要转移所有权时传递 `std::unique_ptr` 值(配合 `std::move`)。
- 作为类成员时,默认使用 `unique_ptr` 表示独占所有权。
- 不要使用 `auto_ptr`。

4. shared_ptr(共享所有权)

shared_ptr 使用引用计数管理对象的生命周期。多个 shared_ptr 可以指向同一对象,当最后一个 shared_ptr 被销毁时,对象被释放。

4.1 初始化

#include <memory>
 
// 方法1:使用 new(不推荐)
 
// 方法2:使用 make_shared(推荐)
std::shared_ptr<Person> sp2 = std::make_shared<Person>();
 
// 方法3:从 unique_ptr 转移
std::unique_ptr<Person> up = std::make_unique<Person>();
std::shared_ptr<Person> sp3 = std::move(up);
 
// 方法4:拷贝构造(引用计数 +1)
std::shared_ptr<Person> sp4 = sp2;

4.2 引用计数

auto sp1 = std::make_shared<Person>();
auto sp2 = sp1;                     // 引用计数变为 2
std::cout << sp1.use_count();       // 输出 2
sp2.reset();                        // 引用计数减为 1
// sp1 离开作用域时,引用计数变为 0,对象被销毁

4.3 常用成员函数

函数说明
get()返回裸指针
reset()减少引用计数,若变为 0 则释放对象;可替换为新对象
use_count()返回引用计数(调试用,不应依赖业务逻辑)
operator bool()判断是否非空
unique()判断是否独占(use_count() == 1,C++17 起废弃)

4.4 注意事项

title: shared_ptr 注意事项
- **不要用同一个裸指针初始化多个 `shared_ptr`**(会导致重复释放)。
  ```cpp
  Person* p = new Person;
  std::shared_ptr<Person> sp1(p);
  std::shared_ptr<Person> sp2(p);   // 错误!两个独立控制块
  • 避免循环引用:两个 shared_ptr 互相指向对方,会导致内存泄漏。
  • 尽量使用 make_shared,而不是 new + shared_ptr 构造。
  • 不要将 this 直接交给 shared_ptr(应使用 enable_shared_from_this)。
  • 性能开销:引用计数需要原子操作,比 unique_ptr 稍慢。

#### 4.5 循环引用与 `weak_ptr`

```cpp
struct Node {
    std::shared_ptr<Node> next;   // 循环引用隐患
    ~Node() { std::cout << "~Node\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;   // a 引用 b
    b->next = a;   // b 引用 a,形成循环,引用计数永不归零 → 内存泄漏
}

解决方案:使用 weak_ptr 打破循环。

4.6 自定义删除器

shared_ptr 支持自定义删除器,类型擦除,无需在模板参数中指定类型。

auto deleter = [](Person* p) { delete p; };
std::shared_ptr<Person> sp(new Person, deleter);
 
// 管理 FILE*
std::shared_ptr<FILE> fp(fopen("test.txt", "r"), fclose);

4.7 使用建议

title: shared_ptr 使用建议
- 仅当确实需要共享所有权时才使用 `shared_ptr`,默认应使用 `unique_ptr`。
- 始终使用 `std::make_shared` 创建。
- 使用 `weak_ptr` 打破循环引用。
- 避免将 `shared_ptr` 作为函数参数传递(除非需要共享所有权),传递裸指针或引用即可。
- 注意性能开销,不要滥用。

5. weak_ptr(弱引用)

weak_ptr 是一种不控制对象生命周期的智能指针,它指向由 shared_ptr 管理的对象,但不增加引用计数。通常用于打破循环引用,或观察对象是否存活。

5.1 基本用法

std::shared_ptr<Person> sp = std::make_shared<Person>();
std::weak_ptr<Person> wp = sp;   // 不增加引用计数
 
// 检查对象是否存活
if (auto sp2 = wp.lock()) {      // 尝试提升为 shared_ptr
    sp2->doSomething();
} else {
    std::cout << "对象已销毁\n";
}
 
// 判断是否过期(对象是否已被释放)
if (wp.expired()) { /* ... */ }

5.2 解决循环引用

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;   // 使用 weak_ptr 打破循环
    ~Node() { std::cout << "~Node\n"; }
};
 
int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->prev = a;   // weak_ptr 不增加引用计数
    // 正确释放
}

5.3 注意事项

title: weak_ptr 注意事项
- `weak_ptr` 不能直接解引用,必须通过 `lock()` 提升为 `shared_ptr`。
- 使用前务必检查提升后的 `shared_ptr` 是否为空。
- `weak_ptr` 不适用于需要确保对象存活的场景(应使用 `shared_ptr`)。

6. 最佳实践总结

场景推荐方案
明确独占所有权unique_ptr
共享所有权(如多线程、缓存)shared_ptr + weak_ptr
打破循环引用weak_ptr
观察对象,不延长生命周期weak_ptr
类成员指针根据所有权选择 unique_ptrshared_ptr
函数参数(仅使用,不转移所有权)传递裸指针或引用
函数参数(转移所有权)unique_ptr 值(调用者 std::move
返回堆对象返回 unique_ptr(明确转移所有权)
数组管理优先 vector / array,而非 unique_ptr<T[]>

黄金法则

  • 能用 unique_ptr 就不要用 shared_ptr
  • 永远不要手动 delete 智能指针管理的对象
  • 使用 make_unique / make_shared,避免显式 new
  • 在类成员中,优先使用 unique_ptr 表示独占成员,shared_ptr 表示共享成员

7. 完整示例:综合应用

#include <iostream>
#include <memory>
#include <vector>
 
class Widget {
public:
    Widget(int id) : id_(id) { std::cout << "Widget " << id_ << " created\n"; }
    ~Widget() { std::cout << "Widget " << id_ << " destroyed\n"; }
    void show() const { std::cout << "Widget " << id_ << "\n"; }
private:
    int id_;
};
 
// 工厂函数:返回独占所有权
std::unique_ptr<Widget> createWidget(int id) {
    return std::make_unique<Widget>(id);
}
 
// 观察者:不持有所有权
void useWidget(Widget* w) {
    if (w) w->show();
}
 
int main() {
    // unique_ptr 示例
    auto w1 = createWidget(1);
    useWidget(w1.get());
 
    // 转移所有权
    auto w2 = std::move(w1);
    useWidget(w2.get());
    // w1 已空
 
    // shared_ptr 示例
    auto sw1 = std::make_shared<Widget>(2);
    auto sw2 = sw1;   // 共享所有权
    std::cout << "use_count: " << sw1.use_count() << '\n';
 
    // weak_ptr 示例
    std::weak_ptr<Widget> wp = sw1;
    if (auto locked = wp.lock()) {
        locked->show();
    }
 
    return 0;
}

8. 总结

智能指针所有权开销使用场景
unique_ptr独占零(与裸指针相同)默认选择,绝大多数情况
shared_ptr共享引用计数(原子操作)多个所有者,如缓存、多线程共享
weak_ptr弱引用shared_ptr 相同打破循环引用,观察者模式

现代 C++ 中,不应该再编写 delete 语句。智能指针 + RAII 彻底解决了内存管理问题。