C++:智能指针

Hello大家好! 很高兴与大家见面! 给生活添点快乐,开始今天的编程之路。

我的博客:<但愿.

我的专栏:C语言题目精讲算法与数据结构C++

欢迎点赞,关注

目录

前言

一 RAII和智能指针的设计思路

1.1RAII

1.2 智能指针

1.2.1智能指针的概念

1.2.2智能指针的特点作用

1.2.3实现智能指针中的难点拷贝构造

二 C++标准库中的智能指针

2.1 C++标准库中的智能指针

2.2为什么又shared_ptr还要unique_ptr

2.3 C++标准库中的智能指针的使用

2.4智能指针的原理

2.4.1auto_ptr的原理

2.4.2 unique_ptr的原理

2.4.3 shared_ptr的原理

2.4.3.1 简单的shared_ptr的实现

2.4.3.2 allocate_shared(shared_ptr的优化)

2.4.3.3shared_ptr中删除器的实现(unique_ptr一样)

2.4.3.3.1库中shared_ptr怎么实现删除器

2.4.3.3.2自己实现shared_ptr的删除器

2.4.4 weak_ptr

2.4.4.1 weak_ptr由来和用途

2.4.4.2shared_ptr的 循环引用问题

2.4.4.2 .1循环引用问题怎么形成的

2.4.4.2 2循环引用问题怎么解决

三 C++11和boost中智能指针的关系

前言

在异常中我们已经讲过,由于异常引起的运行逻辑的变化而导致一些资源难以管理,处理方法是交给一个对象管理,利用这个对象的生命周期进行管理,而这个对象就是智能指针。

一 RAII和智能指针的设计思路

1.1RAII

RAII(Resource Acquisition Is Initialization),资源请求立即初始化,是一种利用对象生命周期对资源进行管理的简单技术。 他是⼀种管理资源的类的设计思想,本质是 ⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指 针、⽹络连接、互斥锁等等(即我们获取到资源后,不用自己管理由于异常等等原因我们很难自己实现管理[自己delete、free],而是把这些资源委托给一个自定义类型对象管理,利用这个对象的生命周期进行管理),注意 定义成一个类模板,是为了实现任意类型都可以调用。
• 利用对象是生命周期管理
RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常 释放,避免资源泄漏问题。
• 总结 :
取到资源立即初始化是:初始化的本质是调用对象的构造函数,也就是说获取到资源后不用自己管理,马上委托给一个对象管(构造一个对象),这个对象出了作用域就会自动析构,不用我们显示的析构。
• RAII的思想对资源进行管理的好处:
无须显示释放资源,对象出了其作用域就会自动调用析构函数释放资源。例如在前面的程序中因为抛异常,导致资源难以释放,而采用这种方法就很好。
对象所须的资源在生命周期内始终保持有效。

【示例】

简单的RAII这里还是与一个除法函数的调用链作为实例

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

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

private:
	T* _ptr;
};
double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}


void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array和array2没有得到释放。
	// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。
	// 但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案
	// 是智能指针,否则代码太戳了
	int* array1 = new int[10];
	// try
	int* array2 = new int[10]; // 抛异常呢
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;

		delete[] array1;
		delete[] array2;
		throw; // 异常重新抛出,捕获到什么抛出什么
	}
	// ...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}

void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[10]); // 抛异常呢

	SmartPtr<int> sp3(sp1);

	for (size_t i = 0; i < 10; i++)
	{
		sp1[i] = sp2[i] = i;
	}

	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}

	return 0;
}

1.2 智能指针

1.2.1智能指针的概念

智能指针用于管理资源对象,既然你把这个对象给智能指针管理那避免不了访问这个对象你的东西,那怎么访问呢?这里就要重载*、->、[ ]等运算符(看情况重载),重载*、->是为了可以和普通指针一样进行访问和复合类型的访问,重载[ ]是方便数组类似的访问。

结合RAII我们可以知道智能指针的基础是;

• RAII的设计思路:方便管理资源
• 运算符的重载:方便我们访问对象的资源。还有一些地方是传值返回此时必然要调用拷贝构造,这也是智能指针的难点简单的智能指针式很简单的,如果不实现拷贝构造编译器默认生成的拷贝构造是一个浅拷贝(会导致两个对象指向同一块资源,从而导致资源释放两次)

1.2.2智能指针的特点作用

【特点】

智能指针的特点是其内部成员变量只有一个指针变量就可以了,在构造时获取到资源后可以委托给我,帮你代管,实现自动释放(借助对象的析构函数,析构函数会自动调用),此时不管出现什么因为(异常)只要出了其作用域都会自动析构。即利用构造和析构函数,把资源委托给智能指针,在智能指针这个对象的生命周期内这个都有效,无论是否发生意外只要出了其作用域都会自动析构。

【作用】

由于智能指针这个对象的生命周期内这个都有效,无论是否发生意外只要出了其作用域都会自动析构。所以这是一个很可靠的方法,所以其在C++中式非常重要的(因为不把资源给其管理,自己管理很容易出问题,只有一出现问题就是内存陷漏,内存陷漏多了程序就终止了【很重要,面试常问】。

1.2.3实现智能指针中的难点拷贝构造

这时候大家一定会想自己实现一个深拷贝不就行了吗?这里有个问题智能指针它是一个代管,即operator*、->、[ ]等都是模拟指针的行为(智能指针是一个伪指针)。由于智能指针不拥有资源,它只是代管资源,所以把一个智能指针拷贝给另一个智能指针的本质是希望我们两个一起管理这个资源(即指向同一块资源,所以进行深拷贝是不行的)。那怎么解决这个问题:那我们来看一看库中怎么解决这个问题。

【实例在RAII的基础上加上访问资源的运算符重载即可】

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

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

	// 重载运算符,模拟指针的行为,方便访问资源
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}

private:
	T* _ptr;
};

二 C++标准库中的智能指针

2.1 C++标准库中的智能指针

• C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了, 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解 决智能指针拷⻉时的思路不同。
• auto_ptr 是C++98时设计出来的智能指针,他的 特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象 ,这是⼀个⾮常糟糕的设计,因为他会到被 拷⻉对象悬空,访问报错的问题。
auto_ptr: 这个智能指针有两个问题:
1)这种方式并不那么复合我们的要求,我们进行拷贝后两个智能这种应该指向同一块资源共同管理。
2)由于进行了管理权的转移,会形成悬空问题,如果此时有人不知道对其进行访问麻烦就大了。【这种方法很不好,所以现在好少人会使用这个,甚至现在又很多公司已经禁止使用这个智能指针】,为了弥补这个的不足C++11引入新的智能指针。
• unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的 特点的不⽀持拷⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他(右值、move(左值)。
unique_ptr 本质和 auto_ptr 一样:
只支持资源的转移(不就和auto_ptr一样),但是要注意这里是对右值(一些临时对象等等)也没人用,这样一想也没有问题。这里这个行为是知道的(auto_ptr是不知道的),虽然和auto_ptr一样会导致悬空问题的发生,但是性质是不一样的这里是自己知道的,自己要为自己负责(语法行为不是库的问题)。
• shared_ptr 是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的 特点是⽀持拷⻉,也⽀持移动。如果需要拷⻉的场景就需要使⽤他了底层是⽤引⽤计数的⽅式实现的。
• weak_ptr 是C++11设计出来的智能指针,他的名字翻译出来是弱指针, 他完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题
智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃 。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤ 对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器, 在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点, unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
• template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);
• shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
• shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
• shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换
成智能指针对象。

【unique_ptr、shared_ptr的接口两者的接口差不多可能由于特性的区别有一点差异】

相同接口)


•get接口:获取底层指针

• relase接口:手动释放置成空(想提前释放资源可以使用)

• reset接口:把之前的释放掉返回一个新的指针

• relase和reset接口的区别:

relase):主动释放

reset):把旧的释放返回一个新的

•operator bool接口:

这个接口涉及到类型的转换 ,首先类型的转换要有一定的联系(例如内置类型自定义类型可以转换成另外一个类型;而其他类型[内置类型/自定义类型都可以]转换成自定义类型要依靠于对应的构造函数)。

涉及这个接口的意义:
自定义类型转换为内置类型 由operator 内置类型( ) 支持。

示例:自定义联系转换为bool

自定义类型转换为bool类型:这个一定不是通过构造函数实现的我们不可能给bool类型写一个构造函数(也写不出来),但是很多地方还是要支持自定义类型转换内置类型的(例如在智能指针中就有一个检查这个智能指针是否为空的接口,即有没有管理资源),因为这里不是上面两种转换情况,所以这里设计这个接口,但是库中的写法很奇怪(括号的位置),因为这个没有返回值比较特殊,返回值就是bool,本来是bool operator(),而强转是(bool) x 这不就变成了仿函数,所以这里是没办法只能这样。

接口的不同)

shared_ptr的构造函数中一些使用了explici修饰(防止编译器中间不优化产生临时对象)因为shared_ptr支持拷贝如果此时发生拷贝没有优化就麻烦了。

shared_ptr重载了<<所以其支持直接打印

shared_ptr因为其有一个特殊的对象引用计数,所以其一定增加了一些引用计数相关的接口

unique接口:判断资源是不是只有一个对象管理(即引用计数是不是1),个人感觉这个接口是多余的因为其还有use_count接口获取对象的引用计数。

2.2为什么又shared_ptr还要unique_ptr

因为shared_ptr中又引用计数,所以我们要维护底层的引用计数、并且其正常拷贝这两个都是有代价的,即shared_ptr有一定的性能消耗。如果不需要拷贝为啥还用shared_ptr用unique_ptr不更好,这就是其存在的意义(如果不用拷贝用两者的可以,都是用unique_ptr性能更优)。

2.3 C++标准库中的智能指针的使用

1)auto_ptr

由于其底层是资源管理权的转移,不用使用原对象进行访问即可

cpp 复制代码
struct Date
{
	int _year;
	int _month;
	int _day;

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	auto_ptr<Date> ap1(new Date(2025, 6, 6));
	// 拷贝时管理权转移,就导致ap1悬空
	auto_ptr<Date> ap2(ap1);

	// 悬空访问
	ap1->_year++;//由于发生资源管理权的转移即原对象指向空(发生悬空),此时就不能使用原对象进行访问

	return 0;
}

auto_ptr运行结果)

2 unique_ptr)

取底层只支持移动不支持构造,底层一定把构造函数好拷贝赋值设置为delete,所以在所有一定要注意其不支持拷贝,在初始化时不能使用{ }初始化因为{ }初始化走的过程是构造,在拷贝构造编译器优化为直接构造,而unique_ptr是不支持构造的所以这种方法初始化时不行的。

cpp 复制代码
int main()
{
	//unique_ptr<Date> up1 = { 2025, 6, 6 };
	//不支持{ }初始化其走的过程会使用到构造,而unique_ptr不支持构造
	unique_ptr<Date> up1(new Date(2025, 6, 6));
	// 不支持拷贝
	//unique_ptr<Date> up2(up1);

	// 可以移动,up1也不能使用
	//unique_ptr<Date> up2(move(up1));

	cout << up1.get() << endl;
	//up1.release();
	//up1.reset(new Date);

	// operator bool()
	//if (up1.operator bool())
	if (up1)
	{
		cout << "up1 不为空" << endl;
	}
	else
	{
		cout << "up1 为空" << endl;
	}

	return 0;
}

3 shared_ptr)

cpp 复制代码
int main()
{
	shared_ptr<Date> sp1(new Date(2025, 6, 6));
	// 支持拷贝
	shared_ptr<Date> sp2(sp1);

	cout << sp1 << endl;
	cout << sp2 << endl;

	cout << sp1.use_count() << endl;

	auto sp3 = make_shared<Date>(2025, 6, 7);

	return 0;
}

2.4智能指针的原理

2.4.1auto_ptr的原理

auto_ptr的原理为:管理权转移!通过auto_ptr对动态分配内存资源进行管理,但如果在使用过程中发生拷贝、赋值等行为时,直接将对资源的管理权转移给新对象!所以其实现还是很简单的在拷贝构造对象和拷贝赋值中进行管理权转移即可。

【实现代码】

cpp 复制代码
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{
	}
	auto_ptr(auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		// 管理权转移
		sp._ptr = nullptr;
	}
	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		// 检测是否为⾃⼰给⾃⼰赋值
		if (this != &ap)
		{
			// 释放当前对象中资源
			if (_ptr)
				delete _ptr;
			// 转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	// 像指针⼀样使⽤
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

2.4.2 unique_ptr的原理

因为其底层不支持构造只支持移动,所以其底层一定是将其构造函数和拷贝赋值函数设置为delete。

cpp 复制代码
template<class T>
class unique_ptr
{
public:
	explicit unique_ptr(T* ptr)
		:_ptr(ptr)
	{
	}
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	// 像指针⼀样使⽤
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//底层将构造函数和赋值拷贝构造函数置为dekete
	unique_ptr(const unique_ptr<T>& sp) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
	unique_ptr(unique_ptr<T>&& sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}
	unique_ptr<T>& operator=(unique_ptr<T>&& sp)
	{
		delete _ptr;
		_ptr = sp._ptr;
		sp._ptr = nullptr;
	}
private:
	T* _ptr;
};

2.4.3 shared_ptr的原理

shared_ptr底层就不一样了,其成员变量还有用于储存引用计数的变量,shared_ptr通过引用计数的方式实现多个对象间资源共享!每一个shared_ptr对象内部会维护一把计数器,用来记录该资源被几个对象共享。

**在shared_ptr对象构造时,引用计数加1;析构时,引用计数减1。但需要主要的是,析构时只有引用计数减为0,也意味着自己是这块资源的最后使用者,此时才能对所管理的资源进行释放!!**但对象构造和析构时都需要对引用计数进行操作。此时多执行并发访问时,可能会导致数据不一致问题!所以我们需要对计数器进行加锁!!

2.4.3.1 简单的shared_ptr的实现

【那怎么实现引用计数了】

方法一(前面STL中的思路 不行):

给其增加一个记录引用计数的成员变量,例如一个shared_ptr对象sp1此时sp1的引用计数是1,如果此时执行shared_ptr sp2(sp1),此时sp2和sp1的引用计数对于二,如果此时sp1析构sp1的引用计数变成1,但是sp2的引用计数还是2(上面分析的原理应该此时sp2的引用计数应该是1),所以这种方法不行。即不能各自有自己的引用计数。

方法二 (方法一中说了不能各自有自己的引用计数这里不就可以使用static静态变量储存这种方法也不行)

虽然使用static静态变量储存此时的引用计数就是同一个引用计数了,但是其还会引发其他问题:例如例如一个shared_ptr对象sp1此时sp1的引用计数是1,如果此时执行shared_ptr sp2(sp1),此时sp2和sp1的引用计数对于二,如果此时还有一个shared_ptr对象sp3,由于此时储存引用计数的变量是静态变量所以sp3的引用计数也等于2(变量应该是1),所以这种方法也不行。其实我们需要的是应该资源配一个引用计数,这个资源就给智能指针管理引用计数。使用静态的成员变量储存其属于这个类型的属于对象共享一个引用计数,而我们需要的是资源和引用计数配对在一起。

方法三可行 (有一个资源就构造一个引用计数)

有一个资源就构造一个引用计数,如果有多个智能指针指向同一个资源就指向同一个引用计数,所以引用计数去堆上开(new).

【怎么解决shared_ptr的难点拷贝】

上面的引用计数的设置就是为拷贝而写,要注意的是要指向同一个资源,把你的指针,引用计数给我,引用计数在++即可。

【怎么实现shared_ptr的拷贝赋值】

sp1=sp3【sp1和sp1都是shared_ptr对象】

方法一(不行):

一上来肯定增加this的对象=给的对象(这样不就和上面的拷贝一样吗),这样是不行的,

拷贝:是一个对象初始化另一个对象,什么这个对象什么也没有,此时直接把资源和引用计数直接给我是没有问题的。

赋值:将一个已经的对象拷贝给了一个对象,即两个存在的对象,可能两个都有自己的资源和引用计数,如果还用这种实现就会导致sp1资源丢了

方法二(不行):
此时有人说sp1是已经存在的对象,我们先把sp1释放不就好了吗?

我们来进行下面操作:sp1;sp2(sp1);sp3;sp1=sp3【sp1、2、3都是shared_ptr对象】

如果此时sp1被释放了就会出问题,因为sp1,sp2之前是指向同一块资源,现在sp2还管着呢,如果此时采用这种方法直接释放sp1,sp1释放sp2就完了(即一个东西是我们两个人一起买的,现在你要用新的你就直接把旧的送人,不管我死活)

方法三

在赋值之前要判断赋值对象(sp1=sp3中的sp3)指向的资源是否还有别的智能指针在管理,通过引用计数就可以知道(引用计数==1就只有自己)。如果不只有自己管理就将引用计数--即可。

还要防止自己给自己赋值,而自己给自己赋值有两种

第一种直接的:sp1=sp1

第二种间接的:sp1=sp3;sp1=sp3

那怎么同时处理这种了,这里就不通过智能指针进行判断了,而是判断是否指向同一块资源(因为这里同一个对象会指向同一块资源)。

【实现代码】

cpp 复制代码
template <class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr = nullptr)
		:_ptr(ptr), _Pcount(new std::atomic<int>(1))
	{
	}

	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) // 防止直接或间接和自己赋值,指向同一块空间
		{
			release(); // 在赋值前,将自身引用计数--
			_ptr = sp._ptr;
			_Pcount = sp._Pcount;
			(*_Pcount)++;
			return *this;
		}
	}

	int use_count() const { return _Pcount->load(); }
	T* get() const { return _ptr; } // const 防止权限放大
	T* operator->() { return _ptr; }
	T& operator*() { return *_ptr; }


	~Shared_ptr()
	{
		release();
	}
private:
	void release()
	{
		if (--(*_Pcount) == 0)
		{
			delete _ptr;
			delete _Pcount;
		}
	}
private:
	T* _ptr;
	std::atomic<int>* _Pcount; // 不能设置为静态(static int count)
};
2.4.3.2 allocate_shared(shared_ptr的优化)

前面已经讲了shared_ptr有不足:shared_ptr中指向的资源就不说了,但是其还开辟了引用计数变量,如果此时大量使用(例如在容器中有很多数据每个数据用一个智能指针管理),此时就会有小块内存性能问题和内存的碎片化这两个问题,C++为解决这个问题而引入allocate_shared

2.4.3.3shared_ptr中删除器的实现(unique_ptr一样)
2.4.3.3.1库中shared_ptr怎么实现删除器

【为什么要实现定制删除器】

因为其底层的释放方式是delete,如果这里开辟空间用new没问题,但是如果使用malloc开空间呢?如果还使用delete释放就不行了,此时就有使用定制删除器(规定释放方法)。记住

【实现方法三种(仿函数,函数指针,lambda)一般不用函数指针麻烦】

从库中可以知道shared_ptr是一个函数模板,而unique_ptr使用在使用时的情况时不一样的。

shared_ptr可以在时传一个删除器(一个可调用对象)D del(del即delete),即底层还是delete只是传了删除器D,就会把那个资源指针传给D(可调用对象进行释放)。对于shared_ptr和删除器相关的都是函数模板,可以自动推导(即可以在函数模板时传,也可以在构造时传删除器);而对于unique_ptr是一个类模板(必须在函数时传,传对应的类型)所以一般使用仿函数或者lambda实现(不用函数指针很麻烦),但是要先定义一个lambda对象,在用decltype获得到其对应的类型传入,但是lambda时不支持构造等等,所以其无法推导,还要在后面函数中加对应的对象

【总结】

要释放这个东西的时候要定制删除器(仿函数,lambda),最好用shared_ptr用lambda,这也是C++库中的不足,shared_ptr设计成函数模板更好,又同样的思路为什么unique_ptr还用类模板。

2.4.3.3.2自己实现shared_ptr的删除器

这里我们可以和库中一样讲构造函数弄成函数模板,但是这里有一个问题,要保持删除器(D)的类型,如果不保持析构函数不知道怎么释放,但是要保持这个类型,而这个删除器的类型是构造函数的不是一整个类的用不了(除非和unique_ptr一样设置成类模板,写成整个类的构造可以用析构也可以用,就定义一个成员变量定制一个删除器,但是我们也知道这种方法的不好,所以我们不使用这种),这里我们可以使用包装器实现(各种类型都释放掉),此时还要一个问题,对于正常的类型时,就为调用删除器那个构造,此时包装器未走列表初始化,此时就使用包装器(function)的默认构造,function默认构造是个空的可调用对象,空的去调用就会抛异常,所以此时还得给一个缺省值。有了删除器后任何资源都可以给智能指针管理(可以控制释放方式)。

【有删除器的shared_ptr代码】

cpp 复制代码
template <class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr = nullptr)
		:_ptr(ptr), _Pcount(new std::atomic<int>(1))
	{
	}

	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr), _Pcount(sp._Pcount)
	{
		(*_Pcount)++;
	}

	template <class D>//定义有删除器的
	Shared_ptr(const Shared_ptr<T>& sp, D del)
		: _ptr(sp._ptr), _Pcount(sp._Pcount), _del(del)
	{
		(*_Pcount)++;
	}
	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr) // 防止直接或间接和自己赋值,指向同一块空间
		{
			release(); // 在赋值前,将自身引用计数--
			_ptr = sp._ptr;
			_Pcount = sp._Pcount;
			(*_Pcount)++;
			return *this;
		}
	}

	int use_count() const { return _Pcount->load(); }
	T* get() const { return _ptr; } // const 防止权限放大
	T* operator->() { return _ptr; }
	T& operator*() { return *_ptr; }


	~Shared_ptr()
	{
		release();
	}
private:
	void release()
	{
		if (--(*_Pcount) == 0)
		{
			_del(_ptr);
			delete _Pcount;
		}
	}
private:
	T* _ptr;
	std::atomic<int>* _Pcount; // 不能设置为静态(static int count)
	std::function<void(T*)> _del = [](T* ptr) { delete ptr; };//这里给缺省值
	//是赋值普通类型没有走删除器版本的构造,所以finction的默认值
};

2.4.4 weak_ptr

2.4.4.1 weak_ptr由来和用途

shared_ptr已经非常完美了,但还是存在一些问题。下面是一个链表结构,我们将两块资源分别交给智能指针shared_ptr进行管理,然后在每一块资源中,还存在一个shared_ptr智能指针指向对方资源。此时就会导致循环引用的问题!而weak_ptr就是对shared_ptr进行补贴,解决循环引用的问题!!

【库中的weak_ptr】

• weak_ptr 不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资 源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,
• weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的 资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤ lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如 果资源没有释放,则通过返回的shared_ptr访问资源是安全的

2.4.4.2shared_ptr的 循环引用问题
2.4.4.2 .1循环引用问题怎么形成的

【代码】

cpp 复制代码
struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl; // for debug
	}
};

int main()
{
	shared_ptr<ListNode> n1 = new ListNode;
	shared_ptr<ListNode> n2 = new ListNode;

	n1->_prev = n2;
	n2->_next = n1;
	return 0;
}

上面的场景:

  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
  2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
  3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
  4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
    • ⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏
2.4.4.2 2循环引用问题怎么解决

这里ListNode结构体中的_next和_prev改成weak_ptr即可。

【为什么可以】

我们看一下weak_ptr的底层,weak_ptr是一个特殊的智能指针,只有在循环引用中才会使用。其底层无指针构造,不支持资源管理(RAII),只绑定到对应的shared_ptr,而引用计数不变。所以把shared_ptr赋给weak_ptr时两者可以指向同一块资源,但是其引用计数不变。

【解决代码】

cpp 复制代码
template <class T>
class Shared_ptr
{
public:
	Shared_ptr(T * ptr = nullptr)
		:_ptr(ptr), _Pcount(new std::atomic<int>(1))
	{}

	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr), _Pcount(sp._Pcount)
	{
		(*_Pcount)++;
	}

	template <class D>
	Shared_ptr(const Shared_ptr<T>& sp, D del)
		:_ptr(sp._ptr), _Pcount(sp._Pcount), _del(del)
	{
		(*_Pcount)++;
	}
	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr) // 防止直接或间接和自己赋值,指向同一块空间
		{
			release(); // 在赋值前,将自身引用计数--
			_ptr = sp._ptr;
			_Pcount = sp._Pcount;
			(*_Pcount)++;
			return *this;
		}
	}

	int use_count() const { return _Pcount->load(); }
	T* get() const  { return _ptr; } // const 防止权限放大
	T* operator->() { return _ptr; }
	T& operator*() { return *_ptr; }


	~Shared_ptr()
	{
		release();
	}
private:
	void release()
	{
		if (--(*_Pcount) == 0)
		{
			_del(_ptr);
			delete _Pcount;
		}
	}
private:
	T* _ptr;
	std::atomic<int>* _Pcount; // 不能设置为静态(static int count)
	std::function<void(T*)> _del = [](T* ptr) { delete ptr; };
};



// 不参与Shared_ptr的引用计数,不支持RAII。
// 但如果Shared_ptr指向的空间释放,可能会导致野指针。
// 所以实际Weak_ptr会有一个use_count观察Shared_ptr引用计数
template <class T>
class Weak_ptr
{
public:
	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++11和boost中智能指针的关系

• 在C++98时,C++出现了第一个智能指针auto_ptr。

• 在99年时,boost成立。并且在C++98和C++11之间,boost给出了更为实用的智能指针:scoped_ptr和shared_ptr和weak_ptr。

• C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版

• C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。其中unique_ptr对应boost中的scoped_ptr,并且这些智能指针的实现原理是参照boost中的相关智能指针实现的!

本篇文章就到此结束,欢迎大家订阅我的专栏,欢迎大家指正,希望有所能帮到读者更好了解C++相关知识 ,C++相关知识就到此结束,后面我将继续更新Linux相关知识。觉得有帮助的还请三联支持一下~后续会不断更新算法与数据结构相关知识,我们下期再见。

相关推荐
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 74. 搜索二维矩阵 | C++ 二分查找 (一维展开法)
c++·leetcode·矩阵
沐知全栈开发2 小时前
SOAP 语法详解
开发语言
cch89182 小时前
PHP vs Java:谁更适合你的项目?
java·开发语言·php
lg_cool_2 小时前
Python 框架之py_trees
开发语言·数据结构·python
a里啊里啊2 小时前
常见面试题目集合
linux·数据库·c++·面试·职场和发展·操作系统
wjs20242 小时前
Go 语言函数
开发语言
攻城狮的梦2 小时前
线上接收附件回调超时排查复现
开发语言·php·lavarel
小小码农Come on2 小时前
QT面试题总结
开发语言·qt
克里普crirp2 小时前
北斗电离层模型BDGIM广播系数
开发语言·python