一文详解智能指针
目录
在 C++ 中,智能指针(Smart Pointer) 是封装了裸指针(raw pointer)的模板类,通过RAII(资源获取即初始化) 机制自动管理动态内存的生命周期 —— 当智能指针离开作用域时,会自动释放其指向的动态内存,从而避免内存泄漏、野指针等问题。C++11 及后续标准引入了三种核心智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr,它们各自适用于不同的 ownership(所有权)场景。
一、智能指针的核心设计思想:RAII
RAII 是智能指针的底层逻辑:将资源(如动态内存)的生命周期与对象(智能指针)的生命周期绑定。具体来说:
- 智能指针在构造时获取资源(如通过
new分配内存,并存储指向该内存的裸指针)。 - 智能指针在析构时自动释放资源(调用
delete或delete[])。 - 由于对象的析构函数会被编译器自动调用(当对象离开作用域、被销毁时),因此无需手动管理内存释放。
二、三种核心智能指针详解
1. std::unique_ptr:独占所有权的智能指针
std::unique_ptr 是独占式智能指针:同一时间内,一个对象的所有权只能被一个unique_ptr拥有。当unique_ptr被销毁时,其指向的对象也会被自动释放。
(1)核心特性
- 独占性:不允许复制(拷贝构造和拷贝赋值被禁用),只能通过移动语义(
std::move)转移所有权。 - 轻量高效:大小与裸指针相同(通常为 8 字节),无额外性能开销。
- 支持数组:通过
std::unique_ptr<T[]>专门管理动态数组(自动调用delete[])。
(2)基本用法
#include <memory> // 智能指针所在头文件
#include <iostream>
class MyClass {
public:
MyClass(int id) : id_(id) {
std::cout << "MyClass(" << id_ << ") 构造" << std::endl;
}
~MyClass() {
std::cout << "MyClass(" << id_ << ") 析构" << std::endl;
}
int getId() const { return id_; }
private:
int id_;
};
int main() {
// 1. 初始化:推荐用std::make_unique(C++14引入),更安全高效
std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>(1); // 管理MyClass(1)
// 2. 访问对象:通过->或*操作符,与裸指针类似
std::cout << "对象ID:" << uptr1->getId() << std::endl; // 输出:对象ID:1
// 3. 转移所有权(只能通过std::move)
std::unique_ptr<MyClass> uptr2 = std::move(uptr1); // uptr1失去所有权,变为nullptr
if (uptr1 == nullptr) {
std::cout << "uptr1已失去所有权" << std::endl;
}
// 4. 管理动态数组(需指定T[])
std::unique_ptr<MyClass[]> arr_uptr = std::make_unique<MyClass[]>(2); // 构造2个对象
// 数组元素通过[]访问
std::cout << "数组第0个ID:" << arr_uptr[0].getId() << std::endl; // 输出默认构造的ID(假设为0)
// 5. 手动释放所有权(通过reset())
uptr2.reset(); // 主动释放对象,调用MyClass(1)的析构函数
return 0; // 函数结束时,arr_uptr析构,自动释放数组(调用2次析构)
}
输出结果:
MyClass(1) 构造
对象ID:1
uptr1已失去所有权
MyClass(0) 构造 // 数组第0个元素
MyClass(0) 构造 // 数组第1个元素
数组第0个ID:0
MyClass(1) 析构 // uptr2.reset()触发
MyClass(0) 析构 // 数组元素0析构
MyClass(0) 析构 // 数组元素1析构
(3)适用场景
- 管理独占资源(如局部动态对象、函数返回的动态对象)。
- 作为容器元素(因可移动,适合
std::vector等容器)。 - 替代裸指针作为类的成员变量,避免手动释放内存。
2. std::shared_ptr:共享所有权的智能指针
std::shared_ptr 是共享式智能指针:多个shared_ptr可以共同拥有同一个对象的所有权,通过引用计数(reference count) 跟踪对象被引用的次数。当最后一个shared_ptr被销毁时,对象才会被释放。
(1)核心特性
- 共享性:支持复制(拷贝构造和拷贝赋值),每次复制会使引用计数 + 1;销毁时引用计数 - 1。
- 引用计数:内部维护一个计数器,记录当前有多少个
shared_ptr指向该对象。当计数为 0 时,自动调用delete释放对象。 - 线程安全:引用计数的操作是线程安全的,但对指向的对象的访问需要手动加锁。
(2)基本用法
#include <memory>
#include <iostream>
int main() {
// 1. 初始化:推荐用std::make_shared(更高效,一次性分配对象和引用计数的内存)
std::shared_ptr<MyClass> sptr1 = std::make_shared<MyClass>(2); // 引用计数=1
// 2. 复制:引用计数+1
std::shared_ptr<MyClass> sptr2 = sptr1; // 引用计数=2
std::cout << "当前引用计数:" << sptr1.use_count() << std::endl; // 输出:2
// 3. 访问对象:与unique_ptr相同
std::cout << "对象ID:" << sptr2->getId() << std::endl; // 输出:2
// 4. 手动释放:reset()使当前shared_ptr失去所有权,引用计数-1
sptr1.reset(); // 引用计数=1
std::cout << "sptr1 reset后计数:" << sptr2.use_count() << std::endl; // 输出:1
return 0; // sptr2析构,引用计数=0 → 调用MyClass(2)的析构函数
}
输出结果:
MyClass(2) 构造
当前引用计数:2
对象ID:2
sptr1 reset后计数:1
MyClass(2) 析构
(3)循环引用问题与解决
shared_ptr的最大陷阱是循环引用(circular reference):两个或多个shared_ptr互相引用,导致引用计数无法归零,对象永久无法释放(内存泄漏)。
示例:循环引用导致内存泄漏
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr; // A持有B的shared_ptr
~A() { std::cout << "A析构" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a_ptr; // B持有A的shared_ptr(形成循环)
~B() { std::cout << "B析构" << std::endl; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // a引用b → b的计数=2
b->a_ptr = a; // b引用a → a的计数=2
// 离开作用域时,a和b的计数各减1(变为1),但循环引用导致计数无法归零
// 因此A和B的析构函数不会被调用 → 内存泄漏
return 0;
}
解决方式:用std::weak_ptr打破循环
weak_ptr是shared_ptr的 “观察者”,它不拥有对象的所有权,也不增加引用计数,可用于访问shared_ptr管理的对象,但不会导致循环引用。
3. std::weak_ptr:弱引用智能指针
std::weak_ptr 是一种弱引用,它本身不管理对象的生命周期,仅作为shared_ptr的 “观察者” 存在:
- 必须从
shared_ptr或另一个weak_ptr构造。 - 不增加引用计数,因此不会影响对象的释放。
- 无法直接访问对象,需通过
lock()方法获取一个临时的shared_ptr(若对象已释放,lock()返回nullptr)。
(1)核心特性
- 无所有权:不影响对象的引用计数,对象的释放仅由
shared_ptr决定。 - 解决循环引用:在循环引用场景中,将一方的
shared_ptr改为weak_ptr,打破计数循环。 - 检查有效性:通过
expired()方法判断所观察的对象是否已被释放(true表示已释放)。
(2)基本用法(解决循环引用)
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr; // A仍持有B的shared_ptr
~A() { std::cout << "A析构" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // B持有A的weak_ptr(打破循环)
~B() { std::cout << "B析构" << std::endl; }
};
int main() {
auto a = std::make_shared<A>(); // a计数=1
auto b = std::make_shared<B>(); // b计数=1
a->b_ptr = b; // b计数=2
b->a_ptr = a; // a计数仍为1(weak_ptr不增加计数)
// 离开作用域时:
// a析构 → a计数=0 → A析构 → a->b_ptr析构 → b计数=1
// b析构 → b计数=0 → B析构
return 0;
}
输出结果:
A析构
B析构 // 循环引用被打破,对象正常释放
(3)访问weak_ptr观察的对象
int main() {
auto sptr = std::make_shared<MyClass>(3); // sptr计数=1
std::weak_ptr<MyClass> wptr = sptr; // wptr观察sptr
// 通过lock()获取shared_ptr(若对象有效)
if (auto temp_sptr = wptr.lock()) { // temp_sptr计数=2
std::cout << "对象ID:" << temp_sptr->getId() << std::endl; // 输出:3
} // temp_sptr析构,计数=1
sptr.reset(); // 计数=0 → 对象析构
// 检查对象是否已释放
if (wptr.expired()) {
std::cout << "对象已释放" << std::endl;
}
return 0;
}
输出结果:
MyClass(3) 构造
对象ID:3
MyClass(3) 析构
对象已释放
三、智能指针的使用准则与注意事项
-
优先使用
std::make_unique和std::make_shared
这两个函数比直接用new初始化智能指针更安全(避免内存泄漏风险)、更高效(make_shared一次性分配对象和引用计数内存)。// 推荐 auto uptr = std::make_unique<MyClass>(1); auto sptr = std::make_shared<MyClass>(2); // 不推荐(但合法) std::unique_ptr<MyClass> uptr(new MyClass(1)); -
避免将裸指针交给多个智能指针管理
若多个智能指针同时管理同一个裸指针,会导致重复释放(未定义行为,通常会崩溃)。MyClass* raw_ptr = new MyClass(1); std::unique_ptr<MyClass> uptr1(raw_ptr); // std::unique_ptr<MyClass> uptr2(raw_ptr); // 错误:同一裸指针被两个智能指针管理,会重复释放 -
unique_ptr的移动语义
unique_ptr不可复制,只能通过std::move转移所有权,转移后原指针变为nullptr。 -
shared_ptr的循环引用
始终警惕循环引用问题,用weak_ptr打破双向shared_ptr引用。 -
不要手动删除智能指针管理的裸指针
智能指针会自动释放内存,手动delete会导致重复释放(崩溃)。 -
智能指针与数组
unique_ptr需显式指定T[]以管理数组(自动调用delete[])。shared_ptr默认用delete释放,管理数组时需手动指定删除器(std::default_delete<T[]>):// shared_ptr管理数组的正确方式 std::shared_ptr<MyClass> arr_sptr(new MyClass[2], std::default_delete<MyClass[]>());
四、三种智能指针的对比总结
| 智能指针类型 | 所有权特性 | 引用计数 | 复制 / 移动支持 | 典型应用场景 | 核心优势 |
|---|---|---|---|---|---|
std::unique_ptr | 独占所有权 | 无 | 仅支持移动 | 局部动态对象、容器元素、独占资源 | 轻量高效,无额外开销 |
std::shared_ptr | 共享所有权 | 有 | 支持复制和移动 | 多所有者共享资源(如容器间共享对象) | 灵活共享,自动管理生命周期 |
std::weak_ptr | 无所有权(仅观察) | 不影响 | 支持复制和移动 | 解决shared_ptr的循环引用 | 打破循环,安全观察共享对象 |
五、总结
智能指针是 C++ 动态内存管理的核心工具,通过 RAII 机制自动释放内存,从根本上避免了内存泄漏和野指针问题。实际开发中:
- 优先用
unique_ptr(轻量、高效,适合大多数独占场景)。 - 需共享资源时用
shared_ptr,但警惕循环引用(用weak_ptr解决)。 - 避免手动管理裸指针,尽可能用智能指针替代,写出更安全、可靠的 C++ 代码。
更多推荐


所有评论(0)