四种常见智能指针的介绍

发布时间:2023年12月31日

一、介绍

当类中有指针成员时,一般有两种方式来管理指针成员:一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;另一种更优雅的方式是使用智能指针(smart pointer),从而实现指针指向的对象的共享。

智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。

一句话简单概括:智能指针是帮助我们管理动态内存分配的,会自动帮忙释放new出来的内存,从而避免内存泄漏的问题。

二、为什么要使用智能指针

答:使用智能指针可以有效的避免内存泄漏。

什么是内存泄漏?

答:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

如下例子:人为原因导致忘记释放;代码逻辑原因抛出异常使内存释放执行不了。

void MemoryLeaks()
{
    // 1.内存申请了忘记释放
    int* p1 = (int*)malloc(sizeof(int));
    int* p2 = new int;
    
    // 2.异常安全问题
    int* p3 = new int[10];
    Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
    delete[] p3;
    
}

常见的内存泄漏

堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 释放。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

三、智能指针的使用及原理

RAII思想

当我们使用一块资源时,一般会经历三个步骤:a.获取资源(创建对象);b.使用资源;c.销毁资源(析构对象)。但我们通常会忘记c销毁资源,从而导致一些内存问题的产生,而RAII的思想,就可以巧妙的解决c步骤的发生。

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术 。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。实际上是把管理一份资源的责任托管给了一个对象。这种做法有两大好处:不需要显式地释放资源;采用这种方式,对象所需的资源在其生命期内始终保持有效 。

原理

智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。

即具有RAII思想,且重载有operator*和operator->像指针一样的行为的一种指针类型。

一个智能指针模板类,至少具有如下格式:

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)  // 构造
    	: _ptr(ptr)
    {}

    ~SmartPtr()  // 释放
    {
    	if(_ptr)
    	delete _ptr;
    }
    T& operator*() {return *_ptr;}  // 重载 *
    T* operator->() {return _ptr;}  // 重载 ->
private:
    T* _ptr;
};

四、常见的智能指针类型

头文件 #include <memory>

1.auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针,其实现原理是管理权转移的思想。

这就导致了很不好的情况产生如下代码,将对象p1转移给p2时,不会发生任何问题,但是当访问到被转移后的p1时,就会报错。

auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2=p1; //auto_ptr不会报错
cout << *p2 << endl;  // 访问没有问题
cout << *p1 << endl;  // 会报错

在p1赋值给p2后,首先p2会先将自己原先托管的指针释放掉,然后接收托管p1所托管的指针,然后p1所托管的指针置NULL,也就是p2托管了p1的指针,而p1放弃了托管,所以再次访问到p1时,就会发生错误。

即auto_ptr:存在潜在的内存崩溃问题。所以在C++11中,auto_ptr已经被弃用了。并且企业中也会禁止使用auto_ptr。

模拟实现:

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		if (this != &ap)  // 检测是否为自己赋值
		{ 
			if (_ptr)  // 先释放自己的空间,再获取别的对象
			{
				std::cout << "Delete:" << _ptr << std::endl;
				delete _ptr;
			}
            // 转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}

	~auto_ptr()
	{
		if (_ptr)
		{
			std::cout << "Delete:" << _ptr << std::endl;
			delete _ptr;
		}
	}

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

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

private:
	T* _ptr;
};

2.unique_ptr

unique_ptr避免了auto_ptr中出现的问题,而它的解决方法也更简单粗暴,即禁用拷贝赋值。

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露,例如,以new创建对象后因为发生异常而忘记调用delete时的情形特别有用。

unique_ptr特性

  1. 基于排他所有权模式:两个指针不能指向同一个资源。
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值。
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的。
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 不允许
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 允许

当程序试图将一个 unique_ptr 赋值给另一个时,如果源unique_ptr 是个临时右值(#2),编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做(#1)。

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

另外:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++11标准中的库函数std::move(),能够允许将一个unique_ptr赋给另一个。

std::unique_ptr<std::string> p1(new std::string("test move copy"));
std::unique_ptr<std::string> p2 = std::move(p1);
std::cout << p1.get() << std::endl;
std::cout << p2.get() << std::endl;

但是,通过运行结果,可以看出,赋值后,p1还是被置空了,结果与auto_ptr一致了。所以,unique_ptr会禁止拷贝复制。

与auto_ptr区别:禁用了拷贝赋值操作。

模拟实现:

template<class T>
class unique_ptr
{
private:
public:
	unique_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	// 防拷贝 C++11, 禁用拷贝赋值
	unique_ptr(unique_ptr<T>& ap) = delete;
	unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;

	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "Delete:" << _ptr << endl;
			delete _ptr;
		}
	}

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

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

private:
	T* _ptr;
};

3.shared_ptr

原理:

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

  1. shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享,发生拷贝或赋值时+1。可以通过成员函数use_count()来查看资源的所有者个数 。
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

成员函数:

  • use_count 返回引用计数的个数。
  • unique 返回是否是独占所有权( use_count 为 1)。
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)。
  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少。
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的。

use_count 使用:

int main()
{
    std::shared_ptr<std::string> sp1;

    std::shared_ptr<std::string> sp2(new std::string("test"));

    std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;    // 未发生拷贝或复制 0
    std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 拷贝1次 +1

    // 共享
    sp1 = sp2;  // 赋值一次,sp2 +1,sp1内部变为sp2,所以结果应该与sp2一致

    std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 2
    std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 打印 2

    std::shared_ptr<std::string> sp3(sp1);  // sp3拷贝sp1; sp1 次数+1,sp3与sp1 一致
    std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 3
    std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl;  // 打印 3
    std::cout << "sp2	use_count() = " << sp3.use_count() << std::endl << std::endl;  // 打印 3

    return 0;
}

运行结果:

可以发现,三次发生计数+1操作的位置在sp2(new std::string("test"))sp1 = sp2; sp3(sp1)三个拷贝、赋值的地方。

reset使用:

int main()
{
    std::shared_ptr<std::string> sp1;

    std::shared_ptr<std::string> sp2(new std::string("test"));

    std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;    // 未发生拷贝或复制 0
    std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 拷贝1次 +1

    // 共享
    sp1 = sp2;  // 赋值一次,sp2 +1,sp1内部变为sp2,所以结果应该与sp2一致

    std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 2
    std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 打印 2

    std::shared_ptr<std::string> sp3(sp1);  // sp3拷贝sp1; sp1 次数+1,sp3与sp1 一致
    std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 3
    std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl;  // 打印 3
    std::cout << "sp2	use_count() = " << sp3.use_count() << std::endl << std::endl;  // 打印 3

    // reset使用
    sp1.reset();
    std::cout << "sp1	use_count() = " << sp1.use_count() << " " << sp1 << std::endl;
    std::cout << "sp2	use_count() = " << sp2.use_count() << " " << sp2 << std::endl;
    std::cout << "sp3	use_count() = " << sp3.use_count() << " " << sp3 << std::endl << std::endl;


    return 0;
}

由运行结果可见,sp1调用reset后,sp1本身被置空,它对应的计数也变为0,而sp2和sp3,会随着sp1调用reset导致计数会减1。

unique使用:

std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = p1;

// #1
std::cout << "p1 is unique: " << std::boolalpha << p1.unique() << std::endl; // 输出:p1 is unique: false
std::cout << "p2 is unique: " << std::boolalpha << p2.unique() << std::endl; // 输出:p2 is unique: false

// #2
std::shared_ptr<int> p3(new int(20));
std::cout << "p3 is unique: " << std::boolalpha << p3.unique() << std::endl; // 输出:p3 is unique: true

// #3
p2.reset();
std::cout << "p1 is unique: " << std::boolalpha << p1.unique() << std::endl; // 输出:p1 is unique: true
std::cout << "p2 is unique: " << std::boolalpha << p2.unique() << std::endl; // 输出:p2 is unique: false

#1 p1进行了一次赋值给p2,所以发生了一次共享,所以不是独占所有权false。

#2 p3,它指向一个新的int类型的对象,此时p3是该对象的唯一所有者。

#3 使用reset函数将p2重置为空,此时p1是该对象的唯一所有者,unique函数返回true。由于p2已经被置空,它不再指向任何资源,输出为false。

swap使用

    // 创建两个 shared_ptr 对象
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    std::shared_ptr<int> ptr2 = std::make_shared<int>(100);
	std::shared_ptr<int> ptr3 = ptr1;

    std::cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << std::endl;
    std::cout << "ptr2: " << *ptr2 << ", use_count: " << ptr2.use_count() << std::endl;

    // 使用 swap 交换资源
    ptr1.swap(ptr2);

    std::cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << std::endl;
    std::cout << "ptr2: " << *ptr2 << ", use_count: " << ptr2.use_count() << std::endl;

由运行结果可以看出,ptr1.swap(ptr2);执行后,ptr1与ptr2对应的值以及计数发生了交换。

shared_ptr的线程安全问题

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

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

模拟实现:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		: _ptr(ptr)
		, _pCount(new int(1))
	{}

	void Release()
	{
		if (--(*_pCount) == 0)
		{
			cout << "Delete:" << _ptr << endl;
			delete _ptr;
			delete _pCount;
		}
	}

	~shared_ptr()
	{
		Release();
	}

	// sp1(sp2)
	shared_ptr(const shared_ptr<T>& sp)
		: _ptr(sp._ptr)
		, _pCount(sp._pCount)
	{
		(*_pCount)++;
	}

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

		// 减减被赋值对象的计数,如果是最后一个对象,要释放资源
		/*if (--(*_pCount) == 0)
		{
			delete _ptr;
			delete _pCount;
		}*/
		Release();

		// 共管新资源,++计数
		_ptr = sp._ptr;
		_pCount = sp._pCount;

		(*_pCount)++;

		return *this;
	}

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

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

private:
	T* _ptr;

	// 引用计数
	int* _pCount;
};

4.weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
	public :
	std::shared_ptr<B> pb_;
	~A()
	{
		std::cout << "A delete\n";
	}
};
class B
{
	public :
	std::shared_ptr<A> pa_;
	
	~B()
	{
		std::cout << "B delete\n";
	}
};
void fun()
{
	std::shared_ptr<B> pb(new B());
	std::shared_ptr<A> pa(new A());
	pb->pa_ = pa;
	pa->pb_ = pb;
	std::cout << pb.use_count() << std::endl;
	std::cout << pa.use_count() << std::endl;
} 

int main()
{
	fun();
	return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用):

(没有调用析构)

如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb; 运行结果如下:

这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb->print(); 英文pb是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

文章来源:https://blog.csdn.net/qzt__l0ve/article/details/135319197
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。