在 C++ 编程中,内存管理始终是一个关键问题,尤其是在手动管理动态分配内存时,容易引发内存泄漏、悬空指针等问题。为了解决这些问题,C++11 引入了智能指针,包括 unique_ptr, shared_ptr, 和 weak_ptr,它们极大地简化了资源管理,并在大多数情况下防止了内存泄漏。


一、unique_ptr:独享所有权的智能指针

1.1 概念

unique_ptr 是一种独占所有权的智能指针,这意味着在任意时间点,只有一个 unique_ptr 可以指向某个动态分配的对象。unique_ptr 在其生命周期结束时会自动释放它所管理的资源,确保不会发生内存泄漏。

1.2 特点

  • 独占所有权unique_ptr 不允许复制,仅允许移动(即右值引用)。
  • 自动释放资源:当 unique_ptr 离开其作用域时,它会自动释放指向的资源,不需要显式调用 delete
  • 高效且安全:由于 unique_ptr 不允许多个指针共享同一个对象,它消除了引用计数的开销,同时避免了悬空指针和重复释放的问题。

1.3 用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <memory>
using namespace std;

class AA {
public:
    string m_name;
    AA(const string& name) : m_name(name) {
        cout << "构造函数调用:" << m_name << endl;
    }
    ~AA() {
        cout << "析构函数调用:" << m_name << endl;
    }
};

int main() {
    // 通过 make_unique 创建 unique_ptr
    unique_ptr<AA> ptr = make_unique<AA>("西施");

    // 通过 move 将所有权转移
    unique_ptr<AA> ptr2 = move(ptr);  // ptr 失去所有权,ptr2 拥有对象

    if (ptr == nullptr)
        cout << "ptr 是空指针" << endl;

    cout << "结束程序时,ptr2 会自动释放资源" << endl;
    return 0;
}

1.4 技巧

  • std::move 转移所有权unique_ptr 不支持复制,但可以通过 std::move 进行所有权的转移。
  • release()reset()release() 用于释放 unique_ptr 对对象的控制权,并返回裸指针;reset() 用于重新管理一个新对象或将 unique_ptr 置为空。
  • 数组支持unique_ptr 支持数组管理,使用 unique_ptr<T[]> 可以安全地管理动态数组,并在离开作用域时自动释放。

二、shared_ptr:共享所有权的智能指针

2.1 概念

shared_ptr 提供了一种共享所有权的模型,多个 shared_ptr 可以同时指向同一个对象。shared_ptr 通过内部的引用计数来跟踪有多少指针共享这个对象,当最后一个 shared_ptr 超出作用域时,对象才会被释放。

2.2 特点

  • 共享所有权:多个 shared_ptr 可以同时指向同一个对象,引用计数会相应增加或减少。
  • 引用计数机制:当引用计数变为零时,对象会被自动销毁。
  • 线程安全shared_ptr 的引用计数操作是原子操作,在多线程环境下是安全的。

2.3 用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <memory>
using namespace std;

class AA {
public:
    string m_name;
    AA(const string& name) : m_name(name) {
        cout << "构造函数调用:" << m_name << endl;
    }
    ~AA() {
        cout << "析构函数调用:" << m_name << endl;
    }
};

int main() {
    shared_ptr<AA> ptr1 = make_shared<AA>("西施");
    shared_ptr<AA> ptr2 = ptr1;  // 引用计数加1

    cout << "ptr1 引用计数:" << ptr1.use_count() << endl;  // 输出 2
    cout << "ptr2 引用计数:" << ptr2.use_count() << endl;  // 输出 2

    ptr1.reset();  // 释放 ptr1 对对象的引用,但对象不会被销毁

    cout << "ptr1 引用计数:" << ptr1.use_count() << endl;  // 输出 0
    cout << "ptr2 引用计数:" << ptr2.use_count() << endl;  // 输出 1
}

2.4 技巧

  • use_count():可以用 use_count() 查看某个对象的引用计数。
  • reset()swap()reset() 可以用于更改对象的所有权,swap() 则用于交换两个 shared_ptr 的管理对象。
  • 线程安全shared_ptr 本身的引用计数是线程安全的,但如果多个线程同时读写 shared_ptr 所指向的对象,则需要额外的同步机制。

三、weak_ptr:非所有权的智能指针

3.1 概念

weak_ptr 是专门为了解决 shared_ptr 中的循环引用问题引入的,它是一种弱引用,不会影响对象的引用计数。weak_ptr 仅仅是观察一个对象的存在,不能直接访问对象,需要通过 lock() 转换为 shared_ptr 才能使用。

3.2 循环引用问题

循环引用发生在两个 shared_ptr 互相持有对方的情况,导致引用计数永远无法归零,从而造成内存泄漏。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <memory>
using namespace std;

class BB;

class AA {
public:
    weak_ptr<BB> m_p;  // 使用 weak_ptr 打破循环引用
    string m_name;
    AA(const string& name) : m_name(name) {
        cout << "构造函数调用:" << m_name << endl;
    }
    ~AA() {
        cout << "析构函数调用:" << m_name << endl;
    }
};

class BB {
public:
    weak_ptr<AA> m_p;
    string m_name;
    BB(const string& name) : m_name(name) {
        cout << "构造函数调用:" << m_name << endl;
    }
    ~BB() {
        cout << "析构函数调用:" << m_name << endl;
    }
};

int main() {
    shared_ptr<AA> ptrA = make_shared<AA>("西施A");
    shared_ptr<BB> ptrB = make_shared<BB>("西施B");

    ptrA->m_p = ptrB;
    ptrB->m_p = ptrA;

    cout << "无循环引用,程序正常结束" << endl;
}

3.3 weak_ptr 的使用

  • lock()weak_ptr 提供 lock() 函数,将 weak_ptr 转换为 shared_ptr,如果对象仍然存在则返回有效的 shared_ptr,否则返回空指针。
  • expired()weak_ptr 可以用 expired() 检查资源是否已经过期。
  • 非所有权weak_ptr 不会影响引用计数,可以安全地用来观察资源的生命周期。

四、总结

智能指针是 C++ 现代编程中处理资源管理的关键工具,能够有效防止内存泄漏并简化复杂的内存管理任务。unique_ptr 提供了最高效的独占所有权管理,而 shared_ptr 则用于需要共享所有权的场景,weak_ptr 则解决了 shared_ptr 的循环引用问题。