【C++之智能指针知识】

C++之智能指针知识

前言:
前面篇章学习了C++对C++11的知识认识和应用,接下来继续学习,C++智能指针的知识以及相关内容。
/知识点汇总/

1、C++11智能指针知识介绍

C++11智能指针是C++11标准中引入的一种重要特性,用于自动管理动态分配的内存,从而避免内存泄漏等问题。

1.1、智能指针的基本概念

智能指针是一个类模板,它封装了普通指针,并在对象生命周期结束时自动释放所管理的资源。智能指针通过RAII(Resource Acquisition Is Initialization,资源获取即初始化)技术实现,即在构造函数中分配资源,在析构函数中释放资源。

1.2、C++11中的智能指针类型

C++11标准中提供了三种主要的智能指针类型:std::unique_ptr、std::shared_ptr和std::weak_ptr。

1.2.1、std::unique_ptr

特点:

std::unique_ptr提供对对象的独占所有权,即同一时间内只能有一个std::unique_ptr指向给定对象。它禁止拷贝操作,但支持移动操作,确保资源的唯一性和安全性。

用途:

适用于管理具有唯一所有权的资源,如动态分配的内存、文件句柄等。

std::unique_ptr<int> ptr = std::make_unique<int>(10);
1.2.2、std::shared_ptr

特点:

std::shared_ptr允许多个智能指针共享同一个对象,内部通过引用计数机制来管理对象的生命周期。当最后一个std::shared_ptr被销毁或重置时,对象才会被删除。

用途:

适用于需要共享资源的场景,如多个对象需要访问同一个数据结构时。

std::shared_ptr<int> ptr1 = std::make_shared<int>(10);  
std::shared_ptr<int> ptr2 = ptr1;
1.2.3、std::weak_ptr

特点:

std::weak_ptr是一种不拥有对象所有权的智能指针,它用于解决std::shared_ptr之间的循环引用问题。它不会增加对象的引用计数,但可以通过lock()方法尝试获取对象的std::shared_ptr。

用途:

主要用于观察std::shared_ptr管理的对象,或解决循环引用导致的内存泄漏问题。

std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);  
std::weak_ptr<int> weakPtr = sharedPtr;  
if (auto lockedPtr = weakPtr.lock()) {  
    // 使用lockedPtr  
}

2、智能指针的优缺点

2.1、优点

自动管理内存,减少内存泄漏的风险。
提高代码的安全性和可维护性。
简化资源管理逻辑,使代码更加简洁。

2.2、缺点

相比原始指针,智能指针在性能上可能略有下降(主要是因为额外的引用计数和管理开销)。
使用不当可能导致循环引用等问题。

3、auto_ptr的简单模拟实现

auto_ptr 是 C++98 中引入的一种智能指针,但在 C++11 中被明确标记为已废弃(deprecated),并在后续版本中逐渐被淘汰。它主要用于自动管理动态分配的内存,但存在一些严重的问题和限制,因此在新代码中不推荐使用。

3.1、auto_ptr的简单模拟实现

在学习中,对某一个类进行简单的模拟,一般情况下,我们只需要模拟实现四个函数,分别是:构造函数、析构函数、拷贝构造函数以及赋值运算符重载。但是对于一个指针来说,它还要加上*运算符重载和->运算符重载

	template<class T>
	class Auto_ptr
	{
	public:

		//构造函数
		Auto_ptr(T* _ptr=NULL)
		{
			ptr = _ptr;
		}

		//析构函数
		~Auto_ptr()
		{
			if (ptr != NULL)
			{
				delete ptr;
			}
		}

		//拷贝构造函数
		Auto_ptr(const Auto_ptr<T>& ap)
		{
			ptr = ap.ptr;
			ap.ptr = NULL;
		}

		//赋值运算符重载
		Auto_ptr<T>& operator=(Auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (ptr != NULL)
					delete ptr;

				ptr = ap.ptr;
				ap.ptr = NULL;

			}

			return *this;
		}

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

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

	private:
		T * ptr;
	};

3.2、对auto_ptr的缺陷的分析

从赋值运算符重载函数和拷贝构造函数来看,他们所使用的的是一个浅拷贝,所以如果auto_ptr对象之间出现了赋值和拷贝的情况,那么就会出现,多个指针指向了同一个空间的情况,在对象声明周期结束的时候,会出现同一块空间被多次释放的情况,所以为了改变这种情况,auto_ptr使用的是一个管理权转移的思想,所以在发生拷贝构造或者是赋值时,将原来的auto_ptr置为空,这样就解决了二次释放的问题。但是这样也有一个缺陷,那就是在用原来的auto_ptr访问数据时,会出现内存错误,因为此时的auto_ptr是一个NULL,用一个NULL去访问数据显然是C++不允许的。

1. 所有权转移问题:
如上所述,auto_ptr 的复制和赋值操作会转移所有权,这可能导致原始对象在不知情的情况下失去对资源的控制。
2.不支持数组:
auto_ptr 无法正确管理动态分配的数组,因为它只能调用 delete 而不是 delete[]。
3.作为容器成员的问题:
由于 auto_ptr 的转移语义,它不能安全地用作标准容器的成员,因为容器的复制和赋值操作会破坏 auto_ptr 的所有权规则。
4.赋值操作的限制:
auto_ptr 不能通过赋值操作来初始化,这限制了它的使用灵活性。

3.3、对赋值运算符重载函数的一些说明

(1)赋值运算符重载
对于一个赋值运算符重载,我们有以下四点要注意的地方(注意这不仅仅是对于以上代码而言,对于所有的赋值运算符重载都适用):
1.检查返回值类型是否是该类类型的引用,并且返回值也是自身引用(即*this)。

因为只有返回一个引用类型,那么才能做到连续赋值(即s1=s2=s3),否则如果函数返回的不是一个引用,那么在进行连续赋值的时候,编译器就会报错。

2.检查函数的形参是否是const引用类型。

因为如果传入的参数不是引用,而是一个实例的话,那么在实参对形参结合的这个过程中会调用拷贝构造函数。而如果是一个引用就会避免这种无端的销毁,从而提高效率。之所以声明为const,是因为在赋值运算符中,我们可以保证不会对对象进行修改。

3.检查在赋值之前是否释放掉自身已有内存。

如果我们忘记在赋值之前释放掉自身已有内存,那么就会出现内存泄漏。

4.检查是否判断自我赋值

因为在赋值运算符重载函数中,我们首先要对自身对象进行释放,然后才开始赋值。如果没有判断自我赋值,那么当是同一个对象进行赋值时,释放掉自身对象,其实也是将传入参数的内存也释放掉了,因此就找不到要赋值的内容了。

4、unique_ptr 的简单模拟实现

std::unique_ptr 是 C++11 引入的一种智能指针,用于自动管理动态分配的内存,确保资源被适时释放,防止内存泄漏。与 std::auto_ptr 相比,std::unique_ptr 拥有更好的性能和更强的语义保证,特别是它不允许拷贝(但支持移动),从而避免了所有权混淆的问题。

4.1、unique_ptr 的简单模拟实现

#include <iostream> 
template<class T>
class unique_ptr {
public:
    // 构造函数  
    explicit unique_ptr(T* ptr = nullptr) 
        : _ptr(ptr) {}

    // 解引用运算符  
    T& operator*()
    {
        return *_ptr;
    }

    // 成员访问运算符  
    T* operator->()
    {
        return _ptr;
    }

    // 析构函数  
    ~unique_ptr()
    {
        delete _ptr;
    }

    // 禁止拷贝  
    unique_ptr(const unique_ptr<T>&) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

    // 移动构造函数  
    unique_ptr(unique_ptr<T>&& other) noexcept
        : _ptr(other._ptr)
    {
        other._ptr = nullptr;
    }

    // 移动赋值运算符  
    unique_ptr<T>& operator=(unique_ptr<T>&& other) noexcept
    {
        if (this != &other)
        {
            delete _ptr;
            _ptr = other._ptr;
            other._ptr = nullptr;
        }
        return *this;
    }

private:
    T* _ptr;
};
int main()
{
    // 测试 unique_ptr 构造和解引用  
    unique_ptr<int> ptr1(new int(10));
    std::cout << "*ptr1 = " << *ptr1 << std::endl; // 输出: *ptr1 = 10  

    // 测试通过指针访问成员  
    unique_ptr<std::string> ptr2(new std::string("Hello, World!"));
    std::cout << ptr2->c_str() << std::endl; // 输出: Hello, World! 

    // 测试移动构造函数  
    unique_ptr<int> ptr3(new int(10));
    unique_ptr<int> ptr4 = std::move(ptr3); // 移动构造  
    // 此时 ptr1 不再拥有资源,尝试访问 *ptr1 是未定义行为  

    // 测试移动赋值运算符  
    unique_ptr<std::string> ptr5(new std::string("Hello"));
    unique_ptr<std::string> ptr6;
    ptr6 = std::move(ptr5); // 移动赋值  
    std::cout << ptr6->c_str() << std::endl; // 输出: Hello  

    // 注意:ptr1 和 ptr3 在这里已经被移动,它们现在是空的(_ptr 为 nullptr)  
    return 0;
}

4.2、对unique_ptr的特性和优势的分析

1. 独占所有权:

unique_ptr 确保了其对所管理资源的独占所有权。这意味着一个资源(如动态分配的内存)只能由一个 unique_ptr 实例拥有。当 unique_ptr 被销毁或重新赋值时,它所管理的资源也会被自动释放。

2. 禁止拷贝:

unique_ptr 不允许拷贝构造函数和拷贝赋值运算符,以防止所有权被意外共享。这避免了 auto_ptr 的所有权转移问题。但是,它提供了移动构造函数和移动赋值运算符,以支持安全地转移所有权。

3. 支持数组:

与 auto_ptr 不同,unique_ptr 有专门的版本用于管理动态分配的数组(即std::unique_ptr<T[]>),它会在销毁时调用 delete[] 而不是 delete。

4. 兼容性:

unique_ptr 可以安全地用作标准容器的元素,只要容器支持移动语义。

5. 可定制删除器:

unique_ptr 允许用户指定一个自定义的删除器,这提供了额外的灵活性,比如管理非内存资源(如文件句柄)时。

4.4、赋值运算符重载函数对于unique_ptr的说明

unique_ptr 的赋值运算符:由于 unique_ptr 禁止拷贝,它的赋值实际上是移动赋值。这意味着它会检查是否正在进行自我赋值,然后释放当前拥有的资源(如果有的话),并将控制权转移到新的资源上。

5、shared_ptr 的简单模拟实现

5.1、shared_ptr 的简单模拟实现

#include<iostream>
#include <atomic>

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

	// sp2(sp1)
	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 (this != &sp)
		if (_ptr != sp._ptr)
		{
			this->release();

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

			++(*_pcount);
		}

		return *this;
	}
	

	void release()
	{
		if (--(*_pcount) == 0)
		{
			// 最后一个管理的对象,释放资源
			delete _ptr;
			delete _pcount;
		}
	}

	~shared_ptr()
	{
		release();
	}

	int use_count()
	{
		return *_pcount;
	}

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

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	std::atomic<int>* _pcount;//引用计数 ,atomic原子的,保证线程安全性 
};



int main() {
    {
        // 创建第一个 shared_ptr 实例  
        shared_ptr<int> sp1(new int(10));

        std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // 应该输出 1  

        // 创建第二个 shared_ptr 实例,通过拷贝构造函数  
        shared_ptr<int> sp2(sp1);

        std::cout << "sp1 use_count after copying to sp2: " << sp1.use_count() << std::endl; // 应该输出 2  
        std::cout << "sp2 use_count: " << sp2.use_count() << std::endl; // 应该输出 2  

        // 赋值操作  
        shared_ptr<int> sp3(new int(3));
        sp3 = sp1;
	
        std::cout << "sp1 use_count after assigning to sp3: " << sp1.use_count() << std::endl; // 应该输出 3  
        std::cout << "sp3 use_count: " << sp3.use_count() << std::endl; // 应该输出 3  

        // 释放资源  
    } // 在这里,sp1, sp2, sp3 的析构函数将被调用,且只有最后一个析构时会删除 int 和引用计数  

    // 尝试访问已经销毁的对象(这里不会直接访问,但理论上不应该发生任何操作)  
    // 注意:在实际使用中,不要这样做,因为它会导致未定义行为  

    return 0;
}

5.2、对shared_ptr 的特性和优势的分析

1. 共享所有权:

shared_ptr 允许多个 shared_ptr 实例共享对同一资源的所有权。每个 shared_ptr 实例维护一个引用计数,当最后一个拥有该资源的 shared_ptr 被销毁或重新赋值时,资源才会被释放。

2. 拷贝和赋值安全:

与 auto_ptr 不同,shared_ptr 的拷贝构造函数和拷贝赋值运算符是安全的,因为它们会递增引用计数,而不是转移所有权。

3. 线程安全:

shared_ptr 的引用计数通常是线程安全的(取决于实现),这允许它在多线程环境中安全使用。但是,对 shared_ptr 所指向的对象本身的访问仍然需要用户自己进行同步。

4. 弱引用(weak_ptr):

shared_ptr 允许配合 weak_ptr 使用,以创建对共享资源的非拥有性引用。weak_ptr 不增加引用计数,因此不会导致资源被释放时延迟。这有助于解决循环引用问题。

5. 自定义删除器:

类似于 unique_ptr,shared_ptr 也允许用户指定一个自定义的删除器,以处理非内存资源。

5.4、赋值运算符重载函数对于shared_ptr 的说明

对于 unique_ptr 和 shared_ptr,由于它们都有特殊的语义(unique_ptr 的独占性和 shared_ptr
的共享性),它们的赋值运算符重载函数都是自动生成的,并且遵循各自的设计原则。

shared_ptr 的赋值运算符:shared_ptr 的赋值运算符会递增新资源的引用计数,并递减旧资源的引用计数(如果有的话)。如果旧资源的引用计数变为零,则释放该资源。这确保了即使在赋值过程中,资源的生命周期也被正确管理。

6、注意事项

(1)、在使用智能指针时,应注意避免循环引用,特别是在使用std::shared_ptr时。
(2)、std::unique_ptr不支持拷贝操作,只支持移动操作,因此在使用时需要特别注意。
(3)、可以通过std::make_unique和std::make_shared等工厂函数来创建智能指针,这些函数通常比(4)、直接使用构造函数更加安全和高效。

综上所述,C++11智能指针是现代C++编程中管理动态内存的重要工具,它们通过自动管理资源生命周期来提高代码的安全性和可维护性。然而,在使用时也需要注意避免一些常见问题,如循环引用等。

附:参考文章原文链接:C++ auto_ptr的简单模拟实现

相关推荐

  1. C++智能指针知识

    2024-07-21 16:24:04       16 阅读
  2. C++面试线程池、智能指针、设计模式

    2024-07-21 16:24:04       47 阅读
  3. C++11 智能指针shared_from_this

    2024-07-21 16:24:04       16 阅读

最近更新

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

    2024-07-21 16:24:04       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-21 16:24:04       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-21 16:24:04       45 阅读
  4. Python语言-面向对象

    2024-07-21 16:24:04       55 阅读

热门阅读

  1. C++分组背包问题_动态规划dp_背包_算法竞赛

    2024-07-21 16:24:04       17 阅读
  2. Qt编程技巧总结篇(5)-信号-槽-多线程(四)

    2024-07-21 16:24:04       18 阅读
  3. cannot import name ‘OrderedDict‘ from ‘typing‘

    2024-07-21 16:24:04       15 阅读
  4. GFS分布式文件系统

    2024-07-21 16:24:04       15 阅读
  5. 牛客暑假训练2 C.Red Walking on Grid

    2024-07-21 16:24:04       18 阅读
  6. Python之后端Django(六)

    2024-07-21 16:24:04       13 阅读
  7. blender和3dmax和maya和c4d比较

    2024-07-21 16:24:04       17 阅读
  8. 数据结构第33节 在不同领域的应用

    2024-07-21 16:24:04       13 阅读
  9. 【软考】UML中的关联关系

    2024-07-21 16:24:04       20 阅读
  10. firefly rk3288 ubuntu23.10 网卡名为end0 改为eth0

    2024-07-21 16:24:04       15 阅读
  11. C++狼人杀游戏(真的能运行!!!)

    2024-07-21 16:24:04       15 阅读