【C++】智能指针

引、内存泄漏

在编写C++程序时,内存资源方面的问题一直是不可疏忽的,其中,内存泄漏就是一个典型。

内存泄漏并不是指内存资源在物理层面上的消失,而是程序按需分配某段内存资源后,失去了对该资源的控制,白白浪费了内存空间,导致系统"负重前行"。这种情况一旦发生,隐患极大,例如在长期运行的程序(如操作系统、后台服务等)中出现内存泄漏,会使系统响应得越来越慢,以至于卡顿、死机。

内存泄漏分为系统资源泄漏和堆内存泄漏。

系统资源泄漏指的是,系统分配的资源没有正确释放而导致的资源浪费。程序使用系统分配的资源,例如套接字、文件描述符、管道等,用完后没有通过相应的函数释放掉,可能导致系统效能减少,系统执行不稳定。

堆内存泄漏指的是,在堆上申请的资源没有正确释放导致的资源浪费。程序执行中通过malloc / calloc / realloc / new等,会按需从堆中分配一份内存资源,这份资源用完后必须通过相应的free或者delete释放。如果这份资源没有被释放,那么之后它将无法继续使用。

cpp 复制代码
// 1.内存申请了忘记释放
void MemoryLeaks()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
}
cpp 复制代码
// 2.异常安全问题(delete未执行到位)
void MemoryLeaks()
{	
	pair<string, string>* p1 = new pair<string, string>;
	Func(); // 这里Func()抛异常会导致 delete p1未执行,p1没被释放.
	delete p1;
}

要避免内存泄漏,一般有事后查错和事前预防两种方案。

事后查错如:

  1. 使用公司自带内存泄漏检测功能的私有内存管理库(这是部分公司的内部规范);
  2. 使用内存泄漏工具检测,如Valgrind和Sanitizer等(但许多工具都不够靠谱,有些还收费昂贵)。

【补】内存泄漏的检测工具

在linux下内存泄漏检测:linux下几款内存泄漏检测工具

在windows下使用第三方工具:VLD工具说明

其他工具:内存泄漏工具比较

事前预防如:

  1. 工程前期良好的设计规范、编码规范,申请的内存资源都有匹配的释放(但这比较理想,如果碰上异常,就算注意释放了,可能还会出问题);
  2. 采用RAII思想或者智能指针来管理资源。

本篇博客主要整理了RAII思想和智能指针原理,结合对库中四种智能指针的模拟实现,旨在让读者能更深入地认识C++的内存管理方案。

目录

引、内存泄漏

一、RAII与智能指针

1.什么是RAII

2.智能指针的原理

二、C++98的auto_ptr

1.基本原理和用法

2.模拟实现

三、C++11的unique_ptr

1.基本原理和用法

2.模拟实现

四、C++11的shared_ptr

1.基本原理和用法

2.模拟实现

3.定制删除器

4.线程安全问题

5.循环引用问题

五、C++11的weak_ptr

1.基本原理和用法

2.模拟实现

补、智能指针的发展历史

补、智能指针与boost库

补、C++与Java关于内存管理方案的比较


一、RAII与智能指针

1.什么是RAII

对于上文中"内存申请了忘记释放",在编写代码时将申请的资源都正确释放即可。

cpp 复制代码
// 1.内存申请了忘记释放
void MemoryLeaks()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	//delete p1;
	//delete p2;
}

对于上文中的"异常安全问题",一般可以通过try - catch来解决。

cpp 复制代码
// 2.异常安全问题(delete未执行到位)
void MemoryLeaks()
{	
	pair<string, string>* p1 = new pair<string, string>;
	Func(); // 这里Func()抛异常会导致 delete p1未执行,p1没被释放.
	delete p1;
	// 抛异常会影响执行流,
	// 可能导致执行流一连跳跃了好几个函数栈,
	// 使在堆区申请的许多资源没有及时回收,
	// 造成内存泄露
}
cpp 复制代码
//抛异常会影响执行流,delete不一定会执行到位
//一般可以通过try - catch解决这个问题
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
void MemoryLeaks()
{
	pair<string, string>* p1 = new pair<string, string>;

	try 
	{
		div();
	}
	catch (...)
	{
		delete p1;
		cout << "delete:" << p1 << endl;
		throw;
	}

	delete p1;
	cout << "delete:" << p1 << endl;
}

int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}
cpp 复制代码
 //但当new的空间多了,try - catch就有些捉襟见肘
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
void MemoryLeaks()
{
	pair<string, string>* p1 = new pair<string, string>;
	pair<string, string>* p2 = new pair<string, string>;
	pair<string, string>* p3 = new pair<string, string>;
	pair<string, string>* p4 = new pair<string, string>;
     //p1可能会抛异常,p2、p3、p4也可能抛异常,为了使程序正常运行
     //在p2抛异常之前应先释放p1,在p2抛异常之前应先释放p3...甚至还有div()也可能抛异常
     //这样下去不知道要套多少层try - catch

    //...
}

int main()
{

    //...

	return 0;
}
cpp 复制代码
//于是乎,前人想到了一种更巧妙的方式:
//利用构造和析构的特性(对象初始化自动调用构造,对象出作用域(生命周期结束)自动调用析构)
//用一个对象来管理new返回的指针
//具体的做法是将指针用一个类封装起来,在类的析构中delete
//这样一来,那怕抛异常也不会影响delete执行了
template<class T>
class SmartPtr
{
public:
	// 资源交给对象管理
    // 对象生命周期内,资源有效;
    // 对象生命周期到了,借助析构释放资源
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl; //这句是用于验证抛异常的时候析构是否也释放了资源
		delete _ptr;
	}

private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
void f()
{  
    pair<string, string>* p1 = new pair<string, string>;
	SmartPtr sp1(p1); // 资源交给对象管理
    //无论是f()的生命周期正常结束,还是div()抛异常
    //都不影响sp1的析构会释放p1

	div();

    //此时不必再手动释放资源
	//delete p1;
}

int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

cpp 复制代码
template<class T>
class SmartPtr
{
public:

	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl; 
		delete _ptr;
	}

private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
void f()
{  
    //实际运用中,可以传匿名对象给SmartPtr对象
	SmartPtr<pair<string, string>> sp1(new pair<string, string>);
    //如果sp1的new本身抛异常了,就不会为sp1调用构造
	div();
    //如果执行到这里,div()抛异常了,sp1的析构也会正常调用

	SmartPtr<pair<string, string>> sp2(new pair<string, string>);
    //如果sp2的new抛异常了,就就不会为sp2调用构造,sp1的析构会正常调用
	SmartPtr<pair<string, string>> sp3(new pair<string, string>);
    //如果sp3的new抛异常了,就就不会为sp3调用构造,sp1、sp2的析构会正常调用

	div();
    //如果执行到这里,div()抛异常了,sp1、sp2、sp3的析构都会正常调用

    //无论new申请了多少资源,都是可以正确地释放的
}

int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

把new申请资源交给具有生命周期的对象来管理,这种方式就叫做RAII。

RAII(Resource Acquisition Is Initialization,申请资源随即初始化)是一种利用对象生命周期来管理资源 的简单技术(这些资源可以是内存、文件句柄、网络连接、互斥量等),可以使资源在对象构造时被获取且通过对象控制访问,资源在对象的生命周期内始终保持有效,最终在对象析构的时候被释放

2.智能指针的原理

上文代码中的SmartPtr就是智能指针吗?实际并不是。

指针可以通过*解引用来得到所指的值,还可以通过->访问所指空间中的一些内容,SmartPtr并不具备指针的这些行为,所以只是一个可以生成管理资源对象的类,还远远不算上智能指针。

cpp 复制代码
//以下代码中,SmartPtr加入了operator*()、 operator->(),
//基本具备了指针的行为
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

int main()
{
	SmartPtr<string> sp1(new string("xxxxx"));
	cout << *sp1 << endl;

	SmartPtr<pair<string, string>> sp2(new pair<string, string>("1111", "22222"));
	cout << sp2->first << endl;
	cout << sp2->second << endl;
	return 0;
}

尽管在上文中,SmartPtr加入了operator*()、 operator->(),基本具备了指针的行为,但它想要成为智能指针,仍需面临一个拷贝问题:

cpp 复制代码
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

int main()
{
	SmartPtr<string> sp1(new string("xxxxx"));
	SmartPtr<string> sp2(new string("yyyyy"));

	sp1 = sp2;
    //SmartPtr类中没有显示的赋值重载,编译器会为其生成默认的赋值重载,
    //默认的赋值重载,是直接将一个对象中的值赋给另一个对象(浅拷贝)
    //在这里,sp2中的指针赋给了sp1,会导致sp1中的指针丢失,sp1中指针管理的资源可能失控

	return 0;
}

从以上代码中,sp1、sp2调用析构的情况来看,经过赋值后,sp1和sp2共享了同一份资源,它们分别去调用了一次析构,导致对同一份资源进行了两次delete,引发了异常。而在赋值前,sp1、sp2各有一份资源,对一份资源进行了两次delete,也意味着有一份资源没有被delete释放,存在内存泄漏的隐患。

关于这个拷贝问题的方案,前人主要探索出了四种,也对应了下文即将出现的四种智能指针,它们出现的顺序也是​​​​​与智能指针的发展历史有关。

【Tips】智能指针的原理:

  1. 通过RAII思想管理资源;
  2. 具备像指针一样的行为(*解引用,->访问);
  3. 能够解决拷贝问题(不同智能指针的解决方案不同)。

二、C++98的auto_ptr

1.基本原理和用法

auto_ptr是解决上文提到的拷贝问题的第一个方案,但它在解决旧问题的同时又留下了新的问题。

对于上文提到的拷贝问题,auto_ptr给出的方案是管理权转移:如果发生拷贝,就将原先的资源转移给新的智能指针管理(还是通过拷贝),然后将原先的智能指针置空 。具体的实现相当于在上文中的SmartPtr类中又加入一个显示的拷贝构造,在拷贝构造中将被拷贝的对象置空

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl; //验证调用了构造
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl; //验证调用了析构
	}
	//private:
	int _a;
};

int main()
{
	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(new A(1));
	//能正常调用构造和析构

	return 0;
}
cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

int main()
{
	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(ap1);
    //如果发生拷贝,
    //就将原先的资源转移给新的智能指针管理,
    //然后将原先的智能指针置空

	return 0;
}

管理权转移发生后,拷贝的对象会被置空,此时再访问它就会导致程序崩溃

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

int main()
{
	auto_ptr<A> ap1(new A(1));

	auto_ptr<A> ap2(ap1);
	//拷贝时发生管理权转移
	//用ap1拷贝ap3,把ap1资源的管理权给了ap3,使ap1自身置空了
	ap3->_a++;//这句不会引发任何问题
	cout << ap2->_a << endl;

	ap1->_a++;//这句虽然编译可通过,但运行后程序会崩溃
	//管理权转移:拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
	//转移的隐患:导致被拷贝对象悬空,访问就会出问题
    //所以使用auto_ptr,不应再使用被拷贝的对象
	return 0;
}
//所以一般实践中,很多公司明确规定不要用auto_ptr

auto_ptr解决了浅拷贝带来的指针丢失、同一份资源被多次释放的旧问题,但遗留了被拷贝对象引起程序崩溃的新隐患,于是上了很多地方的黑名单,被明确规定禁用。

2.模拟实现

cpp 复制代码
//c++98的auto_ptr
namespace CVE
{
	template<class T>
	class auto_ptr
	{
	public:
		// 1、RAII思想管理资源
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		// 2、具备指针的行为
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		//3、拷贝方案:管理权转移(通过显示的拷贝构造实现)
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
            //被拷贝的对象必须置空,否则会造成资源被多次释放
            //使用auto_ptr,不应再使用被拷贝的对象
		}
	private:
		T* _ptr;
	};
}

三、C++11的unique_ptr

1.基本原理和用法

对于上文提到的拷贝问题,unique_ptr给出的方案是:禁止拷贝 。既然这个拷贝问题很难解决,那直接把问题干掉,不就不用解决了吗?具体实现是使用delete关键字在类中直接禁止编译器生成默认的拷贝构造和赋值重载。它虽然没有真正解决拷贝问题,但因为直接干掉了拷贝问题,所以还是相对安全的,可以运用在不需要拷贝的场景。

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl; //验证调用了构造
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl; //验证调用了析构
	}
	//private:
	int _a;
};

int main()
{
	unique_ptr<A> up1(new A(1));
	unique_ptr<A> up2(new A(2));
	//能正常调用构造和析构

	return 0;
}
cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

int main()
{
	unique_ptr<A> up1(new A(1));

	unique_ptr<A> up2(up1);//unique_ptr简单粗暴,直接不让拷贝

	return 0;
}

2.模拟实现

cpp 复制代码
//C++11的unique_ptr

namespace CVE
{
	template<class T>
	class unique_ptr
	{
	public:
        //1.RAII思想管理资源
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
        
        //2.具备指针的行为
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		//3.拷贝方案:禁止拷贝(delete关键字 - 默认成员函数只声明不实现)
		unique_ptr(unique_ptr<T>& ap) = delete;              //禁用拷贝构造
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;//禁用赋值重载
	private:
		T* _ptr;
	};
}

四、C++11的shared_ptr

1.基本原理和用法

shared_ptr也是C++11中的智能指针,不同于unique_ptr禁止拷贝,shared_ptr既支持拷贝,又彻底解决了auto_ptr的毛病。

shared_ptr给出的拷贝方案是:引用计数通过引用计数,来记录管理某一份资源的智能指针对象的具体数量,当计数为0时(意味着这份资源目前没有任何智能指针来管理),就允许释放资源

具体的实现方式是,在类中增加一个指向堆上空间(这个空间是动态申请的)的指针作成员变量,既使每个对象拥有属于自己的独立的计数,同时又使拷贝对象和被拷贝对象共同管理着同一个计数指针(不同的对象指向同一份资源,这些对象应该有相同的引用计数。要实现这一点,不能使用整型变量,因为整型变量只能做到让每个对象有属于自己的引用计数;也不能使用静态变量,因为static只能做到让不同的对象共享同一个引用计数;只有指向堆空间的指针能满足需求。图解见下文)。

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

int main()
{
	shared_ptr<A> sp1(new A(1));
	shared_ptr<A> sp2(new A(2));
	//能正常调用构造和析构

	shared_ptr<A> sp3(sp1);

	sp3->_a++;
	cout << sp3->_a << endl;

	sp1->_a++;
	cout << sp1->_a << endl;
	//解决了auto_ptr的毛病
	//拷贝的对象能正常管理和访问资源了

	return 0;
}

2.模拟实现

cpp 复制代码
//C++11的shared_ptr
namespace CVE
{
	template<class T>
	class shared_ptr
	{
	public:
        //1.RAII思想管理资源

		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)//计数减到0时才释放
			{
				delete _ptr;
				delete _pcount;
			}
		}
        
        //2.具备指针的行为

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}


        //3.拷贝方案:引用计数

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);//每拷贝构造一次,就计数一次
		}

        //赋值重载稍复杂
        //要分情况讨论:
        //1)参与赋值双方指向不同资源:
		//   例如:sp1 = sp2 
        //   需在sp1指向别的空间/sp2的空间之前,要把sp1自己原本的计数减掉,否则会造成内存泄漏
        //2)参与赋值双方指向相同资源(相当于自己赋值给自己):
        //   不必赋值
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr == sp._ptr) //这里是在处理情况2
				return *this;
			//左右操作数管理同一块资源(自己赋值自己),就没必要继续赋值了
			//如果继续走下面的代码,
            //可能在自己赋值自己的过程中把自己释放掉了,留下一堆随机值(野指针风险)

			//先为左操作数减计数,
            //(在这之后如果计数为0,就释放左操作数原有的资源)
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			//再让左操作数指向右操作数的资源,且这份资源的计数应+1
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
			//最终返回左操作数
			return *this;
		}


        //4.其他功能

		int use_count() const//获取引用计数的指针
		{
			return *_pcount;
		}

		T* get() const//获取指向资源的指针
		{
			return _ptr;
		}

	private:
		//两个指针,一个指针指向资源,一个指针指向计数
		T* _ptr;
		int* _pcount;
		//用一个指向动态空间的指针来实现独立的计数,使每个对象都拥有自己的计数
		//同时,使拷贝对象和被拷贝对象共同管理同一个计数指针
	};
}

3.定制删除器

当shared_ptr对象管理的是一个new申请的数组时,应该匹配地使用delete []释放,而非delete。所以为了使不同类型的shared_ptr对象能匹配不同的释放方式,C++库中为shared_ptr提供了定制删除器(它在shared_ptr的构造函数中;其中"D del"接收就是一个删除器,删除器可以是函数指针、仿函数、lambda表达式等),通过仿函数来为不同的shared_ptr对象匹配它们合适的释放方式。

cpp 复制代码
//测试shared_ptr的定制删除器
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};

template<class T>
struct DeleteArrayFunc {
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};

int main()
{
	FreeFunc<int> freeFunc;
	std::shared_ptr<int> sp1((int*)malloc(2), freeFunc);

	DeleteArrayFunc<int> deleteArrayFunc;
	std::shared_ptr<int> sp2((int*)malloc(2), deleteArrayFunc);

	std::shared_ptr<A> sp3(new A[2], [](A* p) {delete[] p; });

	return 0;
}
cpp 复制代码
//将定制删除器写入上文模拟实现shared_ptr的代码中
namespace CVE
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		//1.要在构造内控制析构的删除方式,首先想到借助类的成员变量,
		// 使指定的删除方式可以被析构接收
		// 但成员变量的类型不确定就无法定义成员变量
		//2."function<void(T*)> _del;"
		// void(T*)意味着,无论作为内部参数的指针是什么类型,函数的返回值始终都是确定的void
		// 这样就可以用一个包装器来定义接收删除方式的成员变量
		template<class D>
		shared_ptr(T* ptr,D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			,_del(del)
		{}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)//计数减到0时才释放shared_ptr的类对象
			{
				_del(_ptr);
				delete _pcount;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);//每拷贝构造一次,就计数一次
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{

			if (_ptr == sp._ptr)
				return *this;

			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;

			return *this;
		}

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;

		function<void(T*)> _del = [](T* ptr) {delete ptr; };
		//用一个包装器实现构造控制析构的删除方式
        //设置一个lambda表达式作为缺省值
	};
}

4.线程安全问题

shared_ptr涉及的线程安全分为两方面:

  1. 同一份资源的引用计数是由多个shared_ptr对象同时管理的,如果在两个线程中shared_ptr的引用计数同时++或--,这个操作将不是原子性的。例如某一份资源的引用计数原来是1,被不同的对象一共++了两次,可能还是1。这样一来引用计数就错乱了,会有导致内存泄漏或者程序崩溃的隐患。所以shared_ptr中对引用计数++和--是需要加锁的,也就是说引用计数的操作必须是线程安全的。
  2. shared_ptr管理的资源是从堆空间申请的,如果有两个线程中同时去访问这份资源,也会有线程安全的隐患。

总得来说,为了保证线程安全,必须在引用计数的++和--时就保证原子性,具体的实现方式是shared_ptr内部带有互斥锁

cpp 复制代码
//将互斥量写入上文模拟实现shared_ptr的代码中
#include<mutex>
namespace CVE
{
	template<class T>
	class shared_ptr
	{
	public:

		//引用计数++
		void addCount()
		{
			//必须先上锁再++
			_pmtx->lock();
			(*_pcount)++;
			//完事了要解锁
			_pmtx->unlock();
		}

		//"引用计数--" + "控制资源释放"
		void subCount()
		{
			_pmtx->lock();

			int flag = 0;//管理锁的释放
			//引用计数减为0时就释放资源
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
				flag = 1;//同时将flag置为1
			}
			//只有当flag为1时才释放锁
			_pmtx->unlock();//必须先解锁,再释放锁			
			if (flag)
				delete _pmtx;
		}


		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
		{}

		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			,_del(del)
		{}

		~shared_ptr()
		{
			subCount();
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			addCount();//引用计数++
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				subCount();//旧资源引用计数--

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;

				addCount();//新资源引用计数++
			}

			return *this;
		}

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };

		std::mutex* _pmtx;//互斥锁
	};
}

5.循环引用问题

shared_ptr虽然支持拷贝且解决了auto_ptr,但并非十全十美,它仍存在一个名为"循环引用"的问题,例如发生在下面这样一个情景中:

cpp 复制代码
//用shared_ptr管理双向链表的节点
//要将节点首尾相连(因为是双向链表),只需对节点的前驱指针和后继指针赋值
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

//双向链表节点
struct Node
{
	A _val;
	Node* _next;
	Node* _prev;
};
int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;
	//此处类型不匹配,前驱指针和后继指针是内置类型,sp1和sp2是自定义类型
    //自定义类型无法赋给内置类型
}
cpp 复制代码
//将链表节点的类型改为shared_ptr就支持赋值了
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};
struct Node
{
	A _val;
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
};
int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;
	// 但此时无法正常调用析构
	// 释放在逻辑上发生了死循环,会引起内存泄漏
    // 这种情况就是循环引用
}

Node内部使用了shared_ptr作为前驱指针和后继指针,当"sp1->_next = sp2;"被执行时,sp2中的引用计数会+1;同理,当"sp2->_next = sp1;"被执行时,sp1中的引用计数也会+1。

这就会导致析构无法正常调用。按照析构的顺序,sp2应该先析构,sp2中的引用计数理应-1,但sp1->_prev还指向sp2,会使sp2的引用计数始终为1,而无法正常为sp2调用析构;然后sp1再析构,也会因为sp2->_next还指向sp1,使sp1的引用计数始终为1,无法正常为sp1调用析构。

前人把这种因"套娃"而无法正常调用析构来释放资源的情况称之为"循环引用"。

循环引用一直是shared_ptr的死穴,仅凭shared_ptr本身难以解决,而作为解决问题的尝试,C++11又提供了weak_ptr。

五、C++11的weak_ptr

1.基本原理和用法

weak_ptr是不具有RAII思想的智能指针,专门用来解决shared_ptr的循环引用问题,具体的实现方式是**weak_ptr不增加引用计数。**weak_ptr的构造和赋值都要由shared_ptr支持,所以它实际不参与资源的释放,一般仅通过它访问资源。

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << this;
		cout << "~A()" << endl;
	}
	//private:
	int _a;
};

struct Node
{
	A _val;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
	// weak_ptr是不具有RAII思想的智能指针,专门用来解决shared_ptr循环引用问题
	// weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
};

int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);
	cout << sp1.use_count() << endl;//use_count()是shared_ptr的一个成员函数,
	cout << sp2.use_count() << endl;//可以查看引用计数

	sp1->_next = sp2;
	sp2->_prev = sp1;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	return 0;
}

2.模拟实现

cpp 复制代码
//C++11的weak_ptr
namespace CVE
{
	template<class T>
	class shared_ptr
	{
        //shared_ptr模拟实现的代码见上文
    };


	template<class T>
	class weak_ptr
	{
	public:
		//weak_ptr不支持RAII
		//它无法独立进行管理,它的构造和赋值都要由shared_ptr支持
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

补、智能指针的发展历史

C++98中的auto_ptr,但在拷贝构造或者赋值的时候,原本的auto_ptr会被置空,存在非常大的缺陷,于是上了很多地方的黑名单。

C++11中的unique_ptr,禁止了拷贝和赋值,直接避免了auto_ptr可能存在的缺陷,但它不支持拷贝和赋值。

后续,C++11又提供了shared_ptr,通过引用计数的方式解决了unique_ptr不能拷贝和赋值的缺陷,且通过互斥锁保证了shared_ptr本身的线程安全,但它存在循环引用的问题。

为了解决shared_ptr的循环引用问题,C++11又通过"仅指向不管理"的方式提供了weak_ptr智能指针。

智能指针们各有各的特色,在使用的时候,根据具体情景选择合适的智能指针即可(但最好别使用auto_ptr,最好别)。

补、智能指针与boost库

C++委员会曾发行过一个名为boost的库(相当于是C++标准库的先行版,其中是很多实验性质的内容。它作为标准库的后备,进一步完善了标准库),其中优质的内容基本被C++标准库收录了。标准库中的智能指针就是参照boost库中的智能指针,再加以修改而来的。Boost库中有三个智能指针,scoped_ptr、shared_ptr、weak_ptr,使c++11后来有了unique_ptr、shared_ptr、weak_ptr。

C++的智能指针和boost的智能指针的关系:

  1. C++ 98 中产生了第一个智能指针auto_ptr;
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr;
  3. C++ TR1,引入了shared_ptr等(TR1并不是标准版);
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。其中,unique_ptr对应boost中的scoped_ptr,且这些智能指针的实现原理参考了boost。

补、C++与Java关于内存管理方案的比较

Java虽然没有指针,但有引用。用Java语言new了一个空间之后,并不会返回这个空间的地址,而返回的是这个空间的引用。但Java引用跟C++引用不一样的地方在于,Java引用跟C++指针类似,可以改变它本身的指向。这也导致Java中也存在类似C++中指针的问题。

但Java没有智能指针,而有垃圾回收器。垃圾回收指的是,new了以后不需要手动delete。在Java程序的后台,都有一个垃圾回收器,它会把申请的空间都记录下来,等这个空间不用了就自动释放掉。

C++没有垃圾回收器,而有智能指针。这既是因为垃圾回收的成本很高,也是因为C++程序的运行机制跟Java程序是不一样的。

C++编译好的程序就是一个一个的进程,它们采用CPU调度的机制,直接在操作系统之上运行。而Java不是这样的,要运行Java程序得先安装Java的虚拟机。Java的虚拟机可以看作是跑在操作系统上面的一个进程(这也是为什么,一些对性能极致要求的程序一般不会考虑用Java来写,例如游戏的一些服务器、物联网设备上的程序)。

相关推荐
憧憬成为原神糕手19 分钟前
c++_list
开发语言·c++
zyh2005043021 分钟前
c++的decltype关键字
c++·decltype
idealzouhu22 分钟前
Java 并发编程 —— AQS 抽象队列同步器
java·开发语言
爱吃油淋鸡的莫何22 分钟前
Conda新建python虚拟环境问题
开发语言·python·conda
闲人编程29 分钟前
Python实现日志采集功能
开发语言·python·fluentd·filebeat·日志采集
Sol-itude36 分钟前
关于MATLAB计算3维图的向量夹角总是不正确的问题记录
开发语言·matlab·问题解决·向量
2401_8628867838 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
奔驰的小野码1 小时前
java通过org.eclipse.milo实现OPCUA客户端进行连接和订阅
java·开发语言
小川_wenxun1 小时前
优先级队列(堆)
java·开发语言·算法
惜缘若水1 小时前
【SpinalHDL】Scala编程之伴生对象
开发语言·scala·spinalhdl