C++ 不像 Java 那样有虚拟机动态的管理内存,如果使用裸指针,在程序运行过程中可能就会出现内存泄漏等问题,然而这种问题其实都可以通过 C++11 引入的智能指针来解决。
裸指针的内存泄漏问题
前面提到使用裸指针会存在内存泄漏等问题。这里用具体代码来说明:
1class A {};
2void func() {
3 auto p = new A; // 定义 p 为指向 A 对象的裸指针
4 if (...) {
5 throw exception(SomeExceptionReason);
6 }
7 delete p;
8}
如果 func() 运行过程中抛出异常或其他原因中途退出,则 new 出来的内存得不到释放,随着函数退出,p 的生存周期结束,之后这片内存就永远无法被访问,导致内存泄漏。
智能指针
智能指针设计出来就是为了解放程序员,使其无需担心是否释放内存的问题。其本质上是对裸指针进行封装,利用 RAII 思想在构造函数中 new,在析构函数中 delete,保证了内存不泄露。
根据不同特性,智能指针分为以下三种。
std::unique_ptr
unique_ptr 是一个独占型的智能指针,它不允许其它 unique_ptr 共享其管理的内部指针,故不允许拷贝构造与拷贝赋值,只能通过移动语义来获得资源。
unique_ptr 有两个版本:
管理单个对象(例如以
new分配);1template< 2 class T, 3 class Deleter = std::default_delete<T> 4> class unique_ptr;管理动态分配的对象数组(例如以
new[]分配);1template < 2 class T, 3 class Deleter 4> class unique_ptr<T[], Deleter>;
其中 Deleter 指定删除器类型,即析构函数中调用的对内存进行释放的可调用对象。一旦某个 unique_ptr 对象以移动方式将资源转移给另一个 unique_ptr,或是在生命周期结束后被系统回收时,其关联的删除器会释放内部指针的所有权(即赋值为 nullptr)。
unique_ptr的删除器要求必须是能且仅能够传入T*类型参数的左值可调用对象,而不能是 lambda 表达式:1template<typename T> 2struct deleter { 3 void operator(T* p) { delete p; } 4}; 5 6deleter<int> d; 7std::unique_ptr<int, deleter<int>> ptr(new int, d);
用法如下:
1struct A {
2 ~A() { std::cout << "~A\n"; }
3 void Print() { std::cout << "Print\n"; }
4};
5const int size = 2;
6// 两种版本
7auto ptr = std::unique_ptr<A>(new A);
8auto ptrs = std::unique_ptr<A[]>(new A[size]);
9
10// 移动语义
11auto new_ptr = std::unique_ptr<A>(std::move(ptr)); // 此时 ptr 失去了关联对象指针的所有权
12if (ptr) { // operator bool 检查是否有关联的管理对象
13 std::cout << "ptr is empty";
14}
15std::cout << "\n";
16
17// 成员函数,最常用的就是 get()
18auto raw = new_ptr.get(); // get() 返回 A 类型的指针
19raw->Print();
20
21// 管理单个对象的 unique_ptr
22new_ptr->Print(); // 可以通过 operator-> 解引用,看起来就像一个裸指针一样
23(*new_ptr).Print(); // 可以通过 operator* 解引用
24
25// 管理对象数组的 unique_ptr 可以通过 operator[] 以下标访问对象(而非指针)
26ptrs[1].Print();
27
28/*
29 * output:
30 * ptr is empty
31 * Print
32 * Print
33 * Print
34 * Print
35 * ~A
36 * ~A
37 * ~A
38 */
std::shared_ptr
与 unique_ptr 相反,shared_ptr 是一个共享型的智能指针,它允许其它 shared_ptr 共享其管理的内部指针,故实现了拷贝构造与拷贝赋值,并通过这两种方式进行资源共享。
既然允许共享,那就存在多个 shared_ptr 使用同一片内存,故不能直接在析构时释放内存。在典型的实现中,std::shared_ptr 只保有两个指针:数据指针 _M_ptr 以及计数器指针 _M_refcount。
计数器包含:
- 指向被管理对象的指针或被管理对象本身;
- 删除器;
- 分配器;
- 被管理对象的 shared_ptr 的数量(引用计数);
- 被管理对象的 weak_ptr 的数量;
每当以拷贝的形式新建了一个 shared_ptr,则指向同一片内存的所有 shared_ptr 的引用计数都会加一,每次析构的时候引用计数减一,在最后一个 shared_ptr 析构的时候(也就是引用计数归零时),对应的内存才会释放。
同为智能指针家族,shared_ptr 也实现了以下功能:
get():获取内部关联的对象指针。operator*/operator->:解引用关联的指针;operator bool:检查是否有关联的管理对象;
除此之外,其还对外公开了一个 use_count() 函数,用于返回引用计数值。
shared_ptr 还可以自定义删除器,在引用计数为零的时候自动调用删除器来释放对象的内存,这里删除器只需要是传入 T* 参数的可调用对象即可:
1std::shared_ptr<int> ptr(new int, [](int *p){ delete p; });
shared_ptr 实现上较 unique_ptr 更为宽松,但代价为存在一些安全隐患,需要注意的是:
不用同一个裸指针初始化多个
shared_ptr,也不要对get()返回的裸指针进行delete,否则会出现 double free 导致出问题;不将
this指针初始化shared_ptr并返回,否则会出现 double free,比如:1class A { 2 shared_ptr<A> func() { 3 return std::shared_ptr<A>(this); 4 } 5};A 本身会调用析构函数,函数返回值由于关联了
this指针,进行delete this时还会调用一遍析构函数。如果非要实现这一功能,请继承自std::enable_shared_from_this<A>,之后就可以调用shared_from_this()来获取一个指向自身的 shared ptr 了。尽量用
make_shared代替new,比如:1class A { 2 A(int i) { std::cout << i; } 3}; 4std::shared_ptr<A> p(new int(1)); 5std::shared_ptr<A> q = std::make_shared<A>(1); // 等价于上面那种,but bettermake_shared()为我们提供了一个新的创建共享指针的方法,其函数原型为:1template< class T, class... Args > 2shared_ptr<T> make_shared( Args&&... args );以
args为T的构造函数参数列表,构造T类型对象并将它包装于shared_ptr。等价于用表达式
new T(std::forward<Args>(args)...)构造,其中pv是内部指向适合保有T类型对象的存储的void*指针。避免循环引用。所谓循环引用,就是存在一个引用通过一系列的引用链,最后引用回自身,且看代码:
1struct A; 2struct B; 3 4struct A { 5 std::shared_ptr<B> other; 6}; 7 8struct B { 9 std::shared_ptr<A> other; 10}; 11 12int main() { 13 auto aptr = std::make_shared<A>(); // aptr.count = 1 14 auto bptr = std::make_shared<B>(); // bptr.count = 1 15 aptr->other = bptr; // 由于 copy,bptr.count++ 16 bptr->other = aptr; // 由于 copy,aptr.count++ 17 return 0; 18}这里 aptr 指向了 A 类对象,随后 aptr.other 又与另一个指向 B 类对象的共享指针 bptr 通过拷贝的方式共享了指针,并且 bptr.other 也通过拷贝的方式与 aptr 共享了指针。这样一来 aptr 的 other 指向的对象 bptr,其 other 又指回了自身,从而存在循环引用,并且此时两者的引用计数值均为 2。
当程序结束时,aptr, bptr 调用析构函数,引用计数值减一,但此时两块内存的引用计数值仍然大于零,永远得不到释放。糟糕透了!
std::weak_ptr
为了解决共享指针可能存在的循环引用问题,weak_ptr 被提出。
weak_ptr 用来表达临时所有权的概念:它不管理指针,也就没有 get() 函数,只能通过 weak_ptr / shared_ptr 进行构造。其拷贝与析构都不会影响引用计数,纯粹是作为一个旁观者监视 shared_ptr 中管理的资源是否存在,该功能可以通过成员函数 expire() 实现——检查被引用的对象是否已删除。
当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用
weak_ptr来监视该对象。
当真正需要通过 weak_ptr 去调用那片内存时,需创建一个新的临时 shared_ptr 来共享被管理对象的所有权,此时如果原来的 shared_ptr 被销毁,则该对象的生命周期将被延长至这个临时的 shared_ptr 同样被销毁为止。
具体用法为:
1int* a = new int{0};
2std::shared_ptr<int> shared_p(a);
3std::weak_ptr<int> weak_p = shared_p; // weak_ptr 不共享所有权,仅作监视用
4std::cout << shared_p.use_count() << " " << weak_p.use_count() << std::endl;
5
6auto q = weak_p.lock(); // lock() 创建新的 std::shared_ptr 对象
7std::cout << shared_p.use_count() << " " << weak_p.use_count() << " " << q.use_count();
8
9// output:
10// 1 1
11// 2 2 2
在这样的基础上,weak_ptr 也就能够打破 shared_ptr 中所存在的循环引用现象——令循环中的其中一个指针为 weak_ptr 即可。
1struct A;
2struct B;
3
4struct A {
5 std::shared_ptr<B> other;
6};
7
8struct B {
9 std::weak_ptr<A> other;
10};
11
12int main() {
13 auto aptr = std::make_shared<A>(); // aptr.count = 1
14 auto bptr = std::make_shared<B>(); // bptr.count = 1
15 aptr->other = bptr;
16 bptr->other = aptr;
17 // 不存在 shared_ptr 之间的拷贝,故引用计数值不会发生变化。
18 return 0;
19}
如何手撕一个简单的 shared ptr
首先思考的是:shared ptr 需要支持哪些特性?
- 类模板,支持所有类型及其构造函数参数;
- 线程安全的计数器;
- 拷贝/赋值/移动构造函数;
- 支持用派生类构造;
- 正确释放指针;
1
2template <class T>
3class SharedPointer {
4 public:
5 class Counter {
6 public:
7 Counter(T* ptr) : ptr_(ptr), cnt_(0) {}
8 ~Counter() { delete ptr_; }
9 void addRef() { cnt_.fetch_add(1); }
10 void release() { cnt_.fetch_sub(1); }
11 int getCount() { return cnt_.load(); }
12 T* ptr_;
13
14 private:
15 std::atomic<int> cnt_;
16 };
17
18 public:
19 SharedPointer(T* ptr) { counter_ = new Counter(ptr); }
20 // copy constructor
21 SharedPointer(const SharedPointer<T>& sp) {
22 counter_ = sp.counter_;
23 counter_->addRef();
24 }
25 SharedPointer& operator=(const SharedPointer<T>& sp) {
26 counter_ = sp.counter_;
27 counter_->addRef();
28 }
29
30 // move constructor
31 SharedPointer(SharedPointer<T>&& sp) {
32 counter_ = sp.counter_;
33 sp.counter_ = nullptr;
34 }
35 SharedPointer& operator=(SharedPointer<T>&& sp) {
36 counter_ = sp.counter_;
37 sp.counter_ = nullptr;
38 }
39
40 // derived constructor
41 template <class U>
42 SharedPointer(U* derive) {
43 assert(std::is_base_of<T, U>::value);
44 counter_ = new Counter(derive);
45 }
46
47 ~SharedPointer() {
48 counter_->release();
49 if (counter_->getCount() == 0) {
50 delete counter_;
51 }
52 }
53
54 T* get() { return counter_->ptr_; }
55
56 bool isNull() { return get() == nullptr; }
57
58 private:
59 Counter* counter_;
60};