C++ 智能指针

一、智能指针简介

1.1 为什么需要智能指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 动态申请资源后忘记释放
void UseRawPointer()
{
string *str = new string("hello world");

return;
}

void UseRawPointer()
{
string *str = new string("hello world");

// 发生异常

delete str; // 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放

return;
}

在使用传统指针时,需要手动分配和释放内存,这可能会导致内存泄漏(未释放已分配的内存)或释放后的野指针(指向已释放的内存)。智能指针通过自动内存管理来帮助解决这些问题,确保在不再需要内存时自动释放它。

1.2 智能指针思想

智能指针是在<memory>头文件中的 std 命名空间中定义的。 将智能指针抽象成一个对象,由该对象管理动态申请的内存资源,当该对象出作用域析构时,如果引用计数减到0,则连着将动态申请的内存free。

实现智能指针关键点

  • RAII
  • 支持像指针一样使用

RAII 是 “资源获取即初始化”(Resource Acquisition Is Initialization)的缩写,旨在通过对象生命周期的管理来确保资源(如内存、文件句柄、数据库连接等)的正确获取和释放。RAII 的核心思想是,资源的获取操作应该与对象的初始化操作绑定在一起,资源的释放应该与对象的销毁操作绑定在一起。这意味着在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确管理。

RAII 有以下关键特点:

  1. 资源管理封装: RAII 鼓励将资源的管理和使用封装在对象中,以确保资源在对象的生命周期内正确获取和释放。这允许开发人员不必手动记住释放资源的细节。
  2. 自动化资源释放: 当对象超出其作用域范围时,其析构函数自动被调用,从而自动释放与资源相关的资源,无论是通过正常的代码路径还是异常路径。
  3. 异常安全性: 使用 RAII 可以确保即使在发生异常时,资源也会被正确释放,从而避免资源泄漏。
  4. 代码可读性: RAII 使代码更加清晰和易于理解,因为资源管理逻辑被封装在对象的构造和析构中,而不是分散在代码中。

二、auto_ptr

2.1 面临问题

auto_ptr 是c++ 98定义的智能指针模板,C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代。

auto_ptr 被C++11抛弃的主要原因

  1. 拷贝构造或者赋值采用资源管理器转移思想,存在被拷贝对象悬空问题

    1
    2
    3
    4
    5
    auto_ptr<Date> d1(new Date(2023,10,1));
    auto_ptr<Date> d2(new Date(2023,11,11));

    d1 = d2;
    cout << d2.get() << endl; // 打印空指针,此时继续使用d2访问数据会存在问题
  2. 在STL中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    vector<auto_ptr<string>> v;
    auto_ptr<string> s1(new string("I'm s1"));
    auto_ptr<string> s2(new string("I'm s2"));

    // 必须使用std::move修饰成右值,才可以进行插入容器中
    v.push_back(std::move(s1));
    v.push_back(std::move(s2));

    cout << "v[0]:" << *v[0] << endl;
    cout << "v[1]:" << *v[1] << endl;

    v[0] = v[1]; // 风险,如果进行赋值,存在悬空问题。
    cout << "v[0]:" << *v[0] << endl;
    cout << "v[1]:" << *v[1] << endl; // 程序崩溃,非法访问内存
  3. 不支持对象数组的内存管理

    1
    auto_ptr<int[]> arr(new int[5]); // No matching constructor for initialization of 'auto_ptr<int[]>'

2.2 模拟实现

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
template <class T>
class auto_ptr{
private:
T* _ptr;
public:
explicit auto_ptr(T *p = 0) : _ptr(p) {}

auto_ptr(auto_ptr &a) : _ptr(a.release()) {}

auto_ptr &operator=(auto_ptr &__p) {
reset(__p.release());
return *this;
}

~auto_ptr() { delete _ptr; }

T *get() const { return _ptr; }

// 将auto_ptr内部指针设置为空指针,而不破坏当前由auto_ptr所指向的对象。函数返回调用之前所指向的对象。
T *release() {
T *tmp = _ptr;
_ptr = nullptr;
return tmp;
}

void reset(T *p = 0) {
if (_ptr != p)
delete _ptr;
_ptr = p;
}

T &operator*() const {
return *_ptr;
}

T *operator->() const {
return _ptr;
}
};

三、unique_ptr

  • 唯一拥有:unique_ptr独占所有权的智能指针,意味着在任何时刻只能有一个 unique_ptr 拥有某个对象的所有权。这也确保了对象不会被多个指针共享,避免了重复释放内存的风险。

  • 自动释放:当 unique_ptr 超出其作用域时,会自动调用 delete 释放所管理的对象。这减少了内存泄漏的风险。

  • 不支持拷贝:unique_ptr 不允许拷贝,防止两个指针指向同一个对象。

  • 支持移动语义:可以通过 std::moveunique_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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
template<typename T>
class unique_ptr {
private:
T *_ptr;
public:
explicit unique_ptr(T *p = nullptr) : _ptr(p) {}

unique_ptr &operator=(const unique_ptr &) = delete;

unique_ptr(const unique_ptr &) = delete;

unique_ptr(unique_ptr &&o) : _ptr(o.release()) {}

unique_ptr &operator=(nullptr_t) noexcept {
reset();
return *this;
}

unique_ptr &operator=(unique_ptr &&o) {
if(this != &o){
reset(o.release());
}

return *this;
}

T *release() noexcept {
T *tmp = _ptr;
_ptr = nullptr;
return tmp;
}

void reset(T *p = nullptr) noexcept {
if (_ptr != p)
delete _ptr;
_ptr = p;
}

~unique_ptr() { delete _ptr; }

T *get() const { return _ptr; }

void swap(unique_ptr &x) noexcept {
std::swap(this->_ptr, x._ptr);
}

T &operator*() const {
__glibcxx_assert(get() != nullptr);
return *get();
}

T *operator->() const noexcept {
__glibcxx_assert(get() != nullptr);
return get();
}

explicit operator bool() const noexcept { return nullptr == get() ? false : true; }
};

四、shared_ptr

  • 共享所有权:多个 shared_ptr 可共享对一个对象的所有权,并共同负责管理该对象的生命周期。当最后一个 shared_ptr 被销毁时,对象的生命周期才会结束。

  • 引用计数:每当创建一个新的 shared_ptr 指向同一对象时,引用计数会增加;每当 shared_ptr 被销毁或重置时,引用计数会减少。当引用计数降为零时,所指向的对象会被销毁。

  • 线程安全shared_ptr 本身的操作是线程安全的,引用计数的增加和减少是线程安全的。对所指对象的操作不一定是线程安全的,如果多个线程需要同时读取或修改由 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
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
template<class T>
class shared_ptr {
public:
// 构造函数
explicit shared_ptr(T *ptr = nullptr)
: _ptr(ptr), _pcount(new int(1)), _pmtx(new mutex) {}

// 析构函数
~shared_ptr() {
Release();
}

/* 释放资源
Release() 方法减少引用计数,并根据引用计数的值来判断是否需要删除指向的堆内存对象和引用计数对象。
在操作之前,我们使用互斥量 _pmtx 进行加锁,以保证线程安全。*/
void Release() {
_pmtx->lock();
bool deleteFlag = false;
if (--(*_pcount) == 0) {
if (_ptr) {
// 删除器进行删除
_del(_ptr);
}

delete _pcount;
deleteFlag = true;
}
_pmtx->unlock();
if (deleteFlag) {
delete _pmtx;
}
}

// 增加引用计数
void AddCount() {
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}

// 拷贝构造函数
shared_ptr(const shared_ptr<T> &sp)
: _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx) {
AddCount();
}

// 赋值运算符重载
shared_ptr<T> &operator=(const shared_ptr<T> &sp) {
if (_ptr != sp._ptr) {
Release();

_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;

AddCount();
}

return *this;
}

// operator*() 重载
T &operator*() {
return *_ptr;
}

// operator->() 重载
T *operator->() {
return _ptr;
}

// get() 方法
T *get() {
return _ptr;
}

// use_count() 方法
int use_count() {
return *_pcount;
}

// swap() 方法,交换 2 个 shared_ptr 智能指针的内容
void swap(shared_ptr<T> &sp) noexcept {
std::swap(_ptr, sp._ptr);
std::swap(_pcount, sp._pcount);
std::swap(_pmtx, sp._pmtx);
}

// reset() 方法,重置 shared_ptr 智能指针对象
void reset(T *ptr = nullptr) {
// 释放原有资源
Release();

// 重新赋值
_ptr = ptr;
_pcount = new int(1);
_pmtx = new mutex;
}

private:
T *_ptr; // 指向堆内存对象的指针
int *_pcount; // 引用计数的指针
mutex *_pmtx; // 保护引用计数的互斥量

// 包装器
function<void(T *)> _del = [](T *ptr) {
cout << "lambda delete:" << ptr << endl;
delete ptr;
};
};

4.1 函数模版make_shared

shared_ptr 需要维护引用计数的信息:

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正共享该对象,共享的对象会在最后一个强引用离开的时候销毁( 也可能释放)

  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs.,当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话)。

优点1 提高内存分配效率

通过使用原始的 new运算符分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块,总共需要分配2次内存空间。

内存图1

make_shared 只需要单次内存分配,提高了性能,并减少了内存碎片。

内存图2

优点2 异常安全

使用 new 直接创建对象时,如果在 std::shared_ptr 构造之前抛出异常,可能会导致内存泄漏。而使用 make_shared 可以避免这种情况,因为对象的分配和智能指针的构造是原子操作,不会中途出现内存泄漏的问题。

缺点1 对象构造函数是保护或私有时,无法使用 make_shared

缺点2 对象的内存可能无法及时回收

make_shared 只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由 shared_ptr 和 weak_ptr 共享的,因此两者共同管理着这个内存块(托管对象 + 控制块)。当强引用计数为 0 时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个 weak_ptr 离开作用域时,弱引用也减为 0 才会释放这块内存块。原本强引用减为 0 时就可以释放的内存, 现在变为了强引用和弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。

4.2 类模版enable_shared_from_this

enable_shared_from_this是一个模板类,定义于头文件<memory>enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 shared_ptr 对象 pt 管理)安全地创建指向自身的 shared_ptr 智能指针(假设名为 pt1, pt2, … ) ,它们与 pt 共享对象 t 的所有权

​ 若一个类 T 继承enable_shared_from_this<T> ,则会为该类 T 提供成员函数 shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr 对象,它与 pt 共享 t 的所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Bad {
public:
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}

~Bad() { std::cout << "Bad::~Bad() called" << std::endl; }
};

int main() {
// 错误的示例,每个shared_ptr都认为自己是对象仅有的所有者
std::shared_ptr<Bad> bp1(new Bad());
std::shared_ptr<Bad> bp2 = bp1->getptr();

std::cout << "bp1.use_count() = " << bp1.use_count() << std::endl; // 输出1
std::cout << "bp2.use_count() = " << bp2.use_count() << std::endl; // 输出1

// 进程崩溃,对象析构了2次
}
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
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>

class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
public:
void startAsyncTask() {
// 启动一个异步任务,在线程中执行
auto self = shared_from_this();
std::thread([self] {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
self->onTaskComplete();
}).detach();
}

void onTaskComplete() {
std::cout << "Task complete!" << std::endl;
// 这里可以访问对象的成员或调用其他方法
}

~AsyncWorker() {
std::cout << "AsyncWorker destroyed!" << std::endl;
}
};

int main() {
{
// 创建一个 AsyncWorker 对象并通过 shared_ptr 管理
std::shared_ptr<AsyncWorker> worker = std::make_shared<AsyncWorker>();
worker->startAsyncTask();

// 等待异步任务完成
std::this_thread::sleep_for(std::chrono::seconds(3)); // 主线程等待
}

// 程序结束,确保对象正确销毁
std::this_thread::sleep_for(std::chrono::seconds(1)); // 让 detachment 线程输出 "destroyed"
return 0;
}

应用场景

  • 安全的引用管理: 如果类中的某些方法需要返回一个 shared_ptr 指向该类的实例,而调用方已经拥有一个 shared_ptr,直接返回 this 会产生问题,因为 this 不是智能指针。如果多个地方都创建 shared_ptr,对象可能会在引用计数不一致的情况下被提前释放。通过 enable_shared_from_this,可以确保同一个对象的所有 shared_ptr 共享相同的引用计数。

  • 异步操作或回调: 在异步操作(如网络请求、I/O 操作、线程回调)中,如果想确保对象在操作完成之前不会被销毁,可以使用 shared_from_this() 在回调中安全持有对象的 shared_ptr,从而延长对象的生命周期,防止它在异步操作完成前被销毁。

注意事项

  • shared_from_this() 只能在对象已经通过 shared_ptr 管理时调用。如果在一个尚未通过 shared_ptr 管理的对象上调用 shared_from_this(),则会导致运行时错误(std::bad_weak_ptr 异常)。
  • 当你希望某个对象能够通过多个地方持有它的 shared_ptr 时,继承 enable_shared_from_this 可以避免出现多个 shared_ptr 独立管理同一对象的情况。

五、weak_ptr

  • 不管理生命周期:指向一个由shared_ptr管理的对象,但不会改变shared_ptr的引用计数

  • 访问管理对象weak_ptr 不能直接访问所管理的对象,但可以通过 lock() 方法获取一个有效的 shared_ptr,然后访问对象。

5.1 循环引用

循环引用:指两个或多个对象通过 shared_ptr 相互引用,导致它们的引用计数永远不会降为零,从而引发内存泄漏。

循环引用1
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
class B; 

class A {
public:
shared_ptr<B> ptrB; // 指向 B 的 shared_ptr
A() { std::cout << "A Constructor" << std::endl; }

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

class B {
public:
shared_ptr<A> ptrA; // 指向 A 的 shared_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 和 B 互相引用
a->ptrB = b;
b->ptrA = a;

return 0; // 这里不会调用析构函数,因为循环引用导致引用计数无法降为0
}

使用weak_ptr解决循环引用问题

循环引用2
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
class B; 

class A {
public:
shared_ptr<B> ptrB; // 指向 B 的 shared_ptr
A() { std::cout << "A Constructor" << std::endl; }

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

class B {
public:
weak_ptr<A> ptrA; // 使用 weak_ptr 代替 shared_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;

return 0; // 正常调用析构函数,解决了循环引用问题
}

5.2 相关成员函数

bool expired() const noexcept;

作用:检查 weak_ptr 是否已失效。如果对象已无效(对应的 shared_ptr 引用计数为 0),返回 true,否则返回 false。

shared_ptr<element_type> lock() const noexcept;

作用:尝试获取指向对象的 shared_ptr。如果对象还有效,则返回一个指向该对象的 shared_ptr;如果对象已无效或weak_ptr指向空,则返回空的 shared_ptr

void reset() noexcept;

作用:将 weak_ptr 置为空。

long int use_count() const noexcept;

作用:返回与 weak_ptr 关联的 shared_ptr 的引用计数(即还有多少个 shared_ptr 引用该对象)。如果对象已失效或weak_ptr指向空,则返回 0。