零基础C++(23) 智能指针

1. 引言

C++ 引入智能指针的主要目的是为了自动化内存管理,减少手动 newdelete 带来的复杂性和错误。智能指针通过 RAII(资源获取即初始化)机制,在对象生命周期结束时自动释放资源,从而有效防止内存泄漏和资源管理错误。

2. 原生指针 vs 智能指针

原生指针

原生指针是 C++ 最基本的指针类型,允许程序员直接管理内存。然而,原生指针存在以下问题:

  • 内存泄漏:未释放动态分配的内存。
  • 悬挂指针:指针指向已释放或未初始化的内存。
  • 双重释放:多次释放同一内存区域。

智能指针的优势

智能指针通过封装原生指针,自动管理内存,解决上述问题。主要优势包括:

  • 自动销毁:在智能指针生命周期结束时自动释放资源。
  • 引用计数:共享智能指针能够跟踪引用数量,确保资源在最后一个引用结束时释放。
  • 避免内存泄漏:通过 RAII 机制自动管理资源生命周期。
  • 类型安全:提供更严格的类型检查,减少错误。

3. std::unique_ptr

3.1 定义与用法

std::unique_ptr 是一种独占所有权的智能指针,任何时刻只能有一个 unique_ptr 实例拥有对某个对象的所有权。不能被拷贝,只能被移动。

主要特性

  • 独占所有权:确保资源在一个所有者下。
  • 轻量级:没有引用计数,开销小。
  • 自动释放:在指针销毁时自动释放资源。

3.2 构造函数与赋值

unique_ptr 提供多种构造函数和赋值操作,以支持不同的使用场景。

  • 默认构造函数:创建一个空的 unique_ptr
  • 指针构造函数:接受一个裸指针,拥有其所有权。
  • 移动构造函数:将一个 unique_ptr 的所有权转移到另一个 unique_ptr
  • 移动赋值操作符:将一个 unique_ptr 的所有权转移到另一个 unique_ptr

3.3 移动语义

由于 unique_ptr 不能被拷贝,必须通过移动语义转移所有权。这保证了资源的独占性。

3.4 代码案例

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
40
41
42
#include <iostream>
#include <memory>

class Test {
public:
Test(int val) : value(val) {
std::cout << "Test Constructor: " << value << std::endl;
}
~Test() {
std::cout << "Test Destructor: " << value << std::endl;
}
void show() const {
std::cout << "Value: " << value << std::endl;
}

private:
int value;
};

int main() {
// 创建一个 unique_ptr
std::unique_ptr<Test> ptr1(new Test(100));
ptr1->show();

// 使用 make_unique(C++14 引入)
auto ptr2 = std::make_unique<Test>(200);
ptr2->show();

// 移动 unique_ptr
std::unique_ptr<Test> ptr3 = std::move(ptr1);
if (!ptr1) {
std::cout << "ptr1 is now nullptr after move." << std::endl;
}
ptr3->show();

// 重置 unique_ptr
ptr2.reset(new Test(300));
ptr2->show();

// unique_ptr 自动释放资源
return 0;
}

输出

1
2
3
4
5
6
7
8
9
10
11
Test Constructor: 100
Value: 100
Test Constructor: 200
Value: 200
ptr1 is now nullptr after move.
Value: 100
Test Destructor: 100
Test Constructor: 300
Value: 300
Test Destructor: 300
Test Destructor: 200

解析

  • ptr1 拥有 Test(100)ptr2 拥有 Test(200)
  • 通过 std::moveptr1 的所有权转移到 ptr3ptr1 变为空。
  • ptr2.reset(new Test(300)) 释放了原有的 Test(200),并拥有新的 Test(300)
  • 程序结束时,ptr3ptr2 自动释放各自拥有的资源。

4. std::shared_ptr

4.1 定义与用法

std::shared_ptr 是一种共享所有权的智能指针,允许多个 shared_ptr 实例共享对同一个对象的所有权。通过引用计数机制,管理资源的生命周期。

主要特性

  • 共享所有权:多个 shared_ptr 可以指向同一个对象。
  • 引用计数:跟踪有多少 shared_ptr 实例指向同一对象。
  • 自动释放:当引用计数为0时,自动释放资源。

4.2 引用计数与控制块

shared_ptr 背后依赖一个控制块(Control Block),用于存储引用计数和指向实际对象的指针。控制块的主要内容包括:

  • 强引用计数(use_count:表示有多少个 shared_ptr 指向对象。
  • 弱引用计数(weak_count:表示有多少个 weak_ptr 指向对象(不增加强引用计数)。

4.3 构造函数与赋值

shared_ptr 提供多种构造函数和赋值操作,以支持不同的使用场景。

  • 默认构造函数:创建一个空的 shared_ptr
  • 指针构造函数:接受一个裸指针,拥有其所有权。
  • 拷贝构造函数:增加引用计数,共享对象所有权。
  • 移动构造函数:转移所有权,源 shared_ptr 变为空。
  • 拷贝赋值操作符:释放当前资源,增加引用计数,指向新对象。
  • 移动赋值操作符:释放当前资源,转移所有权,源 shared_ptr 变为空。

4.4 代码案例

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
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <memory>

class Test {
public:
Test(int val) : value(val) {
std::cout << "Test Constructor: " << value << std::endl;
}
~Test() {
std::cout << "Test Destructor: " << value << std::endl;
}
void show() const {
std::cout << "Value: " << value << std::endl;
}

private:
int value;
};

int main() {
// 创建一个 shared_ptr
std::shared_ptr<Test> sp1(new Test(100));
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
sp1->show();

// 通过拷贝构造共享所有权
std::shared_ptr<Test> sp2 = sp1;
std::cout << "After sp2 = sp1:" << std::endl;
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
std::cout << "sp2 use_count: " << sp2.use_count() << std::endl;

// 通过拷贝赋值共享所有权
std::shared_ptr<Test> sp3;
sp3 = sp2;
std::cout << "After sp3 = sp2:" << std::endl;
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
std::cout << "sp2 use_count: " << sp2.use_count() << std::endl;
std::cout << "sp3 use_count: " << sp3.use_count() << std::endl;

// 重置 shared_ptr
sp2.reset(new Test(200));
std::cout << "After sp2.reset(new Test(200)):" << std::endl;
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
std::cout << "sp2 use_count: " << sp2.use_count() << std::endl;
std::cout << "sp3 use_count: " << sp3.use_count() << std::endl;
sp2->show();

// 自动释放资源
std::cout << "Exiting main..." << std::endl;
return 0;
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Test Constructor: 100
sp1 use_count: 1
Value: 100
After sp2 = sp1:
sp1 use_count: 2
sp2 use_count: 2
After sp3 = sp2:
sp1 use_count: 3
sp2 use_count: 3
sp3 use_count: 3
Test Constructor: 200
After sp2.reset(new Test(200)):
sp1 use_count: 2
sp2 use_count: 1
sp3 use_count: 2
Value: 200
Exiting main...
Test Destructor: 200
Test Destructor: 100

解析

  • 创建 sp1,引用计数为1。

  • 拷贝构造 sp2,引用计数增加到2。

  • 拷贝赋值 sp3,引用计数增加到3。

  • ```
    sp2.reset(new Test(200))

    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83



    - 原 `Test(100)` 的引用计数减少到2。
    - 分配新的 `Test(200)`,`sp2` 拥有它,引用计数为1。

    - 程序结束时:

    - `sp1` 和 `sp3` 释放 `Test(100)`,引用计数降到0,资源被销毁。
    - `sp2` 释放 `Test(200)`,引用计数为0,资源被销毁。



    ## 5. `std::weak_ptr`



    ### 5.1 定义与用法



    `std::weak_ptr` 是一种不拥有对象所有权的智能指针,用于观察但不影响对象的生命周期。主要用于解决 `shared_ptr` 之间的循环引用问题。



    **主要特性**:



    - **非拥有所有权**:不增加引用计数。
    - **可从 `shared_ptr` 生成**:通过 `std::weak_ptr` 可以访问 `shared_ptr` 管理的对象。
    - **避免循环引用**:适用于双向关联或观察者模式。



    ### 5.2 避免循环引用



    在存在双向关联(如父子关系)时,使用多个 `shared_ptr` 可能导致循环引用,导致内存泄漏。此时,可以使用 `weak_ptr` 来打破循环。



    ### 5.3 代码案例



    #### 场景:双向关联导致循环引用



    ```cpp
    #include <iostream>
    #include <memory>

    class B; // 前向声明

    class A {
    public:
    std::shared_ptr<B> ptrB;

    A() { std::cout << "A Constructor" << std::endl; }
    ~A() { std::cout << "A Destructor" << std::endl; }
    };

    class B {
    public:
    std::shared_ptr<A> ptrA;

    B() { std::cout << "B Constructor" << std::endl; }
    ~B() { std::cout << "B Destructor" << std::endl; }
    };

    int main() {
    {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->ptrB = b;
    b->ptrA = a;
    }
    std::cout << "Exiting main..." << std::endl;
    return 0;
    }

输出

1
2
3
4
5
A Constructor
B Constructor
Exiting main...
A Destructor
B Destructor

问题

虽然 ab 离开作用域,但 A DestructorB Destructor 并未被调用,因为 ab 相互引用,引用计数无法降到0,导致内存泄漏。

解决方案:使用 weak_ptr

改用 weak_ptr 其中一方(如 BptrA),打破循环引用。

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
#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> ptrB;

A() { std::cout << "A Constructor" << std::endl; }
~A() { std::cout << "A Destructor" << std::endl; }
};

class B {
public:
std::weak_ptr<A> ptrA; // 使用 weak_ptr

B() { std::cout << "B Constructor" << std::endl; }
~B() { std::cout << "B Destructor" << std::endl; }
};

int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b;
b->ptrA = a;
}
std::cout << "Exiting main..." << std::endl;
return 0;
}

输出

1
2
3
4
5
A Constructor
B Constructor
A Destructor
B Destructor
Exiting main...

解析

  • B 使用 weak_ptr 指向 A,不增加引用计数。
  • ab 离开作用域,引用计数降为0,资源被正确释放。
  • 防止了循环引用,避免了内存泄漏。

5.4 访问 weak_ptr 指向的对象

weak_ptr 不能直接访问对象,需要通过 lock() 方法转换为 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
#include <iostream>
#include <memory>

int main() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;

if (auto locked = wp.lock()) { // 尝试获取 shared_ptr
std::cout << "Value: " << *locked << std::endl;
} else {
std::cout << "Object no longer exists." << std::endl;
}

sp.reset(); // 释放资源

if (auto locked = wp.lock()) { // 再次尝试获取 shared_ptr
std::cout << "Value: " << *locked << std::endl;
} else {
std::cout << "Object no longer exists." << std::endl;
}

return 0;
}

输出

1
2
Value: 42
Object no longer exists.

解析

  • wp.lock() 返回一个 shared_ptr,如果对象依然存在,则有效。
  • sp.reset() 释放资源后,wp.lock() 无法获取有效的 shared_ptr

6. 自定义删除器

6.1 用例与实现

有时,默认的 delete 操作不适用于所有资源管理场景。此时,可以使用自定义删除器来指定资源释放的方式。例如,管理文件句柄、网络资源或自定义清理逻辑。

6.2 代码案例

用例:管理 FILE* 资源

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
#include <iostream>
#include <memory>
#include <cstdio>

struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
std::cout << "Closing file." << std::endl;
fclose(fp);
}
}
};

int main() {
{
std::shared_ptr<FILE> filePtr(fopen("example.txt", "w"), FileDeleter());
if (filePtr) {
std::cout << "File opened successfully." << std::endl;
// 使用 filePtr 进行文件操作
fprintf(filePtr.get(), "Hello, World!\n");
}
} // 自动关闭文件

std::cout << "Exiting main..." << std::endl;
return 0;
}

输出

1
2
3
File opened successfully.
Closing file.
Exiting main...

解析

  • 自定义删除器 FileDeleter 用于在 shared_ptr 被销毁时关闭文件。
  • 使用 filePtr.get() 获取原生 FILE* 指针进行文件操作。
  • 离开作用域时,自动调用 FileDeleter 关闭文件。

6.3 使用 Lambda 表达式作为删除器

C++11 允许使用 lambda 表达式作为删除器,简化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>
#include <cstdio>

int main() {
{
auto fileDeleter = [](FILE* fp) {
if (fp) {
std::cout << "Closing file via lambda." << std::endl;
fclose(fp);
}
};

std::unique_ptr<FILE, decltype(fileDeleter)> filePtr(fopen("example.txt", "w"), fileDeleter);
if (filePtr) {
std::cout << "File opened successfully." << std::endl;
fprintf(filePtr.get(), "Hello, Lambda!\n");
}
} // 自动关闭文件

std::cout << "Exiting main..." << std::endl;
return 0;
}

输出

1
2
3
File opened successfully.
Closing file via lambda.
Exiting main...

解析

  • 使用 std::unique_ptr 搭配 lambda 删除器管理 FILE*
  • 提供了更灵活和简洁的删除器实现。

7. 最佳实践与常见陷阱

7.1 选择合适的智能指针

  • **std::unique_ptr**:
    • 用于明确的独占所有权场景。
    • 适用于资源的单一管理者或需要所有权转移的情况。
    • 更轻量,性能更优。
  • **std::shared_ptr**:
    • 用于共享所有权的场景。
    • 需要多个指针共同管理同一资源时使用。
    • 引用计数带来一定的性能开销。
  • **std::weak_ptr**:
    • 用于观察不拥有资源的场景。
    • 适用于需要避免循环引用或只需临时访问资源的情况。

7.2 避免循环引用

在使用 shared_ptr 时,特别是在对象间存在双向引用时,容易导致循环引用,内存泄漏。使用 weak_ptr 打破循环引用。

7.3 使用 make_sharedmake_unique

优先使用 make_sharedmake_unique 来创建智能指针,避免直接使用 new,提高效率和异常安全性。

1
2
auto sp = std::make_shared<Test>(100);
auto up = std::make_unique<Test>(200);

7.4 不要混用原生指针与智能指针

避免在智能指针管理的对象上同时使用原生指针进行管理,防止重复释放或不安全访问。

7.5 理解智能指针的所有权语义

深入理解不同智能指针的所有权规则,避免误用导致资源管理错误。

8. 总结

智能指针是 C++ 中强大的资源管理工具,通过封装原生指针,提供自动化的内存管理,极大地减少了内存泄漏和资源管理错误。std::unique_ptrstd::shared_ptrstd::weak_ptr 各有其应用场景,理解它们的差异和使用方法对于编写安全、高效的 C++ 代码至关重要。此外,通过实现自己的智能指针(如 SimpleSharedPtr),可以更深入地理解智能指针的工作原理,为高级 C++ 编程打下坚实基础。