【C++11】智能指针详解
本文主要探讨C++中的内存泄漏问题及其解决方案。首先分析了内存泄漏的两种类型:堆内存泄漏和系统资源泄漏,指出其对长期运行程序的危害性。随后介绍了预防内存泄漏的四种方法,重点阐述了RAII(资源获取即初始化)思想的核心价值。文章通过代码示例展示了传统new/delete方式的局限性,以及RAII封装如何解决异常情况下的资源释放问题。进一步分析了auto_ptr的设计缺陷(管理权转移)及其被废弃的原因
内存泄漏
内存泄漏是令很多C++开发者头疼的一件事,内存泄漏又称资源泄露,指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
我们长期运行的程序,如网络服务程序,多以守护进程的方式跑在服务器上,永不停歇,如果程序可以终止,那么程序终止时还会释放资源,泄漏的资源会返还操作系统,但是这种永不停歇的程序,一旦有内存泄漏,就会越来越卡,最终可能导致崩溃,事故发生,所以我们是要极力避免的。
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何避免内存泄漏呢?
(1)工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
(2)采用RAII思想或者智能指针来管理资源。
(3)有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
(4)出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
C++传统的new、delete方式具有一定的局限性,有时即使我们保持良好的编程习惯,也无法避免内存泄漏,比如:
void div()
{
int a, b;
std::cin >> a >> b;
if (b == 0) throw std::invalid_argument("Division by zero error.");
}
void func()
{
A* ptr1 = new A();
A* ptr2 = new A();
div();
delete ptr1;
delete ptr2;
}
int main()
{
try
{
func();
}
catch (std::exception& ep)
{
std::cout << ep.what() << std::endl;
}
return 0;
}
// 如果b输入0,结果为:
// A()
// A()
// 1
// 0
// Division by zero error.
如果出现异常,会直接进行栈展开,即使我们写了delete,也不会执行,所以我们应该采用一种更高明的方式防止这种情况的出现。
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
(1)不需要显式地释放资源。
(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。
可以说RAII是现代C++编程的核心思想之一。
我们采用RAII的思想对上述代码中的指针进行封装,
template<class T>
class smart_ptr
{
T* _ptr;
public:
smart_ptr(T* ptr) : _ptr(ptr) {}
~smart_ptr() { delete _ptr; }
};
class A
{
public:
A() { std::cout << "A()" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
};
void div()
{
int a, b;
std::cin >> a >> b;
if (b == 0) throw std::invalid_argument("Division by zero error.");
}
void func()
{
smart_ptr<A> ptr1(new A());
smart_ptr<A> ptr2(new A());
div();
}
int main()
{
try
{
func();
}
catch (std::exception& ep)
{
std::cout << ep.what() << std::endl;
}
return 0;
}
// 如果b输入0,结果为:
// A()
// A()
// 1
// 0
// ~A()
// ~A()
// Division by zero error.
可以看到即使出现异常进行了栈展开,也不影响指针释放,因为就算是栈展开,只要出了该函数的作用域,就要对smart_ptr类型对象进行释放,调用析构,指针的释放被写在了析构中,这样就不怕了。其实这就可以看作是智能指针的雏形了,如果我们对*和->进行重载,就可以当成一个简化版的智能指针来用了。
template<class T>
class smart_ptr
{
T* _ptr;
public:
smart_ptr(T* ptr) : _ptr(ptr) {}
~smart_ptr() { delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
};
class A
{
public:
A() { std::cout << "A()" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
void PRINT() { std::cout << _a << std::endl; }
int _a = 1;
};
void func()
{
smart_ptr<A> ptr1(new A());
smart_ptr<A> ptr2(new A());
ptr1->_a++;
(*ptr1).PRINT();
}
int main()
{
try
{
func();
}
catch (std::exception& ep)
{
std::cout << ep.what() << std::endl;
}
return 0;
}
// 打印结果为:
// A()
// A()
// 2
// ~A()
// ~A()
auto_ptr
auto_ptr是C++98就推出的类,
template <class X> class auto_ptr;
有点智能指针的雏形了,但是设计的不太好,现在基本没有人在用,在C++17已经被废弃,但是我们还是应该了解一下。
用起来其实和之前我们写的简易版的类似,
std::auto_ptr<A> ptr(new A());
ptr->_a++;
(*ptr).PRINT();
该函数设计的不好的地方在哪呢?管理权转移,什么意思,我们看以下代码,
int main()
{
std::auto_ptr<A> ptr1(new A());
std::auto_ptr<A> ptr2(ptr1);
std::auto_ptr<A> ptr3 = ptr2;
std::cout << ptr1.get() << std::endl;
std::cout << ptr2.get() << std::endl;
std::cout << ptr3.get() << std::endl;
return 0;
}
// 打印结果为:
// A()
// 0000000000000000
// 0000000000000000
// 00000237C23C8CD0
// ~A()
如果我们使用拷贝构造或赋值运算符重载,管理权就被转移,原本的auto_ptr对象就会被置为nullptr,这时我们再去访问,就会出问题。
当然这么设计也不是没有原因的,如果只是简单地赋值拷贝,多个对象管理同一个指针,析构时都去delete,那么就会出现double free的问题,我们之前实现的简易版就有这个问题。
我们仿照auto_ptr实现一份我们自己的auto_ptr。
namespace jiunian
{
template<class T>
class auto_ptr
{
T* _ptr;
public:
using Self = auto_ptr<T>;
auto_ptr(T* ptr) : _ptr(ptr) {}
~auto_ptr() { delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
auto_ptr(Self& ap) { _ptr = ap._ptr, ap._ptr = nullptr; }
Self& operator=(Self& ap) { _ptr = ap._ptr, ap._ptr = nullptr; return *this; }
T* get() { return _ptr; }
};
}
unique_ptr
unique_ptr是C++11推出的智能指针,是专门为了解决指针对应的资源难以管理的问题所推出的。
template <class T, class D = default_delete<T>> class unique_ptr;
之前的章节我们也指出auto_ptr的不足,智能指针设计上的困难,即赋值和拷贝怎么实现。unique_ptr是怎么解决这个问题的呢?解决不了问题,那就解决问题本身,你赋值和拷贝会出问题,我直接不让你赋值和拷贝就行了。这也是其名字的由来,unique_ptr——独立指针。
我们来简单使用一下,
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2(std::move(ptr1));
//std::unique_ptr<A> ptr2(ptr1); // 会报错
std::unique_ptr<A> ptr3;
ptr3 = std::move(ptr2);
//ptr3 = ptr2; // 会报错
std::cout << ptr1.get() << std::endl;
std::cout << ptr2.get() << std::endl;
std::cout << ptr3.get() << std::endl;
可以看到我们不能直接对左值unique_ptr进行拷贝和赋值,因为这两个函数直接被删除了,但是移动拷贝和移动赋值都可以,因为这不违背unique_ptr的宗旨,不允许多个unique_ptr对同一个指针进行管理,移动拷贝和移动赋值都是对将亡值和纯右值的资源进行转移,所以得以保留。
我们自己来实现一个unique_ptr。
namespace jiunian
{
template<class T>
class unique_ptr
{
T* _ptr;
public:
using Self = unique_ptr<T>;
unique_ptr() : _ptr(nullptr) {}
unique_ptr(T* ptr) : _ptr(ptr) {}
~unique_ptr() { delete _ptr; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T* get() { return _ptr; }
Self& operator=(const Self&) = delete;
Self& operator=(Self&& up) { _ptr = up._ptr, up._ptr = nullptr; return *this; }
unique_ptr(const Self& up) = delete;
unique_ptr(Self&& up) { _ptr = up._ptr, up._ptr = nullptr; }
};
}
shared_ptr
unique_ptr固然好用,但是我们不可能一直都是只用unique_ptr,如果我们确实要将智能指针复制多份,unique_ptr就不好用了。shared_ptr就是专门解决这个问题的,
template <class T> class shared_ptr;
那么这样的话shared_ptr就得解决那个问题——赋值和拷贝怎么实现?shared_ptr采用引用计数的方式,记录当前这个指针被多少个shared_ptr管理着,被赋值拷贝就加加引用计数,自己析构了就减减,减减后如果没有为0就表示当前的指针还有其他shared_ptr在管理着,这时就不会去释放对应的资源,如果为0,表示当前指针没有其他shared_ptr在管理了,就去释放对应的资源。
我们来简单使用一下,
std::shared_ptr<A> ptr1(new A());
std::shared_ptr<A> ptr2(ptr1);
std::shared_ptr<A> ptr3;
ptr3 = ptr1;
std::cout << ptr1.get() << std::endl;
std::cout << ptr2.get() << std::endl;
std::cout << ptr3.get() << std::endl;
std::cout << ptr1.use_count() << std::endl; // 打印引用计数
// 打印结果为:
// A()
// 0000022BA8DB8990
// 0000022BA8DB8990
// 0000022BA8DB8990
// 3
// ~A()
可以看到三个shared_ptr管理着同一个指针,引用计数为3,最后也只释放了一次指针。
我们来实现一个自己的shared_ptr。
namespace jiunian
{
template<class T>
class shared_ptr
{
T* _ptr;
size_t* _ref_count;
public:
using Self = shared_ptr<T>;
shared_ptr() : _ptr(nullptr), _ref_count(new size_t(0)) {}
shared_ptr(T* ptr) : _ptr(ptr), _ref_count(new size_t(1)) {}
~shared_ptr()
{
if (_ptr && --(*_ref_count) == 0)
delete _ptr, delete _ref_count;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T* get() { return _ptr; }
size_t use_count() { return *_ref_count; }
Self& operator=(const Self& sp)
{
if (sp._ptr == _ptr) return *this;
if (_ptr && --(*_ref_count) == 0)
delete _ptr, delete _ref_count;
_ptr = sp._ptr;
_ref_count = sp._ref_count;
++(*_ref_count);
return *this;
}
Self& operator=(Self&& sp)
{
if (sp._ptr == _ptr) return *this;
if (_ptr && --(*_ref_count) == 0)
delete _ptr, delete _ref_count;
_ptr = sp._ptr;
_ref_count = sp._ref_count;
sp._ptr = nullptr;
sp._ref_count = new size_t(0);
return *this;
}
shared_ptr(const Self& sp) : _ptr(sp._ptr), _ref_count(sp._ref_count) { ++(*_ref_count); }
shared_ptr(Self&& sp) : _ptr(sp._ptr), _ref_count(sp._ref_count)
{
sp._ptr = nullptr;
sp._ref_count = new size_t(0);
}
};
}
shared_ptr的实现有很多要注意的点。首先是引用计数的管理方式,因为要在多个类对象之间共享,所以肯定不能直接定义成size_t,那么这里有很多人会想到static变量,因为static是在该类类型的所有对象中共享,但是细想一下肯定不行,因为这里不是要在该类类型的所有对象中共享,我们只是想在管理了同一个指针的对象之间共享,所以我们采用指针的方式进行管理,赋值拷贝时将引用计数的指针也拷贝过去,真正要释放资源时顺便将引用计数也释放掉就行了。除此之外,赋值运算符重载中也有一个注意事项,那就是特别判断自己给自己赋值的情况,不去特地判断的话,如果此时只有自己一个对象管理该指针,那么会先将引用计数减至0,然后释放资源,之后又自己给自己赋值,成功让自己成为了管理野指针的智能指针。
weak_ptr
shared_ptr看似美好,但是有一个致命的缺点——循环引用。我们看下面这段代码,
class Node
{
public:
Node() { std::cout << "Node()" << std::endl; }
~Node() { std::cout << "~Node()" << std::endl; }
std::shared_ptr<Node> _next;
int _var;
};
int main()
{
std::shared_ptr<Node> no1(new Node());
std::shared_ptr<Node> no2(new Node());
no1->_next = no2;
no2->_next = no1;
return 0;
}
// 打印结果为:
// Node()
// Node()
可以看到,上面的智能指针并没有销毁,为什么呢?当我们创建了这两个智能指针时,对应的资源引用计数分别为1,这时将no1的_next赋值为no2,因为这也是一个智能指针,然后将no2的_next赋值为no1,这时双方引用计数为2,main函数结束,两个智能指针销毁,但是两个销毁都只能将引用计数置为1,之后两个堆上空间中的智能指针互相所指,都希望对方销毁,对方销毁了就会销毁自己资源对应的智能指针,这样自己就能释放了,但是这就像死锁,双方持有资源,都要求对方销毁自己就销毁,最终死循环,这就是循环引用。
那应该怎么办呢?这就要请出shared_ptr的小弟——weak_ptr了。
weak_ptr,顾名思义就是弱指针,主要为了解决shared_ptr循环引用推出的。
template <class T> class weak_ptr;
weak_ptr不拥有对象的所有权,弱引用一个由shared_ptr管理的对象,也就是说,weak_ptr被shared_ptr赋值拷贝后,获得了指针,可以访问,但是不会增加引用计数,且析构时不会释放对象。
weak_ptr不能由普通指针构造,只能由shared_ptr或拷贝构造而来。
我们将之前的循环引用的场景中的_next指针换用weak_ptr。
class Node
{
public:
Node() { std::cout << "Node()" << std::endl; }
~Node() { std::cout << "~Node()" << std::endl; }
std::weak_ptr<Node> _next;
int _var;
};
int main()
{
std::shared_ptr<Node> no1(new Node());
std::shared_ptr<Node> no2(new Node());
no1->_next = no2;
no2->_next = no1;
std::cout << no1.use_count() << std::endl;
std::cout << no2.use_count() << std::endl;
return 0;
}
// 打印结果为:
// Node()
// Node()
// 1
// 1
// ~Node()
// ~Node()
可以看到,引用不会增加,资源被正常释放,没有循环引用。
我们来实现一个自己的weak_ptr。
namespace jiunian
{
template<class T>
class weak_ptr
{
T* _ptr;
public:
using Self = weak_ptr<T>;
weak_ptr() : _ptr(nullptr) {}
weak_ptr(const jiunian::shared_ptr<T>& sp) : _ptr(sp.get()) {}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
weak_ptr(const Self& wp) { _ptr = wp._ptr; }
weak_ptr(Self&& wp) { _ptr = wp._ptr; wp._ptr = nullptr; }
Self& operator=(const Self& wp) { _ptr = wp._ptr; return *this; }
Self& operator=(const jiunian::shared_ptr<T>& sp) { _ptr = sp.get(); return *this; }
Self& operator=(Self&& wp) { _ptr = wp._ptr, wp._ptr = nullptr; return *this; }
T* get() { return _ptr; }
};
}
删除器
我们使用智能指针管理一些资源时,并不能正常直接释放指针,比如
std::shared_ptr<A> ptr1(new A[10]());
std::shared_ptr<A> ptr2((A*)malloc(sizeof(A) * 10));
std::shared_ptr<FILE> ptr3(fopen("test.cpp", "r"));
上面三个都会报错,因为他们都不能直接delete,这时候我们就要用到删除器了,它允许我们自定义资源的释放逻辑。
std::shared_ptr<A> ptr1(new A[10](), [](A* ptr) { delete[] ptr; });
std::shared_ptr<A> ptr2((A*)malloc(sizeof(A) * 10), [](A* ptr) { free(ptr); });
std::shared_ptr<FILE> ptr3(fopen("test.cpp", "r"), [](FILE* ptr) { fclose(ptr); });
可以传lambda,也能传仿函数,也能传函数指针,可以猜到底层应该用的包装器接收的。
对于unique_ptr,也能传删除器,不过由于两者的设计理念不同,unique_ptr追求极致效率,所以要将删除器的类型写进模板参数中,直接在编译时确定方便进行优化,删除器不同对应的类型也不同。
auto del1 = [](A* ptr) { delete[] ptr; };
std::unique_ptr<A, decltype(del1)> ptr1(new A[10](), del1);
auto del2 = [](A* ptr) { free(ptr); };
std::unique_ptr<A, decltype(del2)> ptr2((A*)malloc(sizeof(A) * 10), del2);
auto del3 = [](FILE* ptr) { fclose(ptr); };
std::unique_ptr<FILE, decltype(del3)> ptr3(fopen("test.cpp", "r"), del3);
我们在自己的shared_ptr的基础上进行改进,使其支持删除器。
namespace jiunian
{
template<class T>
class shared_ptr
{
T* _ptr;
size_t* _ref_count;
std::function<void(T* ptr)> _del = [](T* ptr) { delete ptr; };
public:
using Self = shared_ptr<T>;
shared_ptr() : _ptr(nullptr), _ref_count(new size_t(0)) {}
template<class T, class D>
shared_ptr(T* ptr, D d) : _ptr(ptr), _ref_count(new size_t(1)), _del(d) {}
~shared_ptr()
{
if (_ptr && --(*_ref_count) == 0)
_del(_ptr), delete _ref_count;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T* get() { return _ptr; }
T* get() const { return _ptr; }
size_t use_count() { return *_ref_count; }
Self& operator=(const Self& sp)
{
if (sp._ptr == _ptr) return *this;
if (_ptr && --(*_ref_count) == 0)
_del(_ptr), delete _ref_count;
_ptr = sp._ptr;
_ref_count = sp._ref_count;
++(*_ref_count);
return *this;
}
Self& operator=(Self&& sp)
{
if (sp._ptr == _ptr) return *this;
if (_ptr && --(*_ref_count) == 0)
_del(_ptr), delete _ref_count;
_ptr = sp._ptr;
_ref_count = sp._ref_count;
sp._ptr = nullptr;
sp._ref_count = new size_t(0);
return *this;
}
shared_ptr(const Self& sp) : _ptr(sp._ptr), _ref_count(sp._ref_count) { ++(*_ref_count); }
shared_ptr(Self&& sp) : _ptr(sp._ptr), _ref_count(sp._ref_count)
{
sp._ptr = nullptr;
sp._ref_count = new size_t(0);
}
};
}
更多推荐
所有评论(0)