C++智能指针

目录

智能指针

需要智能指针的场景

cpp 复制代码
int div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
 	throw invalid_argument("除0错误");
  return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
	 int* p1 = new int;
	 int* p2 = new int;
	 cout << div() << endl;
	 delete p1;
	 delete p2;
}
int main()
{
 try
 {
 	Func();
 }
 catch (exception& e)
 {
 	cout << e.what() << endl;
 }
 return 0;
}

问题:如果Division函数因除0错误抛出异常,程序会直接跳转到main()中的catch块,Func()中delete语句将永远不会执行,导致p1和p2指向的堆内存被永久占用(内存泄漏)。

解决办法:当前函数中没有匹配的catch会退出当前函数栈,会对函数先前定义的对象调用析构,那么我们可以根据对象的析构帮助我们自动释放资源

cpp 复制代码
void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<double> sp2(new double[10]);
	cout << div() << endl;
}

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效
cpp 复制代码
template<class T>
class SmartPtr
{
public:
	// RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

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

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw invalid_argument("Division by zero condition!");
	}

	return (double)a / (double)b;
}

void Func()
{
	// RAII
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<double> sp2(new double[10]);

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

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

	return 0;
}

智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

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

	~SmartPtr()
	{
		cout << "~SmartPtr()->"<<_ptr << endl;

		delete _ptr;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

void TestSmartPtr1()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 1;

	SmartPtr<pair<string, int>> sp2(new pair<string, int>("xxxx", 1));
	sp2->first += 'y';
	sp2->second += 1;
	sp2.operator->()->second += 1;
}

总结智能指针的原理:

  1. RAII特性,利用对象生命周期来控制程序资源
  2. 重载operator*和opertaor->,具有像指针一样的行为。

std::auto_ptr

auto_ptr的实现原理:管理权转移的思想

cpp 复制代码
// C++98
// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
// 很多公司都明确规定了,不要用这个
template<class T>
class auto_ptr
{
public:
	// RAII
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete->" << _ptr << endl;
			delete _ptr;
			_ptr = nullptr;
		}
	}

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

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};

缺点:管理权转移,导致对象悬空,使用者不知道ap1已经悬空,对ap1解引用就会出问题

主要是在auto_ptr的拷贝构造函数中

cpp 复制代码
auto_ptr(auto_ptr<T>& ap)
    :_ptr(ap._ptr)  // ap2._ptr 接收 ap1._ptr 的值
{
    ap._ptr = nullptr;  // ap1._ptr 被置空(管理权转移)
}

从代码层面看,dz::auto_ptr ap2 = ap1; 是一个非常自然的拷贝操作,使用者很可能误以为ap1和ap2会 "共享" 资源

但实际ap1已经被悄无声息地置空,后续对ap1的任何指针操作(解引用、调用operator->)都会触发错误。

cpp 复制代码
void test_auto_ptr1()
{
	dz::auto_ptr<int> ap1(new int);
	dz::auto_ptr<int> ap2 = ap1;

	// 管理权转移,导致对象悬空
	(*ap1)++;
	(*ap2)++;
}

std::unique_ptr

C++11中开始提供更靠谱的unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝

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

	~unique_ptr()
	{
		cout << "delete->" << _ptr << endl;

		delete _ptr;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	// C++11
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

private:
	// C++98
	// 1、只声明不实现
	// 2、限定为私有
	//unique_ptr(const unique_ptr<T>& up);
	//unique_ptr<T>& operator=(const unique_ptr<T>& up);
private:
	T* _ptr;
};

std::shared_ptr

总有一些情况是需要我们拷贝的:

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

如果只是加一个int的成员变量计数:

两个对象有两个_count,sp1析构不影响sp2中_count,无法正确减去引用计数

如果选择用静态成员变量:

当有两块的资源需要管理时,本来静态成员是2,但sp3初始化时会把静态成员变成1

正确方法

初始化时new一块空间记录引用计数

拷贝时用pcount指针找到引用计数 - -

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	// RAII
	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();
	}

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

	// sp1 = sp3
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//_ptr指向同一块空间不需要赋值,如果是自己给自己赋值会出问题
		if (_ptr != sp._ptr)
		{
		    //如果--引用计数后 == 0 手动释放空间
			release();

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

			++(*_pcount);
		}

		return *this;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}

private:
	T* _ptr;
	int* _pcount;
};

循环引用的问题

cpp 复制代码
struct ListNode
{
	int val;
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;

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

void test_shared_ptr3()
{
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;//查看引用计数的变化
	cout << n2.use_count() << endl;

	// 循环引用
	n1->next = n2;
	n2->prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

当只有n1->next=n2 时是可以正常析构的:

以下是正常析构流程

出了作用域n2先析构,对象是定义在函数中的,而函数调用会建立栈帧。在栈帧里面,c++规定数据后进先出,所以n2后定义,先析构

n2的引用计数减到1,然后到n1析构,引用计数减到0,delete _ptr时会去delete ListNode,ListNode的析构要释放它的next成员,释放next的时候会释放右边的ListNode

循环引用:

n1->next = n2;

n2->prev = n1;

这种情况就会出问题

引用计数无法减到0,也就无法释放资源

为了解决循环引用,出现了weak_ptr

weak_ptr

增加计数就会有问题,那么weak_ptr不参与shared_ptr计数的管理
weak_ptr不支持RAII

cpp 复制代码
struct ListNode
{
	int val;
	weak_ptr<ListNode> next;
	weak_ptr<ListNode> prev;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
void test_shared_ptr3()
{
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	// 循环引用
	n1->next = n2;
	n2->prev = n1;

	cout << n1.use_count() << endl;//引用计数不会因为next,prev变化
	cout << n2.use_count() << endl;
}

用shared_ptr的_ptr初始化weak_ptr的_ptr

cpp 复制代码
shared_ptr中的get:
返回 shared_ptr 内部管理的原始指针(raw pointer)。
它不会改变引用计数,仅仅是返回指向所管理对象的指针值。
	T* get() const
	{
		return _ptr;
	}
cpp 复制代码
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;
};

原理图解:

当 n1->next = n2 时,不会增加 n2 的引用计数,因为next是一个weak_ptr,而weak_ptr没有引用计数

假设ListNode是一个 "人",next是这个人手里的 "望远镜"(weak_ptr),望远镜指向另一个人(另一个ListNode)。

  • 望远镜(next)的存在依赖于它的主人(ListNode),主人消失了,望远镜也会被销毁(调用weak_ptr的析构)。
  • 但望远镜(weak_ptr)的作用只是 "观察" 远处的人,不会决定远处的人何时消失(那是远处人的 "主人"------shared_ptr的责任)。

总结:

  • next是weak_ptr的一个实例,它的生命周期由所在的ListNode对象决定(随ListNode的创建而创建,销毁而销毁)。
  • weak_ptr的设计目的是 "弱引用其他资源",而非 "管理自身作为成员变量的存在"。
  • 区分 "weak_ptr对象自身的生命周期" 和 "它所指向的资源的生命周期",就能理解为什么说它 "不管理资源"------ 它对前者的影响是被动的(随宿主销毁),对后者则完全无所有权(不影响其生死)。

如果share_ptr的生命周期到了,但weak_ptr的生命周期还没到,share_ptr把资源释放,weak_ptr就变成野指针了,为什么这么说

cpp 复制代码
// 创建 shared_ptr,管理一块ListNode资源
    shared_ptr<ListNode> sp(new ListNode);  
// weak_ptr观察该资源
    weak_ptr<ListNode> wp = sp;  

wp为了观测,构造的时候拿到了shared_ptr里的_ptr,但是这个_ptr是shared管的,如果_ptr销毁了,wp还去使用_ptr就会出现野指针问题

为了解决这个问题

库中的weak_ptr不参与share_ptr引用计数的管理,但是可以用use_count()看引用计数,可以用expired()检测指向的资源有没有释放

定制删除器

我们之前写的shared_ptr都是针对new出来的,那如果不是new出来的,就不能直接delete

cpp 复制代码
// 简化版shared_ptr实现,只演示调用逻辑
template<class T>
class shared_ptr {
public:
    // 1. 构造函数:管理单个对象,使用默认删除器
    explicit shared_ptr(T* ptr = nullptr) 
        : _ptr(ptr), 
          _count(ptr ? new size_t(1) : nullptr),
          _deleter(DefaultDelete<T>()) {}  // 默认删除器

    // 2. 构造函数:支持自定义删除器(如数组删除器)
    template<class Deleter>
    shared_ptr(T* ptr, Deleter deleter)
        : _ptr(ptr),
          _count(ptr ? new size_t(1) : nullptr),
          _deleter(deleter) {}  // 使用用户提供的删除器
    // 3. 析构函数
    ~shared_ptr() 
    {
        if (_count && --(*_count) == 0) 
        {
            _deleter(_ptr);  // 调用删除器释放资源
            delete _count;   // 释放引用计数
            _ptr = nullptr;
            _count = nullptr;
        }
    }
private:
    T* _ptr;                  // 指向管理的资源
    size_t* _count;           // 引用计数
    std::function<void(T*)> _deleter;  // 存储删除器(可调用对象)
};

在1.构造函数中,_deleter(DefaultDelete()) {} // 默认删除器

就是设置删除器为默认删除器:delete删除

如果不想delete删除,想自定义删除,就要由用户自己提供(2.构造函数)

cpp 复制代码
 // 1. 构造函数:管理单个对象,使用默认删除器
    explicit shared_ptr(T* ptr = nullptr) 
        : _ptr(ptr), 
          _count(ptr ? new size_t(1) : nullptr),
          _deleter(DefaultDelete<T>()) {}  // 默认删除器
// 2. 构造函数:支持自定义删除器(如数组删除器)
    template<class Deleter>
    shared_ptr(T* ptr, Deleter deleter)
        : _ptr(ptr),
          _count(ptr ? new size_t(1) : nullptr),
          _deleter(deleter) {}  // 使用用户提供的删除器

用户提供的删除器可以自由选择:仿函数形式,lambda表达式,以及包装器

cpp 复制代码
// 仿函数的删除器
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()
{
 	std::shared_ptr<int> sp1((int*)malloc(4), FreeFunc<int>());
 	std::shared_ptr<int> sp2(new int[10], DeleteArrayFunc<int>());
    
 	std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
 	return 0;
}

线程安全问题

学了C++线程库再看,就知道什么是原子操作,什么是锁,为什么会有线程安全问题了

可以移步我的另一篇博客:C++线程库的学习

引用计数不是原子操作,两个线程同时去拷贝的时候,引用计数原来是1,两个线程都拷贝++了两次之后,引用计数可能还是2

解决方法1:通过锁的方法控制

解决方法2:atomic把引用计数变为原子操作

int* _pcount;变为atomic< int >* _pcount;

就可以不用锁,保证++(*_pcount)不可被中断

cpp 复制代码
template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new atomic<int>(1))
		{}

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

		// function<void(T*)> _del;

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//cout << "delete->" << _ptr << endl;
				//delete _ptr;
				_del(_ptr);

				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

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

		// sp1 = sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();

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

				++(*_pcount);
			}

			return *this;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		//int* _pcount;
		atomic<int>* _pcount;

		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};

当然,我们演示的是自己写的shared_ptr会出现的问题
库里的shared_ptr本身是线程安全的,里面引用计数的增减是原子操作
但是std::shared_ptr所指向的对象的访问不是线程安全的

发展历史