在编写C++程序时,我们最常遇到的问题也就是内存方面的问题了。申请内存后未释放,打开文件后未关闭,这些都属于内存泄漏的问题。 举个栗子: 如果我们在申请内存之后,程序抛了一个异常,并且我们的catch代码段也没有去做对应的处理,那么这个时候就会发生内存泄漏。如下程序:
int Division(int a, int b) { if (b == 0) { throw "Division by zero condition"; } return a / b; } int main() { try { int *str = new int[1000]; //申请内存 int a = 10; int b = 0; cout << Division(a, b) << endl; //抛异常 delete[] str; //释放内存语句未执行 } catch (const char* e) { cout << e << endl; //没有对内存进行释放,导致内存泄漏 } return 0; }上面这段代码可以看到在 try 代码段中我们申请了一块空间,但是当我们运行到Division 函数中时,它判断除数是 0 ,抛出异常,那么程序接下来会跳过后面的 delete[ ] str;而且我们也没有在 catch 代码块中进行释放内存的操作,因此会造成内存泄漏。
RAII 是一种利用对象生命周期来控制内存资源的一种技术,因为在面向对象的编程语言中,对象的创建和销毁分别是通过构造函数和析构函数来完成的,而且这两个函数都不用程序猿人为的控制,都是由系统自动调用的。 我们可以将资源的申请放在构造函数中,将资源的释放放在析构函数中,这样即便程序异常退出,在对象生命结束的时候,系统也会自动调用析构函数去执行释放资源的语句。这样大大减少了内存泄漏一系列问题的产生。 RAII带来的好处: 1、省去了我们人为释放资源的过程。 2、资源在对象生命周期内始终有效。
原理:RAII 的思想 + 实现指针的特征(重载 *、->的操作)
std::auto_ptr: auto_ptr 是 C++98版本就提供的一款智能指针。 auto_ptr 的问题:管理权转移,当对象发生拷贝或者赋值之后,前面对象就会悬空,导致前面的智能指针不能再用。 模拟实现: 下面的所有代码中的 A 类如下:
class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } int _num; };auto_ptr 模拟实现:
template <class T> class AutoPtr { public: AutoPtr(T* ptr = nullptr) :_ptr(ptr) {} ~AutoPtr() { if (_ptr) { delete _ptr; } } //拷贝 AutoPtr(AutoPtr& ap) { _ptr = ap._ptr; ap._ptr = nullptr; //资源转移,管理权移交,ap的管理权失效 } //赋值 AutoPtr<T>& operator=(AutoPtr<T>& ap) { if (this != &ap) { //释放当前对象的资源 if (_ptr) { delete _ptr; } //将新来的资源移交给自己 _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };演示拷贝/赋值管理权转移: std::unique_ptr: 为了解决这个管理权转移的问题,在 C++11 中引入了一个新的智能指针 unique_ptr,这个智能指针的做法非常粗暴,那么就是直接不让拷贝和赋值。 所以它的问题就是不能拷贝和赋值。 模拟实现:
template <class T> class UniquePtr { public: UniquePtr() :_ptr(nullptr) {} ~UniquePtr() { if(_ptr) delete _ptr; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: UniquePtr(const UniquePtr<T>&) = delete; UniquePtr<T>& operator=(const UniquePtr<T>&) = delete; private: T* _ptr; };不能赋值和拷贝肯定是不合理的,所以C++11又有了shared_ptr来解决这些问题。 std::shared_ptr: 原理:通过引用计数来解决不能拷贝和赋值的问题。 1、shared_ptr在其内部给每一份资源都维护了一个引用计数,通过引用计数来统计当前资源被几个对象共享。 2、在对象被销毁时,这份资源并不是直接释放,而是该资源的引用计数 -1 。直到引用计数减为 0,证明现在资源没有对象在使用,资源才会被释放。
问题:循环引用
模拟实现:(在模拟实现的过程中要注意,因为引用计数是被多个对象所共享的,所以在对引用计数进行 ++操作或者 --操作时要注意线程安全问题,要通过加锁来实现线程安全)
template <class T> class SharedPtr { public: SharedPtr(T* ptr = nullptr) :_ptr(ptr) ,_mutex(new mutex) ,_Count(new int(1)) {} ~SharedPtr() { Release(); } SharedPtr(const SharedPtr<T>& sp) :_ptr(sp._ptr) , _mutex(sp._mutex) , _Count(sp._Count) { AddRefCount(); } SharedPtr<T>& operator=(const SharedPtr<T>& sp) { if (_ptr != sp._ptr) { Release(); _ptr = sp._ptr; _mutex = sp._mutex; _Count = sp._Count; AddRefCount(); } return *this; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } int UseCount() { return *_Count; } void AddRefCount() { _mutex->lock(); ++(*_Count); _mutex->unlock(); } private: void Release() { bool flag = false; //计数为0标志位 _mutex->lock(); if (--(*_Count) == 0) { delete _ptr; delete _Count; flag = true; } _mutex->unlock(); if (flag == true) delete _mutex; } private: T* _ptr; //指向管理资源的指针 mutex* _mutex; //互斥锁 int* _Count; //引用计数 };上面提到shared_ptr有一个问题:循环引用。 我们看看下面这种情况:
struct ListNode { int data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> node2(new ListNode); cout << node1.use_count() << endl; cout << node2.use_count() << endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count() << endl; cout << node2.use_count() << endl; return 0; }运行结果如图: 看起来好像没什么问题,在没有指向新的资源时,node1 和 node2 的引用计数都是 1. 指向新的资源后,引用计数变为 2。但是按照逻辑,程序执行完应该调用析构函数释放对象资源才对,这里并没有调用析构函数,这就是循环引用问题,这个问题导致了令人谈虎色变的内存泄漏问题。
我们将相互指向的两行代码注释掉,看看运行结果: 这样程序是走的正常的逻辑,当有对象释放资源时,引用计数 -1,引用计数减为 0 时,最终调用析构函数,资源彻底释放。 所以应该如何解决循环引用的问题呢? 主要有以下两种办法: ① 当要发生循环引用的时候,手动打破循环引用释放对象资源 ② 使用弱引用指针 weak_ptr 来打破循环引用
第一种方法较为麻烦,所以第二种方法是最常用的方法。 将我们刚才的代码进行修改,将ListNode中的强引用智能指针改为弱引用智能指针。也就是说在有可能发生循环引用的地方都是用弱引用智能指针。
struct ListNode { int data; weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } };修改之后循环引用的问题也就迎刃而解了。
boost::scoped_ptr: scoped_ptr 为了解决管理权转移问题也是非常简单粗暴直接防拷贝防赋值。
在boost库中有很多优秀的东西,比如 C++11 标准库中的uniqued_ptr、weak_ptr、shared_ptr都是参照boost库中的实验原理实现的。 在 boost 库中有一个 scoped_ptr ,在C++11标准库中的 uniqued_ptr 就是对应 boost 库中的scoped_ptr。
