C++之智能指针的介绍与实现

1.RAII

(1)RAII说白了就是智能指针的中文名。可以用于管理其指向的资源,也就是其作为一个对象时可以根据其自身的生命周期来控制指向资源的释放时间,同时也具备常规指针的功能即调用与修改资源数据。

(2)C++标准库的智能指针在memory头文件中。

智能指针只支持浅拷贝,原因在于C++希望多个智能指针能同时管理一块资源,但此时就要解决一块空间被析构多次可能的情况。

1.auto_ptr:c++98的产物,当将一个auto_ptr的数据拷贝给另一个auto_ptr时,被拷贝的auto_ptr的数据就被清空了,也就是auto_ptr只允许一个指针指向一块空间,但这与预期不同且旧指针都被悬空了,浪费资源。

下面三都出自于c++11

2.unique_ptr:不支持拷贝构造,但支持移动构造,能用了但还是与预期不符,但在不需要拷贝指针时的使用效率很高。

3.shared_ptr:即支持拷贝构造,也支持移动构造,是智能指针的核心。

4.weak_ptr:是shared_ptr的小弟,用于解决share_ptr使用时可能会出现的一些问题的。

2.智能指针的出处

boost是一个C++的扩展库,C++11的不少语法都是出自于此。(也就是boost是C++的一个民间交流社区)

shared_ptr的底层实现是采用引用计数实现的,核心大框为:

cpp 复制代码
template<class T>
struct shared_ptr
{
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{ }
    ~shared_ptr()
    {
    	if (--(*_pcount) == 0)
    	{
	    	delete _ptr;
	    	delete _pcount;
	    }
    }
	T* _ptr;
	int* _pcount;
};

采用开辟新空间而非使用static的原因:

static成员变量被所有类变量共用,即使用static成员变量的话,该类就只能一直维护一块区域了,当不同变量指向不同空间时,该static成员变量就失效了。而使用新空间搭配拷贝构造就可以实现不同空间有独立的记数空间了:

cpp 复制代码
shared_ptr(const shared_ptr<T>& p)
	:_ptr(p._ptr)
	,_pcount(p._pcount)
{
	(*_pcount)++;
}

=重载:

cpp 复制代码
shared_ptr<T>& operator=(shared_ptr<T>& v)
{
	if (_ptr != v._ptr)
	{
		if ((--(*(_pcount))) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
		_ptr = v._ptr;
		_pcount = v._pcount;
		*(_pcount)++;
		return *this;
	}
}

为什么不用this != v,原因:两智能指针指向同一块空间但两指针自身的地址并不相同,此时就会出错。

3.析构问题

智能指针(share_ptr和unique_ptr)析构默认使用delete,那么当管理的不是new出来的的资源时程序调用析构时就会崩溃。

解决方法:

(1)[]的特化

cpp 复制代码
std::shared_ptr<Date> sp1(new Date);
//std::shared_ptr<Date> sp2(new Date[10]);会崩溃
//C++对[]的情况进行了特化处理从而不会崩溃
std::shared_ptr<Date[]> sp2(new Date[10]);

(2)使用删除器(一个实参:函数指针,仿函数,参数包,lambda)

例:c++11库中截的

cpp 复制代码
template <class U, class D> shared_ptr (U* p, D del);
template <class D> shared_ptr (nullptr_t p, D del);

也就是所有情况在析构时都统一调用删除器来进行资源释放。

例:

cpp 复制代码
class Fclose
{
public:
	void operator()(FILE* ptr)
	{
		fclose(ptr);
	}
};
int main()
{
    //使用仿函数
	shared_ptr<FILE> p4(fopen("Test.cpp", "r"), Fclose());
    //使用lambda(返回值为void可以省略)
    shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr);});
	return 0;
}

C++中的智能指针会自动帮我们给删除器调用参数。

4.shared_ptr和unique_ptr传删除器的区别

shared_ptr的删除器直接传函数指针,仿函数,参数包,lambda的变量即可,但unique_ptr不行。

unique_ptr传的是参数模板的类型,因此除了仿函数都不太好用.例lambda:

lambda的类型正常情况下是拿不到的,想要传lambda的类型就要在参数列表处加decltype(一个函数,用与返回参数的类型,是直接返回类型,不是字符串),且只传类型还不行,因为lambda的构造函数是不允许被调用的,因此还得再加一个lambda变量来通过拷贝构造来生成新的lambda变量来进行析构,因此直接传仿函数是最方便的。

cpp 复制代码
//fcloseFunc是一个lambda变量
unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);

5.定制删除器的实现

回顾function:一个能用于回调的参数包,只需知道返回值与参数类型即可接收可调用对象。

cpp 复制代码
template<class T>
struct share_ptr
{
	share_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{ }
	share_ptr(const share_ptr<T>& p)
		:_ptr(p._ptr)
		,_pcount(p._pcount)
	{
		(*_pcount)++;
	}
    template<class D>
    share_ptr(T* ptr,D del)
    :_ptr(ptr)
    ,_pcount(new int(1))
    //只要传值的人知道del的类型与_del的类型匹配即可
    ,_del(del)
    {}
	~share_ptr()
	{
		if (--(*_pcount) == 0)
		{
          //此处的释放资源就同过删除器来处理了
			_del(ptr);
			delete _pcount;
		}
	}
    //只需知道返回值与参数类型即可接收可调用变量
	function<void(T*)> _del;
	T* _ptr;
	int* _pcount;
};

make_shared:一个函数,用于生成一个shared_ptr的。

cpp 复制代码
shared_ptr<int> a = make_shared<int>(721);

其有一个好处:我们自己生成shared_ptr时,会调用两次构造函数,不仅效率低,还容易产生内存碎片,而make_shared会将count的空间放在资源空间上面且一次性开辟完,此时就只需开辟一次且不会产生内存碎片。

C++11的智能指针还自带bool重载,既可以像常规指针那样判空:

cpp 复制代码
 //本质if (sp1.operator bool())返回的就是0或1
if (sp1)
    cout << "sp1 is not nullptr" << endl;

//本质if (!sp4.operator bool())
if (!sp4)
    cout << "sp4 is nullptr" << endl;

还有:

cpp 复制代码
//支持构造函数
share_ptr<int> sp1(new int(1));
//不支持隐式类型转换
//share_ptr<int> sp2 = new int(1);err//or

原因:shared_ptr和uniqued_ptr都使用了explict修饰,来防止普通类型的指针隐式转换为智能指针对象。

6.weak_ptr

循环引用问题:(不使用ListNode*的原因是指针向的时候与智能指针的类型不匹配)

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

int main()
{
    std::shared_ptr<ListNode> n1(new ListNode);
    std::shared_ptr<ListNode> n2(new ListNode);
    n1->_next = n2;
    n2->_prev = n1;
    return 0;
}

上面的代码有一个问题:n1->_next指向n2,n2的count++.n2->_next指向n1,n1的count++.

此时会出现想要释放右边结点,就先需要释放n1的next,释放next就需要释放n1,释放n1就需要释放n2的prev,释放prev就需要释放n2.一根筋两头堵了。

此时就要使用weak_ptr,其就是专门用于处理shared_ptr的循环问题的。

可以发现weak_ptr就只能用同类型或shared_ptr来构造生成。

用途:

(1)其指向与shared_ptr同一块空间时,该空间的count不++。

(2)weak_ptr也不具备访问数据的能力,但其依旧会指向记数空间,目的是防止自己被悬空变野指针。

(3)weak_ptr的一个成员函数expired,就可以通过判断记数空间的值是否为0来确认自己指向的空间是否已被释放。

(4)weak_ptr的一个成员函数lock,用于给当前weak_ptr指向的空间生成一个新的shared_ptr指向该空间,用于延长该空间的存活时间防止weak_ptr自身变成野指针。

7.补充

智能指针本身是多线程安全的,但其指向的资源不一定线程安全。

内存泄漏的解决方式最好还是多用智能指针。

相关推荐
墨^O^1 小时前
C++ Memory Order 完全指南:从 relaxed 到 seq_cst,深入理解无锁编程与 happens-before
linux·开发语言·c++·笔记·学习·算法·缓存
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 51. N 皇后 | C++ 回溯算法&状态数组
c++·算法·leetcode
脱氧核糖核酸__2 小时前
LeetCode热题100——41.缺失的第一个正数(题解+答案+要点)
数据结构·c++·算法·leetcode·哈希算法
脱氧核糖核酸__2 小时前
LeetCode热题100——73.矩阵置零(题目+题解+答案)
c++·算法·leetcode·矩阵
智者知已应修善业2 小时前
【51单片机数码管+蜂鸣器的使用】2023-6-14
c++·经验分享·笔记·算法·51单片机
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(七):<线程同步与互斥>线程同步(下)
java·linux·运维·服务器·c++·学习·操作系统
c++逐梦人2 小时前
C++ RAII流式日志库实现
开发语言·c++
t***5442 小时前
还有哪些设计模式适合现代C++
开发语言·c++·设计模式
Wave8452 小时前
C++ 面向对象基础:类、访问权限,构造函数,析构函数
开发语言·c++