c++11新特性——智能指针详解

智能指针:

一、解决了什么问题

  1. 内存泄漏:在未使用智能指针时,我们在堆上malloc申请一段内存或者new一个对象,如果忘记释放就会造成内存泄漏;
  2. 指针共享所有权的传递和释放,比如:多线程同时使用同一个对象时的析构问题。
  3. 使用普通指针,容易造成内存泄露(忘记释放)、二次释放、程序发生异常时内存泄露等问题等。

二、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 u_ptr = std::make_shared(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 s_ptr = std::make_shared(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  
#include  

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  
#include  

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  
#include  

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 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内部有两个指针,一个指向目标对象,另一个指向控制块,控制块包含引用计数、弱计数和删除器和其他数据。
c++11新特性——智能指针详解_第1张图片5.1 指定删除器
在使用shared_ptr管理非new对象或者没有析构函数时,需要为其指定删除器。

//1-3-1-delete 
#include  
#include  

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. 线程1正在使用某个shared_ptr,线程2,可能在某个时刻需要释放指针,因为线程1使用了 lock() 方法锁住这个指针,线程2只能等线程1使用完才能释放。
  2. 线程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服务期高级架构。

你可能感兴趣的:(c++,c++,开发语言)