目录

智能指针的原理 

RAII

智能指针的问题

 auto_ptr

unique_ptr

shared_ptr 

shared_ptr的线程安全问题

shared_ptr的循环引用问题

定制删除器

lock_guard/unique_lock(RAII机制托管锁)


由于C++没有GC(垃圾回收机制),所有动态开辟的空间都需要我们手动释放,这就会导致

  1. 忘记释放
  2. 异常安全问题

不管哪种情况,都会导致内存泄漏

 下面先来简单复习一下异常安全问题

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

void fun()
{
	int* p = new int;
	cout << Div() << endl;
	delete p;
}

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

此时,如果在Div中抛出异常,就会跳过fun()函数最后的 delete p; 语句,从而导致内存泄漏,这就是异常安全问题

 那么异常是怎么解决这个问题的呢?

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

void fun()
{
	int* p = new int;
	try
	{
		cout << Div() << endl;
	}
	catch (...)//若抛出异常,这里会先捕获异常,释放空间后再重新抛出
	{
		delete p;
		throw;
	}
	delete p;
}

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

使用了异常的重新抛出,先在内层的catch语句中完成空间的释放,再将该异常重新抛出交给外面的catch处理

但这样的解决方法会涌现很多问题

例如,当我们开辟了多个空间,如果DIv抛异常,就需要先将这3个空间都释放掉 

void fun()
{
	int* p1 = new int;
	int* p2 = new int;
	int* p3 = new int;
	try
	{
		cout << Div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		delete p3;
		throw;
	}
	delete p1;
	delete p2;
	delete p3;
}

但是开辟空间如果失败了也是会抛异常的,如果这里的new抛异常怎么办?并且还需要清楚是哪个变量的new抛出的异常

有读者可能会想,给每个new都放在一个单独的try块中,再给每个try写一个单独的catch,不就可以解决了吗?

首先,如果new语句在try块内,那么在try块外的delete语句就会找不到p1,p2,p3

再者,就算可以找到,那怎么判断哪个new抛的异常,从而不去delete对应对象,又是个问题

这时候,就要请出本文的主角——智能指针

智能指针的原理 

智能指针其实是一个类 

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

	~SmartPtr()
	{
		if(_ptr)
        {
			delete _ptr;
            _ptr = nullptr;
        }    
	}

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

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

private:
	T* _ptr;
};

在该类中,定义了构造、析构、*重载、->重载,而类中存的是一个指针

 现在我如果把一个原生指针存进去,就不需要我手动释放了

int* p = new int;
SmartPtr<int> sp(p);

因为类在生命周期结束后会自动调用析构函数,而现在原生指针交给智能指针管理后,当原生指针的生命周期结束时,智能指针的生命周期也会结束,此时调用该类的析构函数,就可以释放该原生指针的空间

*重载 和->重载是为了让智能指针可以更好的模拟原生指针的行为

 既然智能指针可以完全模拟原生指针的行为了,那其实我们可以这么定义

SmartPtr<int> sp(new int);//或SmartPtr<int> sp = new int;//这里调用的也是构造

有了智能指针,再发生异常安全问题时,就无需重新抛出异常了

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

void fun()
{
	SmartPtr<int> sp = new int;
	cout << Div() << endl;
}

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

此时如果Div函数内部抛出异常,那么在异常抛出后会先执行SmartPtr的析构函数,再执行catch块中的代码

即使没有抛异常,在fun函数执行完后,也会调用SmartPtr的析构函数,相当于将指针托管给了智能指针

RAII

 智能指针的这种做法,就被叫做RAII(Resource Acquisition Is Initialization),它是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

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

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

需要注意的是,RAII是一种托管资源的思想,智能指针是依靠RAII实现的,除了智能指针之外,还有例如unique_lock/lock_gurad等等也是依托RAII实现的

智能指针的问题

当我们想将一个智能指针的值赋给另一个智能指针时,问题就出现了

SmartPtr<int> sp1 = new int;
SmartPtr<int> sp2 = sp1;//调用默认拷贝构造

 此时会报错

这是因为sp1和sp2中指针的地址是同一个,那么就会导致两次释放,和浅拷贝问题很像

 

针对这个问题,C++一共给出了三种解决方案

  • C++98:auto_ptr     管理权转移  (了解)
  • C++11:unique_ptr  防拷贝  
  • C++11:shared_ptr  引用计数的共享拷贝  循环拷贝问题需要用weak_ptr解决

 auto_ptr

auto_ptr是C++98中自带的类,因此如果要实现自己的auto_ptr,需要将它放在一个命名空间中

namespace valkyrie
{
	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		auto_ptr(auto_ptr<T>& ap)//管理权转移,将ap存的地址转移给*this
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		auto_ptr& operator=(auto_ptr<T>& ap)//依旧管理权转移,不过需要先将原先this中的空间释放
		{
			if (this != &ap)
			{
				if (_ptr)
					delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}

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

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

	private:
		T* _ptr;
	};

}

 大部分代码都和之前我们实现的SmartPtr一样,唯一区别在于拷贝构造和赋值重载显式定义了

 该拷贝构造的核心在于原先ap1管理的空间,交给ap2管理,而ap1再置空

赋值重载也一样,但需要先将原先存的空间释放掉

那此时如果再想 *ap2 = 1;  就会引发空指针解引用问题,而崩溃

这也是C++早期的设计缺陷,一般都是公司明令禁止使用的

unique_ptr

 unique_ptr 是C++11新引入的智能指针,它对于拷贝构造和赋值重载的做法是直接禁止,已达到防拷贝的目的

namespace valkyrie
{
    template <class T>
	class unique_ptr
	{
	public:
		unique_ptr(unique_ptr<T>&) = delete;//删除该类的拷贝构造
		unique_ptr<T>& operator=(unique_ptr<T>&) = delete;//删除该类的赋值重载
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{
		}

		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}

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

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

	private:
		T* _ptr;
	};

}

这样在我们尝试调用拷贝构造或赋值重载时,就会报错

unique_ptr的思路简单粗暴,是比较推荐实用的,但缺陷也很明显:如果有需要拷贝的场景,它就没法使用

shared_ptr 

 shared_ptr 也是C++11引入的智能指针,它和 unique_ptr 的区别在于它可以拷贝构造和赋值

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

每个资源都对应一个计数指针,用来记录该份资源被几个对象共享

拷贝构造: 

为什么计数的变量是指针?因为只有指针才可以实现在多个对象中同步变化

 赋值重载时,需要先判断原先存储的空间需不需要释放,再执行如同拷贝构造一样的操作

此时就是直接释放的情况,下面再演示一下不需要释放的情况

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

		shared_ptr(const shared_ptr<T>& sp)//拷贝构造,将指针和计数指针都拷贝给它,并将引用计数+1,表示又多一个托管这块内存的智能指针
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		shared_ptr<T>& operator=(shared_ptr<T>& sp)//赋值重载,先判断原先该智能指针中的空间需不需要释放(等于先调用了一次析构函数),再执行和拷贝构造一样的步骤
		{
			if (this != &sp)
			{
				if (--(*_pcount) == 0 && _ptr)
				{
					delete _ptr;
					_ptr = nullptr;
					delete _pcount;
					_pcount = nullptr;
				}

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}

		~shared_ptr()
		{
				if (--(*_pcount) == 0 && _ptr)//先将引用计数-1,看看是否为0,如果为0,就代表没有智能指针在托管这块空间了,直接释放;但如果不为0,就代表还有*pcount个智能指针在托管这块空间,不能释放
				{
					delete _ptr;
					_ptr = nullptr;
					delete _pcount;
					_pcount = nullptr;
				}
		}

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

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

	private:
		T* _ptr;
		int* _pcount;//引用计数,用来统计目前有几个智能指针在同时托管这块空间
	};

}

shared_ptr的线程安全问题

当有多个线程用 shared_ptr 管理着同一份共享资源时,由于引用计数需要++(析构时也需要--),因此就会有多个线程同时对一个引用计数++(--)而导致的数据不一致问题。

C++11的shared_ptr是线程安全的,下面简单实现一下通过将引用计数包装成 atomic 原子类型而实现的线程安全版本:

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

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

        shared_ptr<T>& operator=(shared_ptr sp)
        {
            //现代写法,并且保证了线程安全
            if(this != &sp)
            {
                swap(_ptr,sp._ptr);
                swap(_count,sp._count);
            }
            return *this;
        }

        ~shared_ptr()
        {
            if(_count && --(*_count) == 0)
            {
                delete _ptr;
                delete _count;
            }
        }

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

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

    private:
        T* _ptr;
        atomic<int>* _count;//将引用计数包装成原子类型
    };
}

shared_ptr的循环引用问题

当shared_ptr托管的对象需要双向依赖关系,例如双向链表(next节点和prev节点),或树的父子节点,就会因为引用计数无法归零而导致内存泄漏

例如下面这种情况:

template <class T>
struct ListNode
{
    T val;
    val::shared_ptr<ListNode> prev;
    val::shared_ptr<ListNode> next;
};

int main()
{
    val::shared_ptr<ListNode<int>> ln1(new ListNode<int>);
    val::shared_ptr<ListNode<int>> ln2(new ListNode<int>);

    cout << ln1.getcnt()/*获取当前智能指针的引用计数*/ << endl << ln2.getcnt() << endl;

    ln1->next = ln2;
    ln2->prev = ln1;

    cout << ln1.getcnt() << endl << ln2.getcnt() << endl;

    return 0;
}

在这段代码里,ln1内托管的对象由ln1和ln2->prev同时托管,ln2内托管的对象由ln2和ln1->next同时托管,那么ln1和ln2的引用计数都为2

当程序结束,ln1和ln2调用析构函数后,各自都把引用计数减到1各自都以为还有别的shared_ptr在托管该资源(next和prev),所以都没释放,这就造成了内存泄漏

要想解决,就需要把双向依赖关系里的指针不再引用计数++,因此C++11还有一个 weak_ptr ,它不是智能指针,而是专门用于解决shared_ptr循环引用问题的指针,将shread_ptr赋给weak_ptr时,引用计数不会++,而只是像个普通指针一样

template <class T>
class weak_ptr
{
public:
    weak_ptr() = default; // 默认构造函数
    weak_ptr(const shared_ptr<T> &sp)
        : _ptr(sp.operator->())
    {
    }

    weak_ptr<T> &operator=(shared_ptr<T> sp)//参数是shared_ptr
    {
        _ptr = sp.operator->(); // 获取sp._ptr
        return *this;
    }

    ~weak_ptr()
    {
        _ptr = nullptr;//不释放,只置空
    }

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

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

private:
    T *_ptr;
};

只要将双向依赖关系中的资源交给weak_ptr管理,就解决了循环引用问题

template <class T>
struct ListNode
{
    T val;
    val::weak_ptr<ListNode> prev;
    val::weak_ptr<ListNode> next;
};

int main()
{
    val::shared_ptr<ListNode<int>> ln1(new ListNode<int>);
    val::shared_ptr<ListNode<int>> ln2(new ListNode<int>);

    cout << ln1.getcnt()/*获取当前智能指针的引用计数*/ << endl << ln2.getcnt() << endl;

    ln1->next = ln2;
    ln2->prev = ln1;

    cout << ln1.getcnt() << endl << ln2.getcnt() << endl;

    return 0;
}

定制删除器

在C++11之前,也就是C++98后,智能指针只有auto_ptr,这之间的十几年由于官方一直没有作为,一个名为 boost 的第三方库就出现了,它给出了 scoped_ptr shared_ptr weak_ptr ,并且除了智能指针也有非常多的实现,后续C++11的 unique_ptr shared_ptr weak_ptr 就是参考 boost 库实现的

boost库中除了有scoped_ptr和shared_ptr,还有 scoped_array shared_array 。由于智能指针默认的释放资源的方式都是delete,如果是以new []创建的数组,就不能用delete了,因此scoped_array/shared_array是boost中专门给new []准备的智能指针,它们的释放资源的方式是delete[]

C++11没有引入xxx_array的智能指针,它靠定制删除器来实现(也是借鉴的boost库)

如果直接将new []的数据交给智能指针管理,在析构时会报错

class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int a;
    int b;
};

int main()
{
    shared_ptr<A>(new A[10]);

    return 0;
}

此时可以定义一个仿函数。让智能指针在释放资源时自动调用仿函数内的 operator() 

template<class T>
struct deleteArr
{
    void operator()(T* v)
    {
        delete[] v;
    }
};

int main()
{
    shared_ptr<A>(new A[10],deleteArr<A>());

    return 0;
}

这就叫做定制删除器

除了new,new[]之外,malloc/fopen等等需要回收的类型都可以使用定制删除器交给智能指针管理

class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int a;
    int b;
};

template<class T>
struct deleteArr
{
    void operator()(T* v)
    {
        delete[] v;
    }
};

template<class T>
struct deleteFree
{
    void operator()(T* v)
    {
        free(v);
    }
};

struct deleteFclose
{
    void operator()(FILE* v)
    {
        fclose(v);
    }
};

int main()
{
    shared_ptr<A>(new A[10],deleteArr<A>());
    shared_ptr<A>((A*)malloc(sizeof(A)),deleteFree<A>());
    shared_ptr<FILE>(fopen("tmp.txt","w"),deleteFclose());
    return 0;
}

需要注意的是,上面只是为了演示定制删除器的用法,若真需要用智能指针管理数组,应该用unique_ptr,它有operator[]重载

lock_guard/unique_lock(RAII机制托管锁)

平时有加锁解锁的操作时,如果中间的某个函数抛出异常,就会出现死锁问题,需要通过捕获重新抛出的方式对其解锁

mtx.lock();
try
{
    f();
}
catch(...)
{
    mtx.unlock();//捕获重新抛出
    throw;
}
mtx.unlock();

C++11中,专门为锁的RAII写了两个对象, lock_guard unique_lock ,它们可以在出生命周期时自动释放锁

lcok_guard就是纯粹的RAII的管理锁,构造时加锁,析构时解锁,该对象只有构造和析构两个接口

template<class lock>
class Lock_Guard
{
public:
    Lock_Guard(lock& lk)//引用传参,这样传过去的锁也是同一个
    :_lk(lk)
    {
        //_lk = lk;
        _lk.lock();
    }
    ~Lock_Guard()
    {
        _lk.unlock();
    }
    Lock_Guard(const Lock_Guard<lock>&) = delete;//lock_guard不允许拷贝
    Lock_Guard<lock>& operator=(Lock_Guard<lock>) = delete;
private:
    lock& _lk;//引用类型
};

mutex mtx;

void fun()
{
    Lock_Guard<mutex> lg(mtx);
    throw(string("抛出异常"));
}

需要注意的是lock_guard是不支持拷贝

 unique_lock 除了可以像lock_guard一样RAII,还可以自己加锁或解锁

void fun()
{
    unique_lock<mutex> lg(mtx);
    throw(string("抛出异常"));
    lg.unlock();
    //...其他工作
    lg.lock();
    //...
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐