【C++】智能指针模拟实现及详解

目录

什么是智能指针:

为什么要有智能指针:

auto_ptr:

unique_ptr:

shared_ptr:

shared_ptr的缺陷:

weak_ptr:


什么是智能指针:

概念:

        智能指针是一种特殊的类模板,用于自动管理具有动态分配生命周期的对象。它们通过模拟指针的行为来工作,但提供了自动的内存管理功能,从而减少了内存泄漏的风险。

        使用智能指针可以确保当它们所指向的对象超出作用域或被显式删除时,所指向的对象也会被自动删除。

通俗一点就是:智能指针就是一个类,使用RAII(Resource Acquisition Is Initialization)机制对普通指针进行一层封装,并重载*、-> 符号,让其可以像指针一样去使用,也就是让其用起来像个指针,本质是是一个对象,这样就可以方便的去管理一个对象的生命周期,减少内存泄漏的风险。

RAII:资源获取即初始化

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:1、不需要显示的释放资源,因为出了作用域会自动销毁。2、可以保证对象所需的资源在其生命周期内始终保持有效。

为什么要有智能指针:

在上面是什么智能指针的讲述中提到过一点:智能指针最重要的功能是提供自动的内存管理功能,减少内存泄露的风险。

这里先讲讲内存泄漏:

内存泄漏:

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

内存泄漏的危害:

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

内存泄漏分类:

我们一般关心以下两种方面的内存泄漏:

堆内存泄漏(Heap leak):

        堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统内存泄漏:

        指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

那么我们把眼光放在堆内存泄漏上,来看一下下面这段代码有没有什么问题:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	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;
}

这段程序中可以看出:如果发生除0错误抛异常的话会直接跳到try catch中,那么Func函数中创建出来的两个指针就不会被delete掉,从而引发内存泄漏。

那么如何解决呢? 这就可以使用智能指针去解决了,使用智能指针因为这是一个对象,在对象被创建时初始化好里面的指针,出了作用域会自动销毁去调用对象的析构函数,再在析构函数中释放掉指针即可:简易的智能指针模板代码形式如下:

template<class T>
class smart_ptr
{
public:

	// 创建时初始化
	smart_ptr(T* ptr) :_ptr(ptr)
	{}

	~smart_ptr()
	{
		// 不需要显示的调用,因为对象除了作用域会自动调用析构函数
		if (_ptr)
			delete _ptr;
	}

	// 提供调用接口: 重载* ->
	T& operator*()
	{
		return *_ptr;
	}

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


private:
	T* _ptr = nullptr;
};

理解完智能指针的概念就来看看以下的几种智能指针:

auto_ptr:

auto_ptr的实现原理:就是直接转移管理权,但是现在不推荐使用auto_ptr,先来看一下auto_ptr的简化版:

#include<memory>
template<class T>
class auto_Ptr
{
public:
        auto_Ptr(T* ptr):_ptr(ptr)
        {}

        auto_Ptr(auto_Ptr<T>& sp)
        {
                // 进行置空,交接sp的管理权
                _ptr = sp._ptr;
                sp._ptr = nullptr;
        }

        auto_Ptr<T>& operator=(auto_Ptr<T>& sp)
        {
                // 检测是否自赋值:
                if (this != &sp)
                {
                        if (_ptr)
                                delete _ptr;

                        _ptr = sp._ptr;
                        sp._ptr = NULL;
                }

                return *this;
        }


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

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

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


private:
        T* _ptr;
};


int main()
{
        auto_Ptr<int> p1(new int[10]);
        auto_Ptr<int> p2(p1);


        auto_Ptr<int> p3(new int[5]);
        auto_Ptr<int> p4 = p3;


        return 0;
}

通过上面的代码我们可以看出auto_ptr就只是简单的转移资源,为什么不推荐使用的原因也很简单,就是假如我把p1的资源转移了,但是我忘记了,或者我并不知道,然后又对p1进行操作,那么此时就会发生崩溃。所以很多公司都禁止使用auto_ptr,而是使用unique_ptr、shared_ptr。

unique_ptr:

C++11中开始提供更靠谱的unique_ptr,实现的原理就是:简单粗暴的防拷贝,下面是简化模拟实现的unique_ptr:

namespace yue
{
    template<class T>
    class unique_ptr
    {
    public:
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        // 像指针一样使用
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
        
        // 获取_ptr的地址
        T* Get()
        {
            return _ptr;
        }

        unique_ptr(const unique_ptr<T>& sp) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
    private:
        T* _ptr;
    };
}

unique_ptr直接将拷贝构造函数delete掉了,解决了因为拷贝带来的置空问题(“雀食” 是从根源上解决了问题)所以相对于auto_ptr来讲:

auto_ptr只是简单的转移管理权,被拷贝对象悬空,有风险,不建议使用。

unique_ptr不支持拷贝,没有风险,建议使用unique_ptr。

但是在某些情况下我确实是需要两个指针来管理同一块资源怎么办呢?我就是想让他能拷贝怎么办呢?诶!所以C++11就提供了更靠谱的并且支持拷贝的shared_ptr。

shared_ptr:

C++11提供了更靠谱的并且支持拷贝的shared_ptr,那么来思考一个问题,既然我这两个指针都是指向同一块资源,那么在两个指针都delete的时候,是不是就会引发崩溃呢?(因为同一块内存不能被释放两次)

既然shared_ptr支持拷贝,那么它就得确保即使多个指针指向同一块资源,在析构的时候也只会将内存释放一次:

可以看到程序是正常结束的,并没有出现崩溃,也就是并没有出现同一块资源被释放多次的行为,那是怎么做到的呢?

这里就要引入一个引用计数的概念了,如果有接触学过Linux的话应该很容易想起来,Linux建立硬链接的思路就是利用引用计数,如果不知道也没关系,请看如下:

也就是说在智能指针对象中再加一个参数,(假设为x),初始为1,当这个智能指针每被拷贝一次,这个x就++一下,被释放调用析构函数的时候这个值就--一下,如果这个值减到0了,就说明没有指针指向这块内存了,就可以直接delete掉这块内存了。

那么这个引用计数如何设计呢?

首先,可定不可以设计成(int x = 1;)这样子,因为多个智能指针管理同一个对象我们是只需要一个引用计数的,如果每个对象都有一个计数那肯定是不可以的,所以到这里,你是不是就立马想到设置成静态的、全局的(static),但是很遗憾,这样设置也不可以,为什么也不可以呢?

如果我们要使用static的话,那么大致模板是这样的:

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

	smart_Ptr(const smart_Ptr<T>& sp):_ptr(sp._ptr){
		i++;
	}

	smart_Ptr<T>& operator=(const smart_Ptr<T>& ptr){
		// 不能自己给自己赋值
		if (_ptr != ptr._ptr){
			_ptr = ptr._ptr;
			++i;
		}

		return *this;
	}

	~smart_Ptr() {
		if (--i == 0)
		{
			delete _ptr;
			cout << "~smart_Ptr()" << endl;
		}
	}

	int& Get()
	{
		return i;
	}

private:
	T* _ptr = nullptr;   
	static int i;
};

但是看这个使用案例好像并没有什么错啊,p1p2指向一个,引用计数是2,p3指向一个,引用计数是1。

看起来没问题是正常的,因为这是我埋的坑,因为我使用了不同的类型去实例化模板,一个int,一个double,所以当然不会出错啦,static全局唯一就意味着如果是相同类型的话就算几个指针指向不同的对象,这个引用计数也还是会叠加,如下:

那既然使用不了static那么还有没有其他办法呢?当然有,我们可以直接new一个int*的指针,指向智能指针的引用计数,然后其他的就都一样,这样即使是相同类型,引用计数也是各自独立的。但是要注意,因为这个int*也是new出来的,所以在最后一个delete时要记得一并delete掉。

shared_ptr简化实现:

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

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

    // sp1 = sp2
    // 赋值还要考虑一块资源是否只有一个指针管控
    shared_Ptr<T>& operator=(const shared_Ptr<T>& sp)
    {
        // 防止自赋值
        // 例如s1 = s1的情况,如果s1是管控资源的最后一个指针,按照逻辑上来是先析构的,会成随机值,引起错误
        if (_ptr != sp._ptr) 
        {
            this->release(); // 查询管控数量(如果sp1是管理原本那一块资源的最后一个智能指针,得要先进行释放,才能进行sp1 = sp2的操作)

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

            ++(*_pcount);
        }

        return *this;
    }

    // 把析构要进行的操作分离成一个子函数,以便后面其他函数调用(因为函数调用内部一般不直接显式调用析构)
    void release()
    {
        // 最后一个管理的对象,释放资源
        if (--(*_pcount) == 0)
        {
            cout << "delete: " << _ptr << endl;
            delete _ptr;
            delete _pcount;
        }
    }

    ~shared_Ptr()
    {
        release();
    }

    // 查询引用计数
    int use_count()
    {
        return *_pcount;
    }

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

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


private:
    T* _ptr;

    // 多线程环境下对同一shared_ptr执行写操作不是线程安全的,所以要设置成原子性的。
    atomic<int>* _pcount; // 引用计数
};

shared_ptr的缺陷:

得要注意一个循环引用的问题,来看下面这一段代码:

可以看到,并没有调用Node的析构函数(没有任何输出,正常应该输出~Node()),这就是因为循环引用造成了内存泄漏。

就算只是其中一个有指向都是可以正常释放的,如果都指向了就会出问题,就会触发循环引用,造成内存泄漏,其实就是当相互进行指向后,p1、p2的引用计数都为2,当主函数结束时,后定义的先析构,p2先析构,引用计数减到1,此时由p1的next管着p2,然后p1析构,引用计数也进行--,此时p1的引用计数也减到1,此时由p2的prev管理着p1,但是相互都不满足引用计数为0的条件,释放逻辑是一个死循环,无法释放这段内存,所以造成了内存泄漏。

那怎么解决呢?

这个地方是一个大坑,所以1为写代码的时候注意规范,尽量不要这么去进行操作相互指向。

2就是使用官方给出的weak_ptr,弱指针。

weak_ptr:

weak_ptr不支持直接管理智能指针,也就是不支持RAII,不单独管理资源,是用来辅助解决shared_ptr的循环引用问题。

如果你知道某个地方可能会构成循环引用,你就改成使用weak_ptr:

可以看到使用weak_ptr后就可以调用Node的析构函数,本质就是通过不增加引用计数来解决的,就算我知道p1的next指向p2,p2的prev指向p1,但是我不增加p1p2的引用计数,也就是:

赋值或拷贝时,只指向资源,但不增加shared_ptr的引用计数。

weak_ptr返回的引用计数就是shared_ptr的引用计数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值