C++ 智能指针详解
C++智能指针是自动管理动态内存的RAII工具,主要包括unique_ptr、shared_ptr和weak_ptr三种类型。智能指针能有效防止内存泄漏,提升代码安全性和可维护性。
目录
引言
智能指针是 C++ 标准库提供的RAII风格工具,核心作用是自动管理动态内存,避免手动调用
delete导致的内存泄漏、野指针、重复释放等问题。它本质是封装了原始指针的类模板,利用析构函数的自动调用特性,在智能指针对象生命周期结束时(如出作用域、被销毁)自动释放所管理的内存。让我全面深入地介绍 C++ 的智能指针。RAII(资源获取即初始化)是 C++ 中一种非常重要的编程惯用法(idiom)。它的核心思想是:
将资源的生命周期与对象的生命周期绑定。
具体来说:
获取资源:在对象的构造函数中分配资源(如:动态内存、文件句柄、锁、网络连接等)。
释放资源:在对象的析构函数中释放资源。
自动管理:当对象离开作用域时,C++ 会保证其析构函数被自动调用,从而确保资源被正确释放 —— 即使发生异常也是如此。
为什么需要 RAII?
在没有 RAII 的传统 C 风格代码中,程序员需要手动配对资源获取与释放(如
malloc/free、fopen/fclose)。这极易导致:
资源泄漏:忘记调用释放函数。
重复释放:逻辑错误导致多次释放。
异常不安全:如果函数在获取资源后、释放资源前抛出异常,释放代码永远不会被执行。
RAII 通过构造函数+析构函数+栈展开机制,从根本上解决了这些问题。
一、智能指针概述
1. 为什么需要智能指针?
传统指针的问题,一些操作可能抛出异常或提前返回(比如忘记 delete,内存泄漏!)
// 使用智能指针
#include <memory>
void smartPointer() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 不需要手动 delete,离开作用域自动释放
// 即使抛出异常,也能正确释放
}
2. 智能指针类型
C++11 及之后标准中,常用的智能指针有 3 种:
std::unique_ptr、std::shared_ptr、std::weak_ptr,均定义在<memory>头文件中。
| 智能指针 | 所有权 | 引用计数 | 可拷贝 | 适用场景 |
|---|---|---|---|---|
std::unique_ptr |
独占 | 无 | 否 | 独占所有权,轻量级 |
std::shared_ptr |
共享 | 有 | 是 | 多个所有者共享资源 |
std::weak_ptr |
弱引用 | 无(配合shared_ptr) | 是 | 解决循环引用,观察者 |
二、unique_ptr 详解
unique_ptr是独占所有权的智能指针:同一时间,只有一个unique_ptr能指向某块内存,不允许拷贝(C++11 仅支持移动),销毁时释放内存。
1. 基本使用
- 方式1:直接new
- 方式2:make_unique (C++14)
- 方式3:构造函数
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
void doSomething() { std::cout << "Doing something\n"; }
};
// 创建 unique_ptr
std::unique_ptr<Resource> ptr1(new Resource()); // 方式1:直接new
auto ptr2 = std::make_unique<Resource>(); // 方式2:make_unique (C++14)
auto ptr3 = std::unique_ptr<Resource>(new Resource()); // 方式3:构造函数
// C++14 之前需要自己实现 make_unique
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
2. unique_ptr 的特性
- 独占所有权,不能拷贝
- 可以作为函数返回值
- 可以放入容器
- 自定义删除器
// 1. 独占所有权,不能拷贝
auto ptr = std::make_unique<int>(42);
// auto ptr2 = ptr; // 错误:不能拷贝
auto ptr2 = std::move(ptr); // 正确:转移所有权
// ptr 现在为空
// 2. 可以作为函数返回值
std::unique_ptr<Resource> createResource() {
return std::make_unique<Resource>(); // 移动语义自动应用
}
// 3. 可以放入容器
std::vector<std::unique_ptr<Resource>> resources;
resources.push_back(std::make_unique<Resource>());
resources.push_back(std::make_unique<Resource>());
// 遍历
for (auto& res : resources) {
res->doSomething();
}
// 4. 自定义删除器
auto customDeleter = [](FILE* f) {
std::cout << "Closing file\n";
if (f) fclose(f);
};
std::unique_ptr<FILE, decltype(customDeleter)>
filePtr(fopen("test.txt", "w"), customDeleter);
3. unique_ptr 的常用操作
auto ptr = std::make_unique<int>(42);
// 获取原始指针
int* rawPtr = ptr.get();
std::cout << *rawPtr << std::endl; // 42
// 释放所有权但不删除对象
int* releasedPtr = ptr.release(); // ptr 变为空
delete releasedPtr; // 需要手动删除
// 重置指针
ptr.reset(); // 删除原对象,ptr 变为空
ptr.reset(new int(100)); // 删除原对象,指向新对象
// 检查是否为空
if (ptr) {
std::cout << "ptr is not null\n";
}
// 交换
auto ptr1 = std::make_unique<int>(1);
auto ptr2 = std::make_unique<int>(2);
ptr1.swap(ptr2); // ptr1 现在指向 2,ptr2 指向 1
4. unique_ptr 与数组
// C++11 支持数组
std::unique_ptr<int[]> arrPtr(new int[10]);
// C++14 make_unique 也支持数组
auto arrPtr2 = std::make_unique<int[]>(10);
// 使用
for (int i = 0; i < 10; ++i) {
arrPtr2[i] = i * i;
}
// 不需要手动 delete[]
三、shared_ptr 详解
shared_ptr是共享所有权的智能指针:多个shared_ptr可以指向同一块内存,内部通过引用计数(refcount)管理 —— 当最后一个指向该内存的shared_ptr销毁时,才释放内存。
1. 基本用法
#include <iostream>
#include <memory>
//Test是一个简单演示的类
int main() {
// 1. 创建shared_ptr
std::shared_ptr<Test> ptr1(new Test(10));
std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出1
// 2. 拷贝(共享所有权,引用计数+1)
std::shared_ptr<Test> ptr2 = ptr1;
std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出2
// 3. 重置(引用计数-1,若为0则释放内存)
ptr1.reset();
std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出1
// 4. 推荐创建方式:std::make_shared(更高效,避免内存碎片)
std::shared_ptr<Test> ptr3 = std::make_shared<Test>(20);
return 0; // ptr2、ptr3销毁,引用计数归0,Test(10)、Test(20)析构
}
2. 关键特性
- 引用计数:每次拷贝
shared_ptr,计数 + 1;析构 / 重置,计数 - 1;- 线程安全:
- 引用计数的修改是原子操作(多线程拷贝 / 销毁
shared_ptr安全);- 但指向的对象本身不是线程安全的(多线程修改对象需加锁);
- 不支持数组(C++17 前):若管理数组需自定义删除器,如
std::shared_ptr<int> arr(new int[5], [](int* p) { delete[] p; });;- 常用场景:管理共享资源(如多个对象共享同一个动态资源)。
3. 陷阱:循环引用
shared_ptr的最大问题是循环引用—— 两个shared_ptr互相指向对方,导致引用计数无法归 0,内存泄漏。示例:class A; class B; class A { public: std::shared_ptr<B> b_ptr; ~A() { std::cout << "A 析构" << std::endl; } }; class B { public: std::shared_ptr<A> a_ptr; ~B() { std::cout << "B 析构" << std::endl; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b_ptr = b; // A引用B b->a_ptr = a; // B引用A // 函数结束时,a和b的引用计数都是2,无法析构,内存泄漏 return 0; }解决方案:用
std::weak_ptr打破循环引用。
4. 自定义删除器
// 文件资源管理
auto fileDeleter = [](FILE* file) {
std::cout << "Closing file\n";
if (file) {
fclose(file);
}
};
std::shared_ptr<FILE> filePtr(fopen("test.txt", "w"), fileDeleter);
// 数组资源
auto arrayDeleter = [](int* p) {
std::cout << "Deleting array\n";
delete[] p;
};
std::shared_ptr<int> arrPtr(new int[100], arrayDeleter);
// 使用默认的 default_delete
std::shared_ptr<int> arrPtr2(new int[100], std::default_delete<int[]>());
四、weak_ptr 详解
weak_ptr是为解决shared_ptr循环引用设计的弱引用指针:
- 不拥有内存所有权,仅观察
shared_ptr管理的内存;- 不增加引用计数,不影响内存的释放;
- 可以从
shared_ptr构造,也可以转换为shared_ptr(通过lock())。
1. 解决循环引用
#include <iostream>
#include <memory>
class A;
class B;
class A {
public:
std::weak_ptr<B> b_ptr; // 改为weak_ptr
~A() { std::cout << "A 析构" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 改为weak_ptr
~B() { std::cout << "B 析构" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// weak_ptr不增加引用计数,a和b的引用计数仍为1,函数结束时正常析构
return 0;
}
2. 核心方法
lock():将weak_ptr转换为std::shared_ptr,若原内存已释放则返回空shared_ptr(安全访问对象的关键);expired():判断weak_ptr指向的内存是否已释放(返回true表示已释放);use_count():获取对应的shared_ptr的引用计数(仅观察,不修改)。
示例:安全访问对象
std::weak_ptr<Test> wp;
{
std::shared_ptr<Test> sp = std::make_shared<Test>(10);
wp = sp;
std::cout << "expired: " << wp.expired() << std::endl; // false
// 转换为shared_ptr并访问
if (auto temp_sp = wp.lock()) {
temp_sp->show(); // 安全访问
}
} // sp销毁,内存释放
std::cout << "expired: " << wp.expired() << std::endl; // true
if (auto temp_sp = wp.lock()) {
temp_sp->show(); // 不会执行,temp_sp为空
}
五、智能指针的使用原则
- 优先使用
std::unique_ptr:独占场景下效率最高,语义清晰;- 避免用智能指针管理非动态内存(如栈上对象):
std::unique_ptr<int> ptr(&x);(x 是栈变量,析构时会重复释放,崩溃);- 避免裸指针与智能指针混用:裸指针可能被智能指针自动释放,导致野指针;
std::make_shared优先于直接构造:std::make_shared<Test>(10)比std::shared_ptr<Test>(new Test(10))更高效(一次内存分配),且更安全(避免异常导致内存泄漏);- 自定义删除器:若管理的资源不是
new分配的(如malloc、文件句柄),需自定义删除器;
总结
- 核心作用:智能指针通过 RAII 自动管理动态内存,避免内存泄漏,核心析构函数自动释放资源;
- 类型选择:
unique_ptr:独占所有权,轻量级,优先使用;shared_ptr:共享所有权,通过引用计数管理,注意循环引用;weak_ptr:弱引用,解决shared_ptr循环引用,不影响内存释放;- 最佳实践:优先用
std::make_shared创建shared_ptr,避免裸指针与智能指针混用,不管理栈内存。
更多推荐
所有评论(0)