1. 功能说明
本指南基于 smart_pointers.cpp 文件,详细介绍了 C++ 中智能指针的各种类型和用法,包括:
- unique_ptr:独占所有权的智能指针,确保同一时间只有一个智能指针管理资源
- shared_ptr:共享所有权的智能指针,使用引用计数管理资源
- weak_ptr:弱引用智能指针,不增加引用计数,用于解决循环引用问题
智能指针是 C++11 引入的重要特性,它们能够自动管理动态内存,避免内存泄漏,提高代码的安全性和可靠性。本指南将详细介绍每种智能指针的使用方法、优缺点以及适用场景。
2. 代码解析
2.1 资源类定义
1 | // 资源类,用于演示智能指针的使用 |
解析:
- 定义了一个
Resource类,用于模拟需要动态管理的资源 - 构造函数和析构函数都有输出信息,便于观察资源的创建和销毁
- 提供了
doSomething()方法和getName()方法,用于演示智能指针对资源的访问
2.2 unique_ptr(独占所有权智能指针)
1 | std::cout << "=== unique_ptr(独占所有权智能指针)===" << std::endl; |
解析:
std::unique_ptr:独占所有权的智能指针,同一时间只能有一个unique_ptr管理资源std::make_unique:C++14 引入的函数,用于创建unique_ptr,比直接使用new更安全std::move:转移unique_ptr的所有权,转移后原unique_ptr变为空reset():重置unique_ptr,会自动删除所管理的资源- 离开作用域时,
unique_ptr会自动删除所管理的资源
2.3 unique_ptr 与数组
1 | std::cout << "\n=== unique_ptr 与数组 ===" << std::endl; |
解析:
std::unique_ptr<T[]>:用于管理数组的unique_ptr特化版本- 离开作用域时,会自动使用
delete[]删除数组,而不是delete
2.4 shared_ptr(共享所有权智能指针)
1 | std::cout << "\n=== shared_ptr(共享所有权智能指针)===" << std::endl; |
解析:
std::shared_ptr:共享所有权的智能指针,使用引用计数管理资源std::make_shared:创建shared_ptr的推荐方法,比直接使用new更高效use_count():获取当前shared_ptr所管理资源的引用计数- 当引用计数为 0 时,资源会被自动删除
2.5 shared_ptr 与自定义删除器
1 | std::cout << "\n=== shared_ptr 与自定义删除器 ===" << std::endl; |
解析:
- 自定义删除器:可以为
shared_ptr指定自定义的删除器,用于特殊的资源释放逻辑 - 删除器可以是函数、函数对象或 lambda 表达式
- 当
shared_ptr离开作用域且引用计数为 0 时,会调用自定义删除器
2.6 weak_ptr(弱引用智能指针)
1 | std::cout << "\n=== weak_ptr(弱引用智能指针)===" << std::endl; |
解析:
std::weak_ptr:弱引用智能指针,不增加引用计数lock():尝试获取一个shared_ptr,如果资源仍然存在则返回有效的shared_ptr,否则返回空shared_ptr- 用于解决循环引用问题,以及需要检查资源是否仍然存在的场景
2.7 weak_ptr 打破循环引用
1 | std::cout << "\n=== weak_ptr 打破循环引用 ===" << std::endl; |
解析:
- 循环引用问题:当两个或多个
shared_ptr相互引用时,会导致引用计数永远不为 0,从而导致内存泄漏 - 解决方案:使用
weak_ptr代替shared_ptr作为反向引用,这样就不会增加引用计数 - 在上面的例子中,
node1->next是shared_ptr,而node2->prev是weak_ptr,这样就打破了循环引用
2.8 shared_ptr 与 std::vector
1 | std::cout << "\n=== shared_ptr 与 std::vector ===" << std::endl; |
解析:
std::vector存储shared_ptr是一种常见的用法,用于管理多个资源- 当向量被销毁时,其中的所有
shared_ptr也会被销毁,引用计数相应减少 - 当引用计数为 0 时,资源会被自动删除
2.9 make_shared vs new
1 | std::cout << "\n=== make_shared vs new(性能比较)===" << std::endl; |
解析:
std::make_shared的优点:- 更简洁,代码可读性更好
- 更高效,只进行一次内存分配(同时分配控制块和资源)
- 更安全,避免了在创建
shared_ptr之前发生异常导致的内存泄漏
- 推荐使用
std::make_shared而不是直接使用new创建shared_ptr
2.10 智能指针与异常安全
1 | std::cout << "\n=== 智能指针与异常安全 ===" << std::endl; |
解析:
- 异常安全:即使在发生异常的情况下,智能指针也能确保资源被正确释放
- 当异常发生时,局部变量会被自动销毁,智能指针的析构函数会被调用,从而释放资源
- 相比之下,使用原始指针时,如果在
new和delete之间发生异常,会导致内存泄漏
2.11 shared_ptr 的引用计数管理
1 | std::cout << "\n=== shared_ptr 的引用计数管理 ===" << std::endl; |
解析:
- 引用计数的变化:
- 创建
shared_ptr时,引用计数为 1 - 复制
shared_ptr时,引用计数增加 shared_ptr离开作用域或被重置时,引用计数减少- 当引用计数为 0 时,资源被删除
- 创建
2.12 智能指针的空检查
1 | std::cout << "\n=== 智能指针的空检查 ===" << std::endl; |
解析:
- 智能指针可以像原始指针一样进行空检查
- 空的智能指针转换为布尔值时为
false,非空的智能指针转换为布尔值时为true
3. 编译和运行说明
3.1 编译命令
使用以下命令编译 smart_pointers.cpp 文件:
1 | g++ -std=c++14 -fexec-charset=GBK -o smart_pointers smart_pointers.cpp |
参数说明:
-std=c++14:使用 C++14 标准(因为使用了std::make_unique,这是 C++14 引入的特性)-fexec-charset=GBK:确保输出的中文字符正确显示-o smart_pointers:指定输出可执行文件名为smart_pointers
3.2 运行命令
编译成功后,使用以下命令运行程序:
1 | ./smart_pointers # Linux/macOS |
3.3 预期输出
运行程序后,您将看到类似以下的输出:
1 | === unique_ptr(独占所有权智能指针)=== |
4. 技术要点
4.1 智能指针的类型和特性
| 智能指针类型 | 所有权 | 引用计数 | 线程安全 | 主要用途 |
|---|---|---|---|---|
| unique_ptr | 独占 | 无 | 否 | 管理不需要共享的资源 |
| shared_ptr | 共享 | 有 | 部分(引用计数操作是线程安全的) | 管理需要共享的资源 |
| weak_ptr | 无 | 无 | 否 | 解决循环引用问题,观察资源是否存在 |
4.2 智能指针的使用场景
unique_ptr 的使用场景
- 独占资源:当资源只需要被一个所有者管理时
- 工厂函数返回值:工厂函数返回新创建的对象时
- 作为成员变量:当类需要独占一个资源时
- 容器元素:当容器需要存储对象的所有权时
- 移动语义:当需要转移资源所有权时
shared_ptr 的使用场景
- 共享资源:当资源需要被多个所有者共享时
- 长期存在的对象:当对象的生命周期由多个部分共同管理时
- 循环依赖:与 weak_ptr 配合使用,解决循环依赖问题
- 多线程环境:当多个线程需要访问同一个对象时(需要注意对象本身的线程安全性)
weak_ptr 的使用场景
- 打破循环引用:当存在 shared_ptr 循环引用时
- 观察者模式:当需要观察一个对象但不希望影响其生命周期时
- 缓存:当需要缓存对象但不希望阻止其被释放时
- 延迟加载:当需要在需要时才获取对象时
4.3 智能指针的性能考虑
内存开销:
unique_ptr:最小,只需要存储一个指针shared_ptr:较大,需要存储指针和引用计数(控制块)weak_ptr:与shared_ptr类似,需要访问控制块
时间开销:
unique_ptr:几乎没有额外开销,与原始指针类似shared_ptr:- 创建:使用
make_shared时开销较小 - 复制:需要原子操作增加引用计数,有一定开销
- 销毁:需要原子操作减少引用计数,有一定开销
- 创建:使用
weak_ptr:- 锁定:需要检查资源是否存在,有一定开销
内存分配:
std::make_shared:只进行一次内存分配,同时分配控制块和资源new+shared_ptr:进行两次内存分配,分别分配资源和控制块
4.4 智能指针的线程安全性
引用计数的线程安全性:
shared_ptr的引用计数操作是线程安全的,多个线程可以同时增加或减少引用计数weak_ptr的lock()操作也是线程安全的
对象访问的线程安全性:
- 智能指针本身不保证对象访问的线程安全性
- 如果多个线程同时访问智能指针管理的对象,需要额外的同步措施(如互斥锁)
注意事项:
- 避免在多个线程之间共享
unique_ptr,因为它不支持并发访问 - 对于
shared_ptr,虽然引用计数是线程安全的,但对象本身可能不是
- 避免在多个线程之间共享
4.5 智能指针的最佳实践
优先使用 unique_ptr:
- 当资源只需要一个所有者时,优先使用
unique_ptr unique_ptr更轻量,语义更清晰
- 当资源只需要一个所有者时,优先使用
合理使用 shared_ptr:
- 当资源需要多个所有者时,使用
shared_ptr - 优先使用
std::make_shared创建shared_ptr - 避免循环引用,使用
weak_ptr打破循环引用
- 当资源需要多个所有者时,使用
正确使用 weak_ptr:
- 使用
weak_ptr解决循环引用问题 - 使用
lock()方法安全地获取shared_ptr - 检查
lock()的返回值,确保资源仍然存在
- 使用
避免混合使用智能指针和原始指针:
- 尽量避免在智能指针和原始指针之间转换
- 如果必须转换,确保原始指针的生命周期不超过智能指针
避免使用智能指针管理非动态内存:
- 智能指针只应该用于管理动态分配的内存
- 不要使用智能指针管理栈上的对象或全局对象
5. 常见问题解答
5.1 unique_ptr 和 shared_ptr 有什么区别?
解答:
- 所有权:
unique_ptr独占所有权,同一时间只能有一个unique_ptr管理资源;shared_ptr共享所有权,多个shared_ptr可以管理同一个资源。 - 引用计数:
unique_ptr不使用引用计数,shared_ptr使用引用计数。 - 性能:
unique_ptr更轻量,几乎没有额外开销;shared_ptr有一定的内存和时间开销。 - 线程安全:
shared_ptr的引用计数操作是线程安全的,unique_ptr不是。 - 使用场景:
unique_ptr适用于资源只需要一个所有者的场景;shared_ptr适用于资源需要多个所有者的场景。
5.2 如何解决 shared_ptr 的循环引用问题?
解答:
- 使用
weak_ptr打破循环引用。当两个或多个shared_ptr相互引用时,将其中一个引用改为weak_ptr,这样就不会增加引用计数。 - 例如,在双向链表中,前向指针使用
shared_ptr,反向指针使用weak_ptr。
5.3 make_shared 和 new 有什么区别?
解答:
- 内存分配:
std::make_shared只进行一次内存分配,同时分配控制块和资源;new+shared_ptr进行两次内存分配,分别分配资源和控制块。 - 异常安全:
std::make_shared更安全,避免了在创建shared_ptr之前发生异常导致的内存泄漏。 - 代码可读性:
std::make_shared更简洁,代码可读性更好。 - 内存使用:
std::make_shared创建的对象和控制块在同一块内存中,可能会导致对象的内存比实际需要的时间更长(因为控制块可能被weak_ptr引用)。
5.4 智能指针如何与异常处理交互?
解答:
- 智能指针提供了异常安全的内存管理。即使在发生异常的情况下,智能指针也能确保资源被正确释放。
- 当异常发生时,局部变量会被自动销毁,智能指针的析构函数会被调用,从而释放资源。
- 相比之下,使用原始指针时,如果在
new和delete之间发生异常,会导致内存泄漏。
5.5 如何在智能指针和原始指针之间转换?
解答:
- 从智能指针获取原始指针:
unique_ptr:使用get()方法shared_ptr:使用get()方法
- 从原始指针创建智能指针:
unique_ptr:std::unique_ptr<T> ptr(new T)或std::make_unique<T>()shared_ptr:std::shared_ptr<T> ptr(new T)或std::make_shared<T>()
- 注意事项:
- 避免将同一个原始指针同时用于多个智能指针
- 避免使用智能指针管理由其他方式分配的内存
- 确保原始指针的生命周期不超过智能指针
5.6 智能指针可以管理数组吗?
解答:
- unique_ptr:可以管理数组,使用
std::unique_ptr<T[]>特化版本,会自动使用delete[]删除数组。 - shared_ptr:可以管理数组,但需要指定自定义删除器,因为默认情况下会使用
delete而不是delete[]。- 例如:
std::shared_ptr<int[]> arr(new int[5], std::default_delete<int[]>()); - 或者使用
std::make_shared配合std::unique_ptr:std::shared_ptr<int[]> arr(std::make_unique<int[]>(5).release());
- 例如:
5.7 智能指针的控制块是什么?
解答:
- 控制块是
shared_ptr和weak_ptr用于管理资源的内部数据结构,包含以下信息:- 引用计数(被
shared_ptr引用的次数) - 弱引用计数(被
weak_ptr引用的次数) - 自定义删除器(如果有)
- 自定义分配器(如果有)
- 引用计数(被
- 控制块的内存分配:
- 使用
std::make_shared时,控制块和资源在同一块内存中分配 - 使用
new+shared_ptr时,控制块和资源在不同的内存块中分配
- 使用
- 弱引用计数:当弱引用计数为 0 时,控制块会被销毁
6. 代码优化建议
6.1 智能指针的选择
优先使用 unique_ptr:
- 当资源只需要一个所有者时,优先使用
unique_ptr unique_ptr更轻量,语义更清晰- 示例:
1
2
3
4
5// 推荐
std::unique_ptr<Resource> ptr = std::make_unique<Resource>("A");
// 不推荐
std::shared_ptr<Resource> ptr = std::make_shared<Resource>("A");
- 当资源只需要一个所有者时,优先使用
合理使用 shared_ptr:
- 当资源需要多个所有者时,使用
shared_ptr - 示例:
1
2
3// 当需要共享所有权时
std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>("Shared");
std::shared_ptr<Resource> ptr2 = ptr1; // 共享所有权
- 当资源需要多个所有者时,使用
6.2 智能指针的创建
- 使用 make_shared 和 make_unique:
- 优先使用
std::make_shared和std::make_unique创建智能指针 - 更简洁,更高效,更安全
- 示例:
1
2
3
4
5
6
7// 推荐
auto ptr1 = std::make_shared<Resource>("A");
auto ptr2 = std::make_unique<Resource>("B");
// 不推荐
std::shared_ptr<Resource> ptr1(new Resource("A"));
std::unique_ptr<Resource> ptr2(new Resource("B"));
- 优先使用
6.3 智能指针的使用
避免不必要的复制:
- 对于
shared_ptr,避免不必要的复制,因为这会增加引用计数的开销 - 当只需要访问对象而不需要共享所有权时,使用引用或指针
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13// 推荐:使用引用
void processResource(const Resource& res) {
res.doSomething();
}
// 调用
std::shared_ptr<Resource> ptr = std::make_shared<Resource>("A");
processResource(*ptr);
// 不推荐:不必要的复制
void processResource(std::shared_ptr<Resource> res) {
res->doSomething();
}
- 对于
使用移动语义:
- 对于
unique_ptr,当需要转移所有权时,使用std::move - 示例:
1
2std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>("A");
std::unique_ptr<Resource> ptr2 = std::move(ptr1); // 转移所有权
- 对于
6.4 智能指针与容器
- 在容器中存储智能指针:
- 当容器需要管理对象的生命周期时,存储智能指针
- 对于
unique_ptr,需要使用移动语义或 C++11 后的容器 - 示例:
1
2
3
4
5
6
7// 存储 unique_ptr
std::vector<std::unique_ptr<Resource>> resources;
resources.push_back(std::make_unique<Resource>("A"));
// 存储 shared_ptr
std::vector<std::shared_ptr<Resource>> resources;
resources.push_back(std::make_shared<Resource>("A"));
6.5 智能指针与继承
- 使用智能指针管理多态对象:
- 智能指针可以很好地管理多态对象
- 确保基类有虚析构函数
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Base {
public:
virtual ~Base() = default;
virtual void doSomething() = 0;
};
class Derived : public Base {
public:
void doSomething() override {
std::cout << "Derived::doSomething()" << std::endl;
}
};
// 使用智能指针管理多态对象
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
ptr->doSomething(); // 多态调用
6.6 智能指针与异常处理
- 利用智能指针的异常安全性:
- 当在函数中分配资源并可能抛出异常时,使用智能指针
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 推荐:使用智能指针
void function() {
auto ptr = std::make_unique<Resource>("A");
// 可能抛出异常的代码
throw std::runtime_error("Error");
// 即使抛出异常,ptr 也会自动释放资源
}
// 不推荐:使用原始指针
void function() {
Resource* ptr = new Resource("A");
// 可能抛出异常的代码
throw std::runtime_error("Error");
// 抛出异常后,delete 不会被执行,导致内存泄漏
delete ptr;
}
6.7 智能指针的自定义删除器
- 使用 lambda 表达式作为自定义删除器:
- 当需要特殊的资源释放逻辑时,使用自定义删除器
- lambda 表达式是定义自定义删除器的便捷方式
- 示例:
1
2
3
4
5
6
7
8
9// 自定义删除器
auto deleter = [](Resource* r) {
std::cout << "Custom deleter" << std::endl;
delete r;
};
// 使用自定义删除器
std::unique_ptr<Resource, decltype(deleter)> ptr(new Resource("A"), deleter);
std::shared_ptr<Resource> ptr2(new Resource("B"), deleter);
7. 代码优化示例
7.1 从原始指针到智能指针的转换
优化前:
1 | void process() { |
优化后:
1 | void process() { |
7.2 解决循环引用问题
优化前:
1 | class Node { |
优化后:
1 | class Node { |
7.3 智能指针与工厂模式
优化前:
1 | class Product { |
优化后:
1 | class Product { |
7.4 智能指针与容器
优化前:
1 | void process() { |
优化后:
1 | void process() { |
7.5 智能指针与多线程
优化前:
1 | void threadFunction(Resource* res) { |
优化后:
1 | void threadFunction(std::shared_ptr<Resource> res) { |
8. 总结
C++ 智能指针是现代 C++ 中管理动态内存的重要工具,它们能够自动管理资源的生命周期,避免内存泄漏,提高代码的安全性和可靠性。本指南详细介绍了三种主要的智能指针:
unique_ptr:独占所有权的智能指针,适用于资源只需要一个所有者的场景,是最轻量的智能指针。
shared_ptr:共享所有权的智能指针,使用引用计数管理资源,适用于资源需要多个所有者的场景。
weak_ptr:弱引用智能指针,不增加引用计数,主要用于解决循环引用问题,以及观察资源是否存在。
8.1 核心要点
- RAII 原则:智能指针基于 RAII(资源获取即初始化)原则,利用对象的生命周期管理资源。
- 自动内存管理:智能指针能够自动释放所管理的资源,避免内存泄漏。
- 异常安全:即使在发生异常的情况下,智能指针也能确保资源被正确释放。
- 所有权管理:智能指针明确表达了资源的所有权关系,使代码更易于理解和维护。
- 性能考虑:不同的智能指针有不同的性能特点,应根据具体场景选择合适的智能指针。
8.2 最佳实践
- 优先使用 unique_ptr:当资源只需要一个所有者时,优先使用
unique_ptr。 - 合理使用 shared_ptr:当资源需要多个所有者时,使用
shared_ptr。 - 使用 make_shared 和 make_unique:优先使用
std::make_shared和std::make_unique创建智能指针。 - 避免循环引用:使用
weak_ptr打破shared_ptr的循环引用。 - 注意异常安全:利用智能指针的异常安全性,避免手动管理内存导致的内存泄漏。
- 考虑性能:根据具体场景选择合适的智能指针,平衡安全性和性能。
8.3 应用场景
- 资源管理:管理动态分配的内存、文件句柄、网络连接等资源。
- 工厂模式:工厂函数返回智能指针,确保资源被正确释放。
- 容器:在容器中存储智能指针,管理对象的生命周期。
- 多线程:在多线程环境中安全地共享资源。
- 观察者模式:使用
weak_ptr实现观察者模式,避免内存泄漏。
通过掌握智能指针的使用方法和最佳实践,您可以编写更安全、更可靠、更易于维护的 C++ 代码,避免内存泄漏等常见问题,提高代码质量和开发效率。
smart_pointers.cpp
1 |
|