1 前言
- 传统C/C++编程中,使用new或者malloc动态申请内存后,必须手动调用delete或者free去释放,否则就会造成内存泄漏。在C++ 11语法中,提供了智能指针来管理内存,开发者不必再关心内存的释放问题,智能指针可以自动去释放管理的内存空间。
- C++ 11语法中,主要提供了两种智能指针,shared_ptr 和 unique_ptr。shared_ptr允许多个指针指向同一对象,unique_ptr 则"独占"所指向的对象。
2 shared_ptr
- 共享的智能指针,允许多个指针指向同一个对象。
- shared_ptr大小一般为裸指针大小的两倍。它内部包含一个指向到资源的裸指针,还包含一个指向该资源引用计数的裸指针。
2.1 shared_ptr的初始化
- 初始化一个基本类型
-
// 动态申请一个int类型,值初始化为 10086 std::shared_ptr<int> ptr1(new int(10086));
-
- 通过make_shared
-
// 使用make_shared动态申请内存(推荐此写法) std::shared_ptr<int> ptr3 = std::make_shared<int>();
-
- 初始化一个类对象
-
// 动态申请一个Base类对象 std::shared_ptr<Base> ptr4 = std::make_shared<Base>();
-
2.2 关于内存对象的释放问题
- 使用
new
申请的对象,必须手动调用delete
去释放资源,而使用shared_ptr
申请的对象,离开其作用域时会自动释放资源。通过一个示例演示下。 - 代码
-
#include <iostream> #include <memory> class Base { public: Base() { std::cout << "Base" << std::endl; } void print() { std::cout << "I am Base" << std::endl; } ~Base() { std::cout << "~Base" << std::endl; } }; int main() { { Base *pBase = new Base; pBase->print(); } { std::shared_ptr<Base> ptr4 = std::make_shared<Base>(); ptr4->print(); } system("pause"); return 0; }
-
- 执行结果
-
Base I am Base Base I am Base ~Base 请按任意键继续. . .
-
2.3 shared_ptr的引用计数
- 共享智能指针允许智能指针可以同时管理同一块有效的内存。当进行拷贝或赋值操作时,每个
shared_ptr
都会记录有多少个shared_ptr
指向相同的对象。 - 这个记录值可以称为引用计数,当拷贝一个
shared_ptr
时,引用计数就会加一,当shared_ptr
离开作用域时,引用计数就会减一。 - 当一个
shared_ptr
的引用计数变为0时,就会自动释放自己所管理的对象。 - 可以通过调用
use_count()
接口来获取当前智能指针的引用计数。通过实例演示下 - 代码
-
#include <iostream> #include <memory> int main() { { std::shared_ptr<int> ptr1(new int(10086)); std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl; std::shared_ptr<int> ptr2 = ptr1; std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl; std::shared_ptr<int> ptr3(ptr1); std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl; } system("pause"); return 0; }
-
- 执行结果
-
ptr1.use_count(): 1 ptr1.use_count(): 2 ptr1.use_count(): 3 请按任意键继续. . .
-
- 也可以调用
reset
方法手动释放。调用reset
后,share_ptr
对应的引用计数就会减1,当减到0时就会释放资源。
2.4 shared_ptr删除器介绍
shared_ptr
默认不支持数组的管理,因为它的默认删除器本质上就是调用delete
,因此无法释放数组。- 比如这段代码
std::shared_ptr<int> ptr2(new int[5]{0});
,申请了一个整型数组,但shared_ptr
释放对象只会调用delete
而不是delete []
释放,因此只会释放数组中的第一块空间,后面的内存就不会释放了。 - 为了有直观的感受,我们申请一块类对象数组来看下效果。
- 代码
-
#include <iostream> #include <memory> class Base { public: Base() { std::cout << "Base" << std::endl; } void print() { std::cout << "I am Base" << std::endl; } ~Base() { std::cout << "~Base" << std::endl; } }; int main() { { std::shared_ptr<Base> ptr(new Base[3]); ptr->print(); } system("pause"); return 0; }
-
- 打印结果。
-
Base Base Base I am Base ~Base
-
- 可以看到,只释放了一块对象。需要说明的是,上面这段代码会产生未定义行为而导致程序崩溃。
- 使用删除器就可以解决这个问题,修改代码如下
-
#include <iostream> #include <memory> class Base { public: Base() { std::cout << "Base" << std::endl; } void print() { std::cout << "I am Base" << std::endl; } ~Base() { std::cout << "~Base" << std::endl; } }; void deletePtr(Base* p) { delete[] p; } int main() { { // 写法, 手动调用自定义删除器 std::shared_ptr<Base> ptr(new Base[2], deletePtr); ptr->print(); } { // 写法2, 使用lamaba表达式 std::shared_ptr<Base> ptr(new Base[2], [](Base* p) {delete[] p; }); } { // 写法3, 使用c++提供的删除器,要释放数组时就传入数组类型 std::shared_ptr<Base> ptr(new Base[2], std::default_delete<Base[]>()); } system("pause"); return 0; }
-
- 打印结果
-
Base Base I am Base ~Base ~Base Base Base ~Base ~Base Base Base ~Base ~Base 请按任意键继续. . .
-
- 需要注意的是,C++17中,shared_ptr已经支持管理数组,可以直接传入数组类型
std::shared_ptr<int[]> ptr2(new int[5]{0});
,不必再指定自定义删除器。 - 使用删除器不仅可以管理数组,也可以管理文件开关,数据库的连接与断开等。
- 下面通过一个实例看下通过删除器如何管理文件。
-
#include <iostream> #include <memory> #include <fstream> class CFileDelete { public: CFileDelete(const std::string strFile):mStrFile(strFile){ } void operator()(std::ofstream* fp) { fp->close(); } private: std::string mStrFile; }; int main() { { std::shared_ptr<std::ofstream> fp(new std::ofstream("test.exe"), CFileDelete("test.txt")); } system("pause"); return 0; }
-
2.5 获取原始指针
- 使用shared_ptr管理数组时,不允许通过下标访问数组。这个时候就可以通过
get
来获取shared_ptr
的原始指针来访问数组中的数据。具体示例如下-
#include <iostream> #include <memory> int main() { { int num = 5; std::shared_ptr<int> ptr(new int[num] {0}); // 通过get获取原始指针来访问数组对象 int *p = ptr.get(); for (int i = 0; i < num; i++) { p[i] = i; std::cout << p[i] << " "; } std::cout << std::endl; } system("pause"); return 0; }
-
- 打印结果
-
0 1 2 3 4 请按任意键继续. . .
-
2.6 避免通过同一个裸指针创建多个share_ptr对象
- 先看以下一段代码
-
#include <iostream> #include <memory> int main() { { int* pData = new int; std::shared_ptr<int> ptr1(pData); std::shared_ptr<int> ptr2(pData); std::cout << "ptr1.use_count: " << ptr1.use_count() << std::endl; std::cout << "ptr2.use_count: " << ptr2.use_count() << std::endl; } system("pause"); return 0; }
-
- 这段代码会导致程序崩溃,因为ptr1和ptr2是没有关联的,引用计数都为1,当离开作用域时都会调用
delete
去释放内存,这就造成同一块内存delete
了两次,会导致程序崩溃。实际使用时要避免这种写法。
2.7 shared_ptr的线程安全
- 一个shared_ptr对象可以被多个线程同时读
- 两个shared_ptr对象可以被两个线程同时读写,即使它们管理的是同一个对象
- 多个线程读写同一个shared_ptr对象,那么需要加锁
3 unique_ptr
- unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,即多个unique_ptr不能指向同一块内存区域。
3.1 unique_ptr的初始化
- 初始化一个基本类型
-
// 动态申请一个int类型,值初始化为 10086 std::unique_ptr<int> ptr1(new int(10086));
-
- 初始化一个类对象
-
// 动态申请一个Base类对象 std::unique_ptr<Base> ptr1(new Base);
-
- unique_ptr不允许进行赋值操作
-
std::unique_ptr<int> ptr1(new int); std::unique_ptr<int> ptr2 = ptr1; // 不允许
-
3.2 unique_ptr管理数组
- 使用unique_ptr来管理数组时,可以直接使用下标来访问或者修改元素值。
- 代码
-
#include <iostream> #include <memory> int main() { { int num = 5; std::unique_ptr<int[]> ptr(new int[num]{0}); // 修改数组元素 for (int i = 0; i < num; i++) { ptr[i] = i; } // 访问数组元素 for (int i = 0; i < num; i++) { std::cout << ptr[i] << " "; } std::cout << std::endl; } system("pause"); return 0; }
-
- 打印结果
-
0 1 2 3 4 请按任意键继续. . .
-
- 使用
unique_ptr
管理数组时,内存释放问题。 - 上面介绍过,使用
shared_ptr
来管理数组,默认调用delete
释放对象,因此必须指定删除器。而使用unique_ptr管理数组时,会调用delete[]
,这样就不必手动指定删除器。具体看以下示例 - 代码
-
#include <iostream> #include <memory> class Base { public: Base() { std::cout << "Base" << std::endl; } void print() { std::cout << "I am Base" << std::endl; } ~Base() { std::cout << "~Base" << std::endl; } }; int main() { { int num = 3; std::unique_ptr<Base[]> ptr(new Base[num]); for (int i = 0; i < num; i++) { ptr[i].print(); } } system("pause"); return 0; }
-
- 打印结果。可以看到,没有手动指定删除器,数组对象也都全部释放了。
-
Base Base Base I am Base I am Base I am Base ~Base ~Base ~Base 请按任意键继续. . .
-
3.3 unique_ptr删除器介绍
-
上面介绍过shared_ptr的删除器,本质上就是调用
delete
,而unique_ptr
的删除器,本质上是调用delete []
, 这也是它为什么可以直接管理数组的原因。 -
unique_ptr的删除器与shared_ptr稍有不同。shared_ptr的删除器是它的构造函数的一个参数,而unique_ptr的删除器是它的模板参数,是类型的一部分。
-
// shared_ptr删除器 template< class Y, class Deleter> shared_ptr( Y* ptr, Deleter d ); // unique_ptr删除器 template<class T, class Deleter = std::default_delete<T>> class unique_ptr;
-
-
具体写法区别如下
-
// 自定义shared_ptr删除器写法 std::shared_ptr<std::ofstream> fps(new std::ofstream("test.exe"), CFileDelete("test.txt")); // 自定义unique_ptr删除器写法 std::unique_ptr<std::ofstream, CFileDelete> fpu(new std::ofstream("test.exe"), CFileDelete("test.txt"));
-
3.4 典型应用:实现工厂模式
- 代码
-
#include <iostream> #include <memory> #include <string> class CBase { public: CBase() { std::cout << "CBase " << std::endl; } virtual ~CBase() { std::cout << "~CBase " << std::endl; } }; class CSubA : public CBase { public: CSubA() { std::cout << "CSubA " << std::endl; } virtual ~CSubA() { std::cout << "~CSubA " << std::endl; } }; class CSubB : public CBase { public: CSubB() { std::cout << "CSubB " << std::endl; } virtual ~CSubB() { std::cout << "~CSubB " << std::endl; } }; // 实现工厂方法 std::unique_ptr<CBase> CBaseMakeU(std::string name) { std::unique_ptr<CBase> pBase; if (name.compare("CSubA") == 0) { pBase.reset(new CSubA); } else if (name.compare("CSubB") == 0) { pBase.reset(new CSubB); } else { pBase.reset(new CBase); } return pBase; } int main() { { std::unique_ptr<CBase> base = CBaseMakeU("CSubA"); } system("pause"); return 0; }
-
- 执行结果
-
CBase CSubA ~CSubA ~CBase 请按任意键继续. . .
-
4 weak_ptr
weak_ptr
是 C++ 标准库中的一个智能指针类型,主要用于解决shared_ptr
引用循环的问题。weak_ptr
不增加所指向对象的引用计数,因此不会影响对象的生命周期。它通常与shared_ptr
一起使用,提供了一种观察共享对象而不增加其引用计数的方法。
4.1 weak_ptr主要特点
- 不增加引用计数:
weak_ptr
不持有对象的所有权,因此不会增加对象的引用计数。- 当最后一个
shared_ptr
被销毁时,对象会被删除,即使还有weak_ptr
存在。
- 防止引用循环:
- 在某些情况下,多个
shared_ptr
之间可能存在循环引用,导致对象无法被正确释放。使用weak_ptr
可以打破这种循环引用。
- 在某些情况下,多个
- 临时访问:
weak_ptr
提供了lock()
方法,可以临时获取一个shared_ptr
,以便在需要时访问对象。- 如果对象已经被删除,
lock()
会返回一个空的shared_ptr
。
- 检测对象是否已删除:
weak_ptr
提供了expired()
方法,可以检查对象是否已经被删除。
4.2 weak_ptr 构造
- 代码
-
#include <iostream> #include <memory> int main() { { std::shared_ptr<int> ptrs = std::make_shared<int>(0); std::cout <<"ptrs.use_count: " << ptrs.use_count() << std::endl; std::weak_ptr<int> ptrw(ptrs); std::cout << "ptrs.use_count: " << ptrs.use_count() << std::endl; std::cout << "ptrw.use_count: " << ptrw.use_count() << std::endl; } system("pause"); return 0; }
-
- 打印结果
-
ptrs.use_count: 1 ptrs.use_count: 1 ptrw.use_count: 1 请按任意键继续. . .
-
4.3 weak_ptr lock方法
- 一个典型的使用场景如下
-
#include <iostream> #include <memory> int main() { { std::shared_ptr<int> ptrs = std::make_shared<int>(10010); std::weak_ptr<int> ptrw(ptrs); ptrs.reset(); // lock方法会检查当前指针是否有效,如果有效则返回对应指针,如果无效则返回NULL std::shared_ptr<int> ptrs1 = ptrw.lock(); if (ptrs1) { std::cout << "*ptrs1" << *ptrs1 << std::endl; } else { std::cout << "ptrs1 is null" << std::endl; } } system("pause"); return 0; }
-
4.4 通过weak_ptr解决循环引用问题
- 先看以下一段代码,了解下循环引用的产生场景以及导致的问题
-
#include <iostream> #include <memory> class B; class A { public: A() { std::cout << "A " << std::endl; } ~A() { std::cout << "~A " << std::endl; } void setB(std::shared_ptr<B> &pb) { mPb = pb; } private: std::shared_ptr<B> mPb; }; class B { public: B() { std::cout << "B " << std::endl; } ~B() { std::cout << "~B " << std::endl; } void setA(std::shared_ptr<A> &pa) { mPa = pa; } private: std::shared_ptr<A> mPa; }; int main() { { std::shared_ptr<A> pA = std::make_shared<A>(); std::shared_ptr<B> pB = std::make_shared<B>(); pA->setB(pB); pB->setA(pA); std::cout << "pA use_count: " << pA.use_count() <<std::endl; std::cout << "pB use_count: " << pB.use_count() << std::endl; } system("pause"); return 0; }
-
- 执行结果
-
A B pA use_count: 2 pB use_count: 2 请按任意键继续. . .
-
- 从执行结果可以看到,A和B都没有被释放。这是由于pA和pB离开作用域的时候,引用计数都为1,因此不会调用析构释放资源。
- 如何解决这个问题,只需要将类B的成员变量mPa由
std::shared_ptr<A>
改为std::weak_ptr<A>
即可。