智能指针:
一、解决了什么问题
- 内存泄漏:在未使用智能指针时,我们在堆上malloc申请一段内存或者new一个对象,如果忘记释放就会造成内存泄漏;
- 指针共享所有权的传递和释放,比如:多线程同时使用同一个对象时的析构问题。
- 使用普通指针,容易造成内存泄露(忘记释放)、二次释放、程序发生异常时内存泄露等问题等。
二、C++11 智能指针
std::auto_ptr : 已被c++11废弃
std::unique_ptr :独占资源所有权的指针。
std::shared_ptr :共享资源所有权的指针。
std::weak_ptr :共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。
三、线程安全问题
对于shared_ptr来说,引用计数是线程安全的,但是数据是线程不安全的,需要内部对数据进行加锁。
四、指针指针使用范例
4.1 unique_ptr
当我们想独占资源的所有权时,可以使用std::unique_ptr对资源进行管理,离开该std::unique_ptr对象的作用域时,会自动释放资源。这是很基本的RAII思想(RAII,Resource Acquisition Is Initialization,由c++之父Bjarne Stroustrup提出,即:资源获取即初始化,他说:使用局部对象来管理资源的技术称之为“资源获取即初始化”)。
4.1.1 使用裸指针,需要手动释放
{ int* p = new int(10); // ... delete p; // 要记得释放内存 }
4.1.2 使用unique_ptr,自动释放内存
{ std::unique_ptr<int> u_ptr = std::make_unique<int>(10); // ... // 离开该作用域会自动释放内存 }
4.1.3 unique_ptr所有权的转移,只支持move转移
{ std::unique_ptr<int> u_ptr = std::make_unique<int>(10); std::unique_ptr<int> u_ptr1 = u_ptr; // 编译报错, unique_ptr只支持move转移所有权 std::shared_ptr<int> s_ptr = u_ptr; // 编译报错,不支持与shared_ptr混用 std::unique_ptr<int> u_ptr2 = std::move(u_ptr); }
4.1.4 unique_ptr 还可以指向数组
{ std::unique_ptr<int[]> u_ptr = std::make_unique<int[]>(10); for (int i = 0; i < 10; i++) { u_ptr [i] = i; } for (int i = 0; i < 10; i++) { std::cout << u_ptr [i] << std::endl; } }
4.2 shared_ptr
本质是对资源做引用计数,当引用计数为0时,释放该资源。
4.2.1 使用shared_ptr,自动释放内存
{ std::shared_ptr<int> p = new int(1); // 错误,不能将裸指针直接赋值为智能指针 std::shared_ptr<int> s_ptr = std::make_shared<int>(10); assert(s_ptr.use_count() == 1); // 此时,s_ptr的引用计数为1 { std::shared_ptr<int> s_ptr1 = s_ptr; assert(s_ptr.use_count() == 1); // 此时,s_ptr的引用计数为2 } assert(s_ptr.use_count() == 1); // 离开s_ptr1 的作用域,引用计数减1 } // 引用计数为0,释放该资源
4.2.2 shared_ptr也可以指向数组
{ std::shared_ptr<int[]> s_ptr(new int[10]); // std::shared_ptr<int[]> u_ptr = std::make_shared<int[]>(10); c++20才支持 for (int i = 0; i < 10; i++) { s_ptr[i] = i; } for (int i = 0; i < 10; i++) { std::cout << s_ptr[i] << std::endl; } }
4.2.3 当需要获取原始指针时,可以通过get方法。不过,谨慎使用get方法。
{ int main() { int *ptmp = new int(10); std::shared_ptr<int> s_ptr(ptmp); // 裸指针委托智能指针进行管理 //std::shared_ptr<int> s_ptr = std::make_shared<int>(10); int* ptr = s_ptr.get(); std::cout << ptmp << ", " << ptr << std::endl; return 0; } }
4.2.4 通过 shared_from_this() 返回 this 指针
不要将this指针作为shared_ptr 返回出来,因为 this 指针本质上市裸指针,可能会导致多次析构。
#include <iostream> #include <memory> using namespace std; class A { public: shared_ptr<A> GetSelf(){ return shared_ptr<A>(this); } ~A(){ cout << "Destructor A" << endl; } } int main() { shared_ptr<A> s_ptrA(new A); shared_ptr<B> s_ptrB = s_ptrA->GetSelf(); return 0; }
运行结果会调用两次析构函数。因为用一个this指针构造了两个指针指针,而这两个智能指针是没有关系的。所以,在程序结束时,都会调用各自的析构函数,导致重复析构。
正确的做法是:让需要返回的类共有继承 std::enable_shared_from_this 类,然后使用基类的成员函数shared_from_this() 来返回目标类 this 的 shared_ptr。
#include <iostream> #include <memory> using namespace std; class A : public std::enable_shared_from_this<A> { public: shared_ptr<A> GetSelf(){ return shared_from_this(); } ~A(){ cout << "Destructor A" << endl; } } int main() { shared_ptr<A> s_ptrA(new A); shared_ptr<B> s_ptrB = s_ptrA->GetSelf(); return 0; }
4.2.5 循环引用,导致内存泄漏
#include <iostream> #include <memory> using namespace std; class A; class B; class A { public: std::shared_ptr<B> bptr; ~A() { cout << "A is deleted" << endl; } }; class B { public: std::shared_ptr<A> aptr; // public: std::weak_ptr<A> aptr; // 解决循环引用问题 ~B() {cout << "B is deleted" << endl; } }; int main() { { std::shared_ptr<A> ap(new A); std::shared_ptr<B> bp(new B); ap->bptr = bp; bp->aptr = ap; } cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构 return 0; }
说明:循环引用导致ap和bp的引用技术都是2,在离开作用域后,各自的引用记数减1,并没有减为0。导致两个智能指针都没有被析构,导致内存泄漏。
解决方法:把A和B任何一个成员变量改为weak_ptr
五、shared_ptr实现原理
shared_ptr内部有两个指针,一个指向目标对象,另一个指向控制块,控制块包含引用计数、弱计数和删除器和其他数据。
5.1 指定删除器
在使用shared_ptr管理非new对象或者没有析构函数时,需要为其指定删除器。
//1-3-1-delete #include <iostream> #include <memory> using namespace std; void DeleteIntPtr(int *p) { cout << "call DeleteIntPtr" << endl; delete p; } int main() { std::shared_ptr<int> p(new int(1), DeleteIntPtr); std::shared_ptr<int> p(new int(1), [](int *p) { delete p; }); //使用lambda表达式 // 注意 在指定 unique_ptr 的删除器时,需要指明删除器的类型 std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误 std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正确 return 0; }
六、weak_ptr
share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互
使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从
一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
6.1 基本用法
int main() { shared_ptr<int> sp(new int(10)); weak_ptr<int> wp(sp); cout << wp.use_count() << endl; //结果讲输出1 // 通过 expired 方法判断所观察对象是否已经被释放 if(wp.expired()) cout << "weak_ptr无效,资源已释放"; else cout << "weak_ptr有效"; return 0; }
6.2 通过lock方法获取监视对象的shared_ptr。
举例:比如在多线程情况下
- 线程1正在使用某个shared_ptr,线程2,可能在某个时刻需要释放指针,因为线程1使用了 lock() 方法锁住这个指针,线程2只能等线程1使用完才能释放。
- 线程2已经释放了该shared_ptr,线程1会通过expired判断该资源是否还存在,规避了异常发生。
std::weak_ptr<int> gw; // 全局变量 void func() { auto sptr = gw.lock(); if (gw.expired()){ cout << "gw无效,资源已释放"; }else{ cout << "gw有效, *spt = " << *spt << endl; } } int main() { { auto sp = std::shared_ptr<int>(100); gw = sp; func(); // 还在shared_ptr作用域内,gw有效 } func(); // 出了作用域,打印:"gw无效,资源已释放"; }
文章参考与<零声教育>的C/C++linux服务期高级架构。