C++:智能指针(RAII思想)

目录

1、什么是智能指针?

2、为什么需要智能指针

3、RAII思想及智能指针的原理

4、智能指针的发展

4.1 auto_ptr

4.2 unique_ptr

4.3 share_ptr

5、share_ptr的模拟实现

6、循环引用问题

7、share_ptr中的自定义删除器


1、什么是智能指针?

        智能指针是一种用于管理动态分配的内存的 C++ 类。它们提供了对堆内存的自动分配和释放,以防止内存泄漏和悬挂指针的情况。常见的智能指针包括 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。它们提供不同的所有权和生命周期管理模型,以满足不同的需求。

std::unique_ptr: 代表独占所有权的智能指针。它允许一个对象拥有对动态分配的内存的唯一所有权,当其超出作用域时,会自动释放所管理的内存。它不能被复制或赋值,因为复制会导致所有权转移,从而破坏唯一性。

std::shared_ptr: 代表共享所有权的智能指针。多个 shared_ptr 对象可以共享同一块动态分配的内存。内部维护一个引用计数,当所有 shared_ptr 对象都释放了对内存的引用时,才会释放内存。这种指针适用于需要多个所有者的情况。

std::weak_ptr: 代表弱引用的智能指针。它不会增加引用计数,也不会影响内存的释放。通常与 shared_ptr 搭配使用,用于避免循环引用导致的内存泄漏问题。可以通过 lock() 方法获取一个 shared_ptr,如果所指向的对象还存在的话。

2、为什么需要智能指针

我们来看一下这段代码:

#include<iostream>
using namespace std;
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;
}

1、在 Func() 函数中,如果 new int 抛出异常,导致内存分配失败,那么 p1 指针会成为悬空指针,没有办法释放动态分配的内存,因为没有对应的 delete 调用。这会导致内存泄漏。

2、同样地,如果 p2new int 抛出异常,会导致 p2 成为悬空指针,同样会造成内存泄漏。

3、如果在 div() 函数中,当用户输入的 b 为 0 时,会抛出一个 invalid_argument 异常。而在 Func() 中调用 div(),如果这个异常被抛出,那么 new int 分配的内存将无法被释放,同样会导致内存泄漏。

智能指针可以很好地解决这些问题。通过使用智能指针,我们可以确保在发生异常或函数退出时,动态分配的内存会得到正确的释放,从而避免内存泄漏。因为智能指针的析构函数会在对象超出作用域时自动调用 delete 操作,确保资源的正确释放。因此,将 p1p2 替换为 std::unique_ptr<int>std::shared_ptr<int> 将会更安全和可靠。

3、RAII思想及智能指针的原理

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这样做有两大好处:

无需显式释放资源 通过 RAII,资源的释放被嵌入到对象的析构函数中,因此不需要在代码中显式地释放资源。这样可以避免忘记释放资源而导致的内存泄漏或资源泄漏问题。

资源的自动管理 对象的生命周期由语言的自动对象生存期规则控制,因此当对象超出作用域时,资源会自动释放,从而确保资源在不再需要时被正确释放,避免资源泄漏和错误使用资源的问题。

智能指针设计:

#include<iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{

	}
	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
		}
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

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

这段代码定义了一个简单的模板类 SmartPtr,用于管理动态分配的内存。然后,在 Func 函数中创建了两个 SmartPtr<int> 对象,分别管理两个动态分配的整型变量,当对象超出作用域资源就会自动释放,然后调用了 div 函数进行除法运算。如果除数为零,则抛出 invalid_argument 异常。

仅仅释放资源还不能称为智能指针,所以需要将* 、->重载下,才可让其像指针一样去使用。

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;
};

4、智能指针的发展

4.1 auto_ptr

// std::auto_ptr<int> sp1(new int);
// std::auto_ptr<int> sp2(sp1); // 管理权转移     这个时候sp1已经失效了

auto_ptr 采用了独占所有权的模型,意味着当一个 auto_ptr 被赋值给另一个 auto_ptr 后,原始 auto_ptr 将失去对资源的所有权。这导致在代码中进行所有权转移时很容易出现问题,例如可能会导致资源多次释放或悬空指针。auto_ptr 存在诸多弊端,因此不建议在现代 C++ 中使用它。

自 C++ 11 起,此类模板已弃用。unique_ptr 是一个具有类似功能的新工具,但具有更高的安全性

4.2 unique_ptr

 既然auto_ptr的拷贝存在问题,那么unique_ptr直接禁用了赋值拷贝。

我们先来看一下

  1. 移动赋值运算符 (1): 接受一个右值引用参数,用于将资源所有权从一个 unique_ptr 转移到另一个。这种赋值运算符用于实现资源的移动语义,通常在转移所有权时使用。

  2. 赋值为 null 指针 (2): 接受一个 nullptr_t 参数,用于将 unique_ptr 赋值为 null 指针,即释放当前持有的资源,并将 unique_ptr 设置为 null。

  3. 类型转换赋值运算符 (3): 接受一个右值引用参数,用于将资源所有权从一个 unique_ptr 转移到另一个,但允许指定不同的模板参数类型和删除器类型。这种赋值运算符用于允许更灵活的类型转换和删除器设置。

  4. 拷贝赋值运算符 (4): 这个赋值运算符是被删除的,意味着不能直接对 unique_ptr 进行拷贝赋值操作。这是因为 unique_ptr 是独占所有权的智能指针,不允许多个指针共享同一个资源,因此拷贝赋值在语义上是不合适的,所以被明确删除。

unique_ptr不允许多个指针共享一个资源,虽然还有使用场景,但明显场景受限制。这个时候有了更加优化的share_ptr.

4.3 share_ptr

share_ptr允许多个指针共享一个资源,采用引用计数的思想,只有当计数为一时才会析构,避免了同一块空间多次释放的问题。由于share_ptr比较完善,所以实际应用场景也会更多一些。

5、share_ptr的模拟实现

shared_ptr 的基本思想:共享同一个指针,但维护一个引用计数来追踪有多少个 shared_ptr 指向相同的资源。每次智能指针被复制时,引用计数增加;每次智能指针被销毁时,引用计数减少。如果引用计数减少到 0,智能指针会释放所持有的资源。

template <class T>
class share_ptr
{
public:
	share_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_count(new int(1))
	{

	}
	share_ptr(const share_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		_count = sp._count;

		//拷贝时引用计数++
		++(*_count);
	}
	share_ptr<T>& operator=(const share_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();//释放可能存在的资源
			_ptr = sp._ptr;
			_count = sp._count;

			//拷贝时引用计数++
			++(*_count);
		}
		return *this;
	}

	void release()
	{
		if (--(*_count) == 0)
		{
			delete _ptr;
			delete _count;
		}
	}
	~share_ptr()
	{
		release();
	}

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

	T* get() const
	{
		return _ptr;
	}
	
private:

	T* _ptr;
	int* _count;

};

int main()
{
	share_ptr<int> sp;


	return 0;
}

share_ptr还需要注意循环引用问题

6、循环引用问题

循环引用是指两个或多个对象之间相互引用,导致它们的引用计数永远无法归零,从而导致内存泄漏。这在使用智能指针时是一个常见的问题,特别是在使用 shared_ptr 时更容易出现。

#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

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;

    return 0;
}

A 拥有一个指向类 B 对象的 shared_ptr,而类 B 拥有一个指向类 A 对象的 shared_ptr。当 ab 超出作用域时,它们的引用计数永远不会归零,因为彼此都持有对方的指针,而只有指针释放时所指向的对象才会释放,但是指针是对象的成员,所以循环引用谁也不会释放,从而导致内存泄漏。

解决方案:

#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_weak_ptr;  // 将其中一个指针设计为 weak_ptr
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

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

    a->b_weak_ptr = b;  // 使用 weak_ptr

    b->a_ptr = a;

    // 在使用前检查 weak_ptr
    if (auto shared_b = a->b_weak_ptr.lock()) {
        // 如果对象仍然存在,则可以安全地使用 shared_b
        std::cout << "Object B is still alive." << std::endl;
    } else {
        std::cout << "Object B is expired or deleted." << std::endl;
    }

    return 0;
}

使用weak_ptr打破循环引用:将其中一个指针设计为 weak_ptr,这样它不会增加对象的引用计数。但是需要在使用前对 weak_ptr 进行检查,以确保对象仍然存在。

7、share_ptr中的自定义删除器

如何根据不同的对象来执行特定的清理操作。

shared_ptr 允许指定一个自定义的删除器(deleter),并且提供了对应的构造函数

#include <iostream>
#include <memory>

// 自定义资源
struct MyResource {
    MyResource() { std::cout << "资源已分配。" << std::endl; }
    ~MyResource() { std::cout << "资源已释放。" << std::endl; }
    void CustomCleanup() { std::cout << "自定义清理操作。" << std::endl; }
};

// 删除器函数对象
struct CustomDeleter {
    void operator()(MyResource* ptr) const {
        if (ptr) {
            ptr->CustomCleanup();
            delete ptr;
        }
    }
};

int main() {
    // 创建 shared_ptr,并指定删除器
    std::shared_ptr<MyResource> ptr(new MyResource(), CustomDeleter());

    // 使用 shared_ptr
    // ...

    // 当 shared_ptr 超出作用域时,将会调用删除器来释放资源
    return 0;
}

MyResource 是我们要管理的自定义资源,CustomDeleter 是我们定义的删除器函数对象。在创建 shared_ptr 时,我们将资源指针和删除器一起传递给 shared_ptr 的构造函数。shared_ptr 超出作用域时,删除器将被调用来执行特定的清理操作,例如执行自定义的清理函数 CustomCleanup() 并释放资源。

我们可以根据不同的场景使用不同的删除器

std::shared_ptr<A> sp(new A[10], [](A* p){delete[] p; });

这里创建了一个 shared_ptr,它指向一个包含 10 个 A 对象的动态数组。在这里,我们传递了一个 lambda 函数作为删除器,这个 lambda 函数负责释放数组内存,使用 delete[] 来释放动态数组的内存。

std::shared_ptr<FILE> sp(fopen("test.txt", "w"), [](FILE* p){fclose(p); });

 这里创建了一个 shared_ptr,它指向通过 fopen 打开的文件指针。同样地,我们传递了一个 lambda 函数作为删除器,这个 lambda 函数使用 fclose 来关闭文件。

这种方式允许我们在 shared_ptr 不再需要资源时执行特定的清理操作,确保资源被正确释放,避免内存泄漏或资源泄漏。

相关推荐

  1. C++ RAII

    2024-05-04 16:46:03       47 阅读
  2. C++ 智能指针

    2024-05-04 16:46:03       52 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-04 16:46:03       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-04 16:46:03       106 阅读
  3. 在Django里面运行非项目文件

    2024-05-04 16:46:03       87 阅读
  4. Python语言-面向对象

    2024-05-04 16:46:03       96 阅读

热门阅读

  1. PIXI入门系列之终章

    2024-05-04 16:46:03       35 阅读
  2. python编程功能选择建议处理方式

    2024-05-04 16:46:03       31 阅读
  3. D3CTF2024

    D3CTF2024

    2024-05-04 16:46:03      26 阅读
  4. gateway中对返回的数据进行处理

    2024-05-04 16:46:03       23 阅读
  5. jQuery学习笔记

    2024-05-04 16:46:03       27 阅读
  6. 【华为OD机试】堆内存申请【C卷|100分】

    2024-05-04 16:46:03       32 阅读
  7. P1596 [USACO10OCT] Lake Counting S 题解

    2024-05-04 16:46:03       37 阅读