八股篇|通过手撕来习惯使用智能指针

智能指针是C++11标准在RAII思想下引入智能指针库,其中包括 std::shared_ptrstd::unique_ptrstd::weak_ptr。现代C++编程中经常使用智能指针,而且在当前程序员人才过剩时代,面试手撕成了常态。

智能指针

C++程序设计中使用堆内存是非常频繁的操作,在工作开发业务逻辑模块时,经常会出现使用多个对象进行数据传输。而堆内存的申请和释放都由程序员自身控制,一不留神可能会导致内存泄漏,后期排查起来要了命。

C++里面的四个智能指针: auto_ptrunique_ptrshared_ptrweak_ptr 其中后三个是C++11支持, 并且第一个已经被C++11弃用。

auto_ptr

C98就诞生的智能指针,不过存在缺陷。

主要问题包括:

  1. auto_ptr 在拷贝构造和赋值操作中会转移所有权,容易导致潜在的内存泄漏问题。
  2. auto_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
#include <iostream>
#include <memory>

class MyClass {
public:
MyClass() {
std::cout << "MyClass Constructor" << std::endl;
}

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

void doSomething() {
std::cout << "Doing something..." << std::endl;
}
};

int main() {
std::auto_ptr<MyClass> ptr(new MyClass());

ptr->doSomething();

// auto_ptr 在拷贝时会转移所有权,因此不能进行拷贝
// std::auto_ptr<MyClass> ptr2 = ptr; // 这行代码会导致编译错误

// 不需要手动释放内存,当指针离开作用域时,内存会自动释放

return 0;
}

现代编译环境更严格,并且运用场景更复杂,该智能指针已被弃用。

std::unique_ptr

std::unique_ptr正如指针名字一样,独一无二的。它不允许其他的智能指针共享其内部的指针,不允许通过赋值将 一个unique_ptr赋值给另一个unique_ptr

但是通过std::move可以进行所有权转移,原指针则不再拥有控制权,并被置为空。

1
2
3
4
5
6
7
unique_ptr<std::string> my_ptr(new std::string);
cout << "my_ptr:" << my_ptr << endl; //my_ptr:01666158
//unique_ptr<std::string> my_other_ptr = my_ptr; // 报错,不能复制
unique_ptr<std::string> my_other_ptr = std::move(my_ptr);

cout << "my_other_ptr:" << my_other_ptr << endl; //my_other_ptr:01666158
cout << "my_ptr:" << my_ptr << endl; //my_ptr:00000000
std::make_unique初始化

std::make_uniqueC++14 标准库中的一个函数,用于创建一个带有给定参数的 std::unique_ptr。它用于创建具有自动内存管理的动态分配对象。

以下是 std::make_unique 的一般语法:

1
std::make_unique<T>(args...)

其中:

  • T 是要创建的对象的类型。
  • args... 是要传递给 T 构造函数的参数。

例如,如果您想要创建一个指向值为 42 的整数的 std::unique_ptr,可以使用 std::make_unique 如下所示:

1
auto ptr = std::make_unique<int>(42);

这将创建一个指向值为 42 的整数的 std::unique_ptr<int>

std::share_ptr

std::share_ptr也如它的名字一样,将指针分享使用。所以在std::share_ptr内部有一个引用计数,每一个拷贝都只向同一个内存。当最后一个计数的指针被析构时,该内存才会被释放

1、std::share_ptr初始化
1
2
3
4
5
6
7
8
9
10
11
12
std::shared_ptr<int> share_p1(new int(1));
std::shared_ptr<int> share_p2 = share_p1;
std::shared_ptr<int> share_p3;
share_p3.reset(new int(1));
if (share_p3) {
cout << "p3 is not null";
}

//输出结果
p3 is not null
p2 address:0000020E43C86B70
p3 address:0000020E43C86C30
2、std::make_shared初始化

std::make_sharedC++11标准库中的一个函数,它比普通初始化方式更高效。主要是因为:

  1. 减少内存分配次数
    • std::make_shared 会一次性分配内存用于存储对象和控制块(用于引用计数等),而不是分别为对象和控制块分配内存。
    • 相比之下,使用 newstd::shared_ptr 分别分配对象和控制块会导致两次内存分配,增加了内存分配的开销。
  2. 减少内存碎片
    • 由于 std::make_shared 一次性分配了对象和控制块所需的内存,可以减少内存碎片的产生。
    • 使用 newstd::shared_ptr 分别分配对象和控制块可能会导致内存碎片的产生,影响内存的利用效率。
  3. 更好的缓存局部性
    • 由于 std::make_shared 一次性分配了连续的内存块用于存储对象和控制块,这种连续存储的方式有利于缓存局部性,提高访问效率。
    • 相比之下,使用 newstd::shared_ptr 分别分配的内存块可能不是连续的,降低了缓存局部性,影响访问效率。
1
auto share_p4 = std::make_shared<int>(100);
3、std::share_ptr的循环引用

循环引用是share_ptr的特殊情况,该情况会导致内存泄漏,比如下面两个例子,AptrBptr互相指向对方,导致互相引用计数都是2。当结束程序运行时,两个指针计数都无法为0,导致无法析构,从而内存泄漏。

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
class Aptr;
class Bptr;

class Aptr {
public:
std::shared_ptr<Bptr> bptr;
~Aptr() {
cout << "A is deleted" << endl;
}
};

class Bptr {
public:
std::shared_ptr<Aptr> aptr;
~Bptr() {
cout << "B is deleted" << endl; // 析构函数后,才去释放成员变量
}
};


int main()
{
std::shared_ptr<Aptr> pa;

std::shared_ptr<Aptr> ap(new Aptr);
std::shared_ptr<Bptr> bp(new Bptr);
ap->bptr = bp;
bp->aptr = ap;
pa = ap;

ap->bptr.reset(); // 手动释放成员变量才行
cout << "main leave. pa.use_count():" << pa.use_count() << endl; // 循环引用导致ap bp退出了作用域都没有析构
}

//输出结果
//main leave. pa.use_count():3

为了避免循环引用发生,std::weak_ptr用来协助管理。

std::weak_ptr

std::weak_ptr 是 C++11 中引入的一种智能指针,用于解决 std::shared_ptr 的循环引用(circular reference)问题。std::weak_ptr 是一种弱引用指针,它不会增加引用计数,也不会拥有被指向对象的所有权,因此不会影响对象的生命周期。

  1. 解决循环引用问题
    • 当两个对象相互持有对方的 std::shared_ptr 时,会导致循环引用,使得对象无法被正确释放,造成内存泄漏。std::weak_ptr 可以打破这种循环引用,避免内存泄漏。
  2. 安全地访问被 std::shared_ptr 管理的对象
    • 通过 std::weak_ptr 可以安全地访问被 std::shared_ptr 管理的对象,因为 std::weak_ptr 不会增加引用计数,当对象被释放后,std::weak_ptr 会自动变为一个空指针。
  3. 使用前需要进行 lock 操作
    • 为了访问 std::weak_ptr 指向的对象,需要通过 lock 方法将 std::weak_ptr 转换为 std::shared_ptr,如果对象存在,则返回一个有效的 std::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
std::weak_ptr<int> gw;
void getWeakptr()
{
if (gw.expired()) {
cout << "gw无效,资源已释放" << endl;;
}
else {
auto spt = gw.lock();
cout << "gw有效, *spt = " << *spt << endl;
}
}

int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
getWeakptr();
}
getWeakptr();
}

//输出结果
//gw有效, *spt = 42
//gw无效,资源已释放

针对上面的循环引用只需要把其中一个智能指针改成weak_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
class ClassB; // 前向声明

class ClassA {
public:
std::shared_ptr<ClassB> bPtr;

void doSomething() {
std::cout << "ClassA is doing something" << std::endl;
}

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

class ClassB {
public:
std::weak_ptr<ClassA> aWeakPtr;

void doSomething() {
std::cout << "ClassB is doing something" << std::endl;
// 使用lock()方法获取ClassA对象的shared_ptr
if (auto aPtr = aWeakPtr.lock()) {
aPtr->doSomething();
}
else {
std::cout << "ClassA object has been destroyed" << std::endl;
}
}

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

void testWeak2Share()
{
std::shared_ptr<ClassA> aPtr = std::make_shared<ClassA>();
std::shared_ptr<ClassB> bPtr = std::make_shared<ClassB>();

aPtr->bPtr = bPtr;
bPtr->aWeakPtr = aPtr;

// 调用ClassB的方法,间接调用ClassA的方法
bPtr->doSomething();
}


int main()
{
testWeak2Share();
}

//输出结果
//ClassB is doing something
//ClassA is doing something
//ClassA destructor called
//ClassB destructor called

在上面的示例中,ClassA持有ClassBshared_ptr,而ClassB持有ClassAweak_ptr。通过使用weak_ptr,我们成功解决了循环引用的问题,避免了内存泄漏。

ClassB调用doSomething()方法时,它会尝试获取ClassAshared_ptr,如果ClassA对象还存在,则可以正常调用doSomething()方法;否则会输出提示信息。

实现智能指针

模拟实现std::unique_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
#include <iostream>
#include <memory>

using namespace std;

template<class T>
class UniquePtr
{
public:
UniquePtr(T* ptr = nullptr)
: _ptr(ptr)
{}

~UniquePtr()
{
if (_ptr)
delete _ptr;
}

//移动构造
UniquePtr(UniquePtr&& other) noexcept : _ptr(other._ptr) {
other._ptr = nullptr;
}

UniquePtr& operator=(const UniquePtr&& other) noexcept {
if (this != &other) {
delete _ptr;
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}

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

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

friend std::ostream& operator<<(std::ostream& os, const UniquePtr& myPtr) {
if (myPtr._ptr) {
os << myPtr._ptr;
}
else {
os << "nullptr";
}
return os;
}

private:
// C++98防拷贝的方式:只声明不实现+声明成私有
//UniquePtr(UniquePtr<T> const&);
//UniquePtr& operator=(UniquePtr<T> const&);

//C++11防拷贝方式
UniquePtr(UniquePtr<T> const&) = delete;
UniquePtr& operator=(UniquePtr<T> const&) = delete;

private:
T* _ptr;
};

int main()
{
UniquePtr<std::string> my_ptr(new std::string);

cout << "my_ptr:" << my_ptr << endl;
//UniquePtr<std::string> my_other_ptr = my_ptr; // 报错,不能复制
UniquePtr<std::string> my_other_ptr = std::move(my_ptr);

system("pause");
return 0;
}

image-20240330171955868

模拟实现std::share_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
111
112
113
114
115
116
117
118
119
120
121
122
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>

using namespace std;

template<class T>
class SharePtr
{
public:
SharePtr(T* ptr = nullptr)
:_ptr(ptr)
,_pRefCount(new int(1))
//, _pMutex(new mutex)
{

}

~SharePtr() {
std::cout << _ptr << endl;
Release();
}

SharePtr(const SharePtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
//, _pMutex(sp._pMutex)
{
AddRefCount();
}

// sp1 = sp2
SharePtr<T>& operator=(const SharePtr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
// 释放管理的旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;

AddRefCount();
}
return *this;
}

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

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

int UseCount() { return *_pRefCount; }
T* Get() { return _ptr; }

void AddRefCount()
{
// 加锁或者使用加1的原子操作
_pMutex.lock();
++(*_pRefCount);
_pMutex.unlock();

}

friend std::ostream& operator<<(std::ostream& os, const SharePtr& myPtr) {
if (myPtr._ptr) {
os << myPtr._ptr;
}
else {
os << "nullptr";
}
return os;
}

private:
void Release()
{
bool deleteflag = false;
// 引用计数减1,如果减到0,则释放资源
_pMutex.lock();
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
deleteflag = true;

std::cout << "Share was Release" << endl;
}
_pMutex.unlock();
}


private:
T* _ptr;
int* _pRefCount; //引用计数
mutex _pMutex; //互斥锁
};

void TestMySharePtr()
{
SharePtr<int> share_p1(new int(1));
SharePtr<int> share_p2 = share_p1;
SharePtr<int> share_p3;

std::cout << "share_p1:" << share_p1 << endl;
std::cout << "share_p2:" << share_p2 << endl;
std::cout << "share_p3:" << share_p3 << endl;

std::cout << "share_p1 use_count:" << share_p1.UseCount() << endl;
std::cout << "share_p2 use_count:" << share_p2.UseCount() << endl;
std::cout << "share_p3 use_count:" << share_p3.UseCount() << endl;
}

int main()
{
TestMySharePtr();

system("pause");
return 0;
}

image-20240330180016724