c++中的智能指针

发布时间:2024年01月18日

1. 智能指针概念及原理

1.1 智能指针概念

智能指针是一种现代 C++ 编程中的工具,用于确保程序不存在内存和资源泄漏且是异常安全的。智能指针是在 头文件中的 std 命名空间中定义的。它们对 RAII 或“获取资源即初始化”编程惯用法至关重要。智能指针的主要目的是确保资源获取与对象初始化同时发生,从而能够创建该对象的所有资源并在某行代码中准备就绪。实际上,RAII 的主要原则是为将任何堆分配资源(例如,动态分配内存或系统对象句柄)的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。在现代 C++ 中,原始指针仅用于范围有限的小代码块、循环或者性能至关重要且不会混淆所有权的 Helper 函数中。

智能指针原理是用对象的生命周期来控制程序资源,用来防止内存泄漏。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:1、不需要显式地释放资源。2、采用这种方式,对象所需的资源在其生命期内始终保持有效。

1.2 为什么使用智能指针

用如下代码看看现象:

void func()
{
	int a = 0, b = 0;
	cin >> a >> b;
	if (a == 10)
		throw "exception!";
}
int main()
{
	int* ptr1 = new int(1); // 1.如果在此处抛异常,结果如何?
	int* ptr2 = new int(2); // 2.如果在此处抛异常,结果如何?

	try
	{
		func(); // 3.如果在此处抛异常,结果如何?
	}
	catch(const char* message)
	{
		cout << message << endl;
	}
	delete ptr1;
	delete ptr2;

	return 0;
}

new失败会抛异常,在上述的3个抛异常处,只有1处抛异常不会造成问题,其余两处抛异常皆会造成资源泄露。
内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

1.3 用对象的生命周期来控制程序资源

用一个Pointer类来封装指针,随便抛异常,程序退出,自动调用类的析构函数,释放资源,代码如下:

template <class T>
class Pointer
{
public:
	Pointer(T* ptr): _ptr(ptr)
	{}
	~Pointer()
	{
		if (_ptr != nullptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
void func()
{
	int a = 0, b = 0;
	cin >> a >> b;
	if (a == 10)
		throw "exception!";
}
int main()
{
	Pointer<int> p1(new int(1));
	Pointer<int> p2(new int(2));

	try
	{
		func(); 
	}
	catch(const char* message)
	{
		cout << message << endl;
	}

	return 0;
}

2. 智能指针发展过程

2.1 auto_ptr

auto_ptr是最早设计的智能指针,它能够自动管理动态分配的内存,从而避免了内存泄漏的问题,但是有很明显的缺点,例如不能进行复制构造和赋值操作,否则会导致内存泄漏。如果用auto_ptr进行复制构造或赋值操作,那么auto_ptr会使用管理权转移原理,转移之后,原来的指针就不能使用,实验代码如下所示:

#include <iostream>
#include <memory>
using namespace std;

int main()
{
	auto_ptr<int> p1(new int(10));
	auto_ptr<int> p2(p1); // 在此处是浅拷贝,auto_ptr只是包装了一下,下面两行是简化后的
	//int* p1 = new int(10);
	//int* p2 = p1;
	//可以看到 拷贝是浅拷贝,所以释放资源会出现问题。

	cout << *p2 << endl;
	cout << *p1 << endl;

	return 0;
}

可以看到,将p1的权限转给p2后,p1就不能进行访问,所以auto_ptr是不安全的。
在这里插入图片描述
auto_ptr内部简易实现代码如下:

namespace k
{
	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr): _ptr(ptr)
		{}
		// 管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~auto_ptr()
		{
			if (_ptr != nullptr)
				delete _ptr;
		}
	private:
		T* _ptr;
	};
	void test1()
	{
		auto_ptr<int> p1(new int(10));
		auto_ptr<int> p2(p1);

		cout << *p2 << endl;
		cout << *p1 << endl;
	}
}

然后那些写库的大佬就对auto_ptr进行改进,拷贝会出问题,那么就直接不让拷贝,所以就有了unique_ptr,那么怎样防止拷贝呢?

2.2 防拷贝的方法

在C++中,防止拷贝的主要目的是防止对象被意外拷贝或拷贝后出现不可预期的行为。如果一个类被拷贝,它的成员变量也会被拷贝,这可能会导致内存泄漏、数据损坏或其他问题。 因此,防止拷贝可以提高程序的稳定性和安全性。

另外,防止拷贝还可以防止对象被多个线程同时访问,从而避免了线程安全问题。

在C++中,我们可以通过禁止在类外使用拷贝构造函数和赋值运算符的重载来达到防止该类被拷贝的目的。这可以通过将拷贝构造函数和赋值运算符声明为私有并不给出实现来实现。这样做的好处是,如果其他函数或友元函数调用这些函数,会导致链接错误。

在c++98中,防拷贝是用只声明,不实现 来解决的(设置为私有外界访问不到,调用会报错),具体代码如下:

template <class T>
class POINTER
{
public:
    POINTER(T* ptr)
        :_ptr(ptr)
    {}
    
    ~POINTER()
    {
        if (_ptr)
        {
            cout << "delete:" << _ptr << endl;
            delete _ptr;
        }
    }
private:
    POINTER(const POINTER<T>& ptr);
    POINTER<T>& operator=(const POINTER<T>& ptr);
private:
    T* _ptr;
};
int main()
{
    POINTER<int> ptr1(new int(1));
    POINTER<int> ptr2 = new int(2);
    // 拷贝,会报错
    POINTER<int> ptr3(ptr1);
    POINTER<int> ptr4 = ptr2;
	
    return 0;
}

另一种方法是使用C++11中的delete关键字。这种方法可以防止类的拷贝,而且更加简单明了,具体代码如下:

template <class T>
class POINTER
{
public:
    POINTER(T* ptr)
        :_ptr(ptr)
    {}

    ~POINTER()
    {
        if (_ptr)
        {
            cout << "delete:" << _ptr << endl;
            delete _ptr;
        }
    }
    POINTER(const POINTER<T>& ptr) = delete;
    POINTER<T>& operator=(const POINTER<T>& ptr) = delete;
private:
    T* _ptr;
};

int main()
{
    POINTER<int> ptr1(new int(1));
    POINTER<int> ptr2 = new int(2);
    // 拷贝,报错
    POINTER<int> ptr3(ptr1);
    POINTER<int> ptr4 = ptr2;

    return 0;
}

在这里插入图片描述

2.3 unique_ptr

unique_ptr是C++11标准库提供的一种智能指针,它能够自动管理动态分配的内存,从而避免了内存泄漏的问题,unique_ptr是独占式的,即完全拥有它所管理对象的所有权,不和其它的对象共享,unique_ptr禁用了拷贝构造和拷贝赋值构造,仅仅实现了移动构造和移动赋值构造,这也就使得它是独占式的。unique_ptr内部简易实现代码如下:

namespace k
{
	template <class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr) : _ptr(ptr)
		{}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~unique_ptr()
		{
			if (_ptr != nullptr)
				delete _ptr;
		}
		unique_ptr(unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
	private:
		T* _ptr;
	};
}

如下图,拷贝的话会直接报错:
在这里插入图片描述
总结,unique_ptr不能复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库(STL),unique_ptr是auto_ptr的替代者,因此建议使用unique_ptr。那么如果需要拷贝或赋值,那么该怎么弄?所以就出现了shared_ptr。

2.4 shared_ptr

在这里插入图片描述

shared_ptr 的主要功能是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向该对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。shared_ptr 所指向的资源具有共享性,即多个 shared_ptr 可以指向同一份资源,并在内部使用引用计数机制来实现这一点。shared_ptr 内存:每个 shared_ptr 对象在内部指向两个内存位置:指向对象的指针和用于控制引用计数数据的指针。shared_ptr 可以像普通指针一样使用,可以将 * 和 -> 与 shared_ptr 对象一起使用,也可以像其他 shared_ptr 对象一样进行比较。以下是 shared_ptr 的一些常用函数:
get() 函数,表示返回当前存储的指针(就是被 shared_ptr 所管理的指针)。
use_count() 函数,表示当前引用计数。
reset() 函数,表示重置当前存储的指针。
operator*,表示返回对存储指针指向的对象的引用。
operator->,表示返回指向存储指针所指向的对象的指针,以便访问其中一个成员。

以下是 shared_ptr 的一些使用方法:

构造函数:
shared_ptr ptr:ptr 的意义就相当于一个 NULL 指针。
shared_ptr ptr(new T()):从 new 操作符的返回值构造。
shared_ptr ptr2(ptr1):使用拷贝构造函数的方法,会让引用计数加 1。
shared_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用拷贝构造函数。
shared_ptr 的“赋值”:
shared_ptr a(new T()); shared_ptr b(new T()); a = b;,此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1。
shared_ptr 也可以直接赋值,但是必须是赋给相同类型的 shared_ptr 对象,而不能是普通的 C 指针或 new 运算符的返回值。
reset() 函数,已定义的 shared_ptr 指向新的 new 对象。
make_shared 辅助函数创建 shared_ptr。
以上是 shared_ptr 的一些基础应用。
简易shared_ptr代码如下:

namespace k
{
	template<class T>
	class shared_ptr
	{
	public:
		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))
			, _pmtx(new mutex)
			, _del(del)
		{}
		~shared_ptr()
		{
			Release();
		}
		void Release()
		{
			_pmtx->lock();

			bool deleteFlag = false;

			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					_del(_ptr);
				}
				delete _pcount;
				deleteFlag = true;
			}

			_pmtx->unlock();

			if (deleteFlag)
			{
				delete _pmtx;
			}
		}

		void AddCount()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
		{
			AddCount();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

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

				AddCount();
			}

			return *this;
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}

	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;

		// 包装器
		function<void(T*)> _del = [](T* ptr)
		{
			cout << "lambda delete:" << ptr << endl;
			delete ptr;
		};
	};
}

但在使用shared_ptr的过程中可能会出现一些问题,如循环引用。刚好weak_ptr 解决 shared_ptr 的循环引用问题,从而避免内存泄漏。
使用场景:

1.当你有两个对象 A 和 B,并且 A 拥有 B,B 也拥有 A 时(或者更复杂的循环引用情况),这时你应该使用 weak_ptr 来替代其中一个 shared_ptr,从而打破循环引用,防止内存泄漏。
2.当你想使用对象,但是并不管理对象,并且在需要时可以返回对象的 shared_ptr 时,可以使用 weak_ptr。

weak_ptr 介绍:它是 C++11 中的一个智能指针,是 shared_ptr 的辅助工具,用于协助 shared_ptr 观测资源的使用情况。weak_ptr 指向一个由 shared_ptr 管理的对象,但不会影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。weak_ptr 可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造,获得资源的观测权。weak_ptr 本身不参与引用计数,因此,它无法阻止引用计数变为零。但是,可以使用 weak_ptr 尝试获取初始化该副本的 shared_ptr 的新副本。weak_ptr 类型指针不会影响所指堆内存空间的引用计数,也就是说,weak_ptr 类型指针只能访问某一 shared_ptr 指针指向的堆内存空间,无法对其进行修改。以下是 weak_ptr 的一些常用函数:

operator=,重载赋值运算符,使 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
swap(x),其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
reset(),将当前 weak_ptr 指针置为空指针。
use_count(),查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired(),判断当前 weak_ptr 指针是否过期(指针为空,或者指向的堆内存已经被释放)。
lock(),如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
以下是 weak_ptr 的一些使用方法:

构造函数:
weak_ptr wp:wp 的意义就相当于一个 NULL 指针。
weak_ptr wp2(wp1):使用拷贝构造函数的方法,会让引用计数加 1。
weak_ptr wp3(sp):sp 是一个 shared_ptr 类型指针,wp3 指向 sp 所指向的堆内存空间。
weak_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用拷贝构造函数。
weak_ptr 的“赋值”:
weak_ptr a; weak_ptr b; a = b;,此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1。
weak_ptr 也可以直接赋值,但是必须是赋给相同类型的 weak_ptr 对象,而不能是普通的 C 指针或 new 运算符的返回值。
reset() 函数,已定义的 weak_ptr 指向新的 shared_ptr 对象。
make_shared 辅助函数创建 shared_ptr。

下面是一个使用 weak_ptr 的例子:

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A deleted" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;
    ~B() { std::cout << "B deleted" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;

    std::cout << "a use count: " << a.use_count() << std::endl;
    std::cout << "b use count: " << b.use_count() << std::endl;

    return 0;
}

在这个例子中,我们创建了两个类 A 和 B,并且在 A 中包含了一个指向 B 的 shared_ptr,在 B 中包含了一个指向 A 的 weak_ptr。这样做的目的是为了避免 A 和 B 之间的循环引用,从而导致内存泄漏。

在 main 函数中,我们创建了两个 shared_ptr 对象 a 和 b,并且将它们互相绑定。最后,我们输出了 a 和 b 的引用计数,可以看到它们的引用计数都是 2。

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