cpp智能指针

cpp智能指针

引入

在cpp 中,我们用new来定义一个我们自己掌控生命周期的对象.
他不像局部变量这种会在函数结束的时候销毁,或者普通全局变量会在程序结束的时候变量所在内存被销毁.
我们需要手动delete,但是这样的步骤就是有点繁琐了,比如我们这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main() {
while (true) {
int* p = new int[1000000];

std::cout << "申请了一块内存" << std::endl;
delete p;
}

return 0;
}

这个p在这里如果没有delete p就会疯狂的 申请新的内存,内存疯涨,建议不要轻易尝试,可能会死机
所以这里我们的智能指针就解决这样一些问题

“申请了堆内存以后,忘记释放、重复释放、异常情况下没法正确释放” 这些资源管理问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <memory>
void test() {
auto p = std::shared_ptr<int[]>(new int[1000000]);
std::cout << "申请了一块内存" << std::endl;
}
int main() {
while (true) {
test();
}

return 0;
}

这里智能指针就会自动销毁int的内存.智能指针会在指针p销毁的时候,同时对应指向的内存也会被销毁,当然下面还是要具体情况具体分析.
cpp一共提供了三种智能指针,后续我们基于具体情况也会讲智能指针自动销毁的原理

delelte 和delete[]的区别

delete对重复调用析构函数,主要区别会在类对象数组体现出来.
如果我们
类,A初始化,对象数组 A *p=new A[10]

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>

class A {
private:
int a = 10;
int b = 10;

public:
A() {
std::cout << "A 默认构造 called" << std::endl;
}

A(int a, int b) : a(a), b(b) {
std::cout << "A 构造 called" << std::endl;
}

int Axor() {
return a ^ b;
}

int Aplus() {
return a + b;
}

~A() {
std::cout << "A 析构 called" << std::endl;
}
};

int main() {
A* p = new A[10];

for (int i = 0; i < 10; i++) {
p[i] = A(i, i + 1);
}

delete p;

return 0;
}

这只是一个势力代码,输出的时候我们可以发现,如果使用delete p就会只调用一次析构,这样会导致释放内存崩溃

1
2
3
4
5
6
A 构造 called
A 析构 called
A 析构 called
munmap_chunk(): invalid pointer
[1] 3593216 IOT instruction (core dumped) ./test

unique_ptr

这个智能指针有一个要求,就是一个由这个智能指向的对象智能有一个unuque_ptr指针
利用
uniqure_ptr

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

删除了拷贝和赋值.
一般来说构造一个智能指针有两种构造方法

1
auto p = std::make_unique<int[]>(1000000);
1
auto p = std::unique_ptr<int[]>(new int[1000000]);

但是这两个在使用上有一定区别,现代cpp更常用的是第一种.

1
2
3
make_unique(size_t __num)
{ return unique_ptr<_Tp>(new remove_extent_t<_Tp>[__num]()); }

这里stl里面定义的make_unique.我们可以看到他会自动帮我们初始化一个内存的初始值都为0.
这里其实make_unique等价于

1
auto p = std::unique_ptr<int[]>(new int[1000000]());

unique_ptr不会初始化,会直接在他的指针指向的内存上取值如果这个内存上之前有值可能就是随机值
unique_ptr的基本结构如下,类模板一般会定义

1
2
3
4
5
p
┌──────────────┐
│ ptr ─────────┼────> A对象
│ deleter │
└──────────────┘

指针和删除器
智能指针的基础操作
tips:智能指针本质是一个类对象
p.get()可以获取裸指针
p.reset()释放指针,这里操作后p指向null
也可以通过move转移控制权

1
2
3
4
5
6
7
8

void reset(pointer __p) noexcept
{
const pointer __old_p = _M_ptr();
_M_ptr() = __p;
if (__old_p)
_M_deleter()(__old_p);
}

这里p.reset()里面可以传入另一个指针,用old_p存储之前的指针然后释放掉,之后新的指针就被指向为M_ptr.
reset可以释放对象,也可以指向新对象
p.release()
把这个p从智能指针释放出来,变成裸指针

1
2
3
4
5
6
     pointer release() noexcept
{
pointer __p = _M_ptr();
_M_ptr() = nullptr;
return __p;
}

p.swap(q),交换p和q变量的智能指针

1
2
3
4
5
6
   swap(__uniq_ptr_impl& __rhs) noexcept
{
using std::swap;
swap(this->_M_ptr(), __rhs._M_ptr());
swap(this->_M_deleter(), __rhs._M_deleter());
}

获取智能指针的删除器

1
2

auto& deleter = p.get_deleter();

share_ptr

依然,这里是一个智能指针的类模板,表现上类似是一个指针,多个 share_ptr可以指向一个堆内存,但是这个share_ptr有一个count计数,计算
share_ptr的结构和unique_ptr不一样,
share_ptr会引入计数器,同时计时器和删除器这些东西都放在控制器里面

1
2
3
element_type*        _M_ptr;       // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.

类中会定义,ptr表示 裸指针,
refcount是一个__shared_count类对象里面叫做引用计数管理对象.

1
2
3
4
5
6
7
控制块 {
强引用计数; // 有多少个 shared_ptr 拥有对象
弱引用计数; // 有多少个 weak_ptr 观察对象
删除器; // 最后怎么删除对象
分配器; // 控制块/对象怎么分配和释放
可能还有对象本身; // make_shared 时常见
}

后续我们来讲解一下share_ptr的相关操作
他是可以复制和拷贝的,每次之后计数都会加1,move操作没有增加指针数量,不加1
我们用

1
p1.use_count())

返回计数

share_ptr是reset操作和 unique不同,他会先判断,share_ptr的 计数是否为0之后再决定是否销毁当前的内存.

同时share_ptr没有release操作

weak_ptr

waek_ptr一般是和share_ptr一起出现.
通俗来说,弱引用一般是指不影响对象生命周期的一种引用,
它是一种弱引用,指向shared_ptr所管理的对象,而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
waek_ptr提供
|use_count()|返回与之共享对象的shared_ptr的数量|
|expired()|检查所指对象是否已经被释放|
|lock()|返回一个指向共享对象的shared_ptr,若对象不存在则返回空shared_ptr|
|owner_before()|提供所有者基于的弱指针的排序|
|reset()|操作弱引用指针,减少弱引用计数|
|swap()|交换两个weak_ptr对象|
下面给出lock和expired的示例

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>
struct A
{
private:
int a;

};

int main(){
std::shared_ptr<A> p1=std::make_shared<A>();
std::cout<<"对象计数"<<p1.use_count()<<std::endl;
std::weak_ptr<A> wp=p1;
std::shared_ptr<A> p2=wp.lock();
std::cout<<"对象计数"<<p2.use_count()<<std::endl;
std::weak_ptr<A> wp2=std::make_shared<A>();
std::cout<<"wp2是否存在对象"<<wp2.expired()<<std::endl;
std::cout<<"wp是否存在对象"<<wp.expired()<<std::endl;

return 0;

}
'''
对象计数1
对象计数2
wp2是否存在对象1
wp是否存在对象0
'''

这里我们可以看看cpp在weak_ptr主要起到一个观测.无法直接管理对象.
owner_before操作share_ptr和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
#include <iostream>
#include <memory>

struct A {
int x;
int y;
};

int main() {
auto p = std::make_shared<A>();

std::shared_ptr<int> px(p, &p->x);
std::shared_ptr<int> py(p, &p->y);

std::cout << "px.get() = " << px.get() << std::endl;
std::cout << "py.get() = " << py.get() << std::endl;

std::cout << "px.use_count() = " << px.use_count() << std::endl;
std::cout << "py.use_count() = " << py.use_count() << std::endl;

std::cout << std::boolalpha;
std::cout << "px.owner_before(py) = " << px.owner_before(py) << std::endl;
std::cout << "py.owner_before(px) = " << py.owner_before(px) << std::endl;

return 0;
}

这里因为px和py虽然, 裸指针不一样,但是 因为是都是来自p 所以控制块都是p的

1
2
3
_M_less(const __weak_count<_Lp>& __rhs) const noexcept
{ return std::less<_Sp_counted_base<_Lp>*>()(this->_M_pi, __rhs._M_pi); }

主要是对两个大小做判断,我的学识浅薄,没发现这个比较有什么意义.但是可以判断两个控制块是否一样.比如这里不一样肯定就是没有大小的比却所以输出都是flase.如果不一样的话肯定有true和false这两个
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>

class Resource {
public:
Resource(const std::string& id, const std::string& data)
: id_(id), data_(data) {
std::cout << "创建资源: " << id_ << std::endl;
}

~Resource() {
std::cout << "销毁资源: " << id_ << std::endl;
}

void show() const {
std::cout << "资源ID: " << id_ << ", 内容: " << data_ << std::endl;
}

private:
std::string id_;
std::string data_;
};

class ResourceManager {
private:
static std::unordered_map<std::string, std::weak_ptr<Resource>> resources_;
public:
std::shared_ptr<Resource> getResource(const std::string& id) {
auto it = resources_.find(id);
if (it != resources_.end()) {
std::cout << "资源已存在: " << id << std::endl;
std::shared_ptr<Resource> res = it->second.lock();
return res;
} else {
std::shared_ptr<Resource> res = std::make_shared<Resource>(id, "这是资源 " + id);
resources_[id] = res;
return res;
}
}
void clearcache(){
for(auto it=resources_.begin(); it!=resources_.end(); ){
if(it->second.expired()){
std::cout << "清除过期资源: " << it->first << std::endl;
it = resources_.erase(it);
} else {
++it;
}
}
}
~ResourceManager() {
std::cout << "资源管理器销毁,正在清理资源..." << std::endl;
clearcache();
}


};
std::unordered_map<std::string, std::weak_ptr<Resource>> ResourceManager::resources_;

int main(){

ResourceManager manager; {
auto res1 = manager.getResource("res1");
res1->show();
std::cout<<"res1 use count: "<<res1.use_count()<<std::endl;
auto res2 = manager.getResource("res2");
res2->show();

auto res1_again = manager.getResource("res1");
res1_again->show();}
std::cout << "资源已超出作用域,正在清理..." << std::endl;
manager.clearcache();
return 0;
}

这里我们用weak_ptr和另一个类构建了一个哈希表,用另一个缓存类,专门分配缓存,利用weak_ptr,在main函数下面的局部的一个作用域下面,这个作用域结束res1等等这些指针share_ptr 跟随一起销毁,之后count计数归零,之后指向的内存也被销毁,
所以我们可以用这个静态成员变量专门动态的释放内存,只要当前调用生成的指针销毁之后,缓存中的weak_ptr指向的同一个内存也会被销毁.如果用的是share_ptr存放在这个哈希表中,那么我们就需要等到整个对象销毁后再销毁resourse了,但是这里是static 要等到整个程序结束了.

解决循环引用

什么是循环引用
当有两个类A和B,类A里指向类B,类B里指向类A,具体如下情况

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

class B;

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

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

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

~B() {
std::cout << "B 析构" << std::endl;
}
};

int main() {
auto pa = std::make_shared<A>();
auto pb = std::make_shared<B>();

pa->b = pb;
pb->a = pa;
std::cout << "pa.use_count() = " << pa.use_count() << std::endl; // 输出 1
std::cout << "pb.use_count() = " << pb.use_count() << std::endl; // 输出 1

return 0;
}

这里pa和pb->a都是指向一个A的类对象,pb和pa->b都是指向一个B的类对象.
很明显,这么写无法调用析构函数,至于为什么,因为main函数结束的时候,pa和pb销毁,但是a和b都还在并且都是无法销毁的因为shared_ptr 计数都为1,这里我们只需要把其中一个比如
A类的
写作

1
std::weak_ptr<B> b;

之后,main结束后B就会被销毁B销毁的时候就顺带销毁 std::shared_ptr a;就也销毁类A

构建单例模式

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>
#include <mutex>



class A {
public:
static std::weak_ptr<A> instance_;
static std::mutex mtx;
private:
A(){
std::cout << "A 构建" << std::endl;
}
public:
static std::shared_ptr<A> getInstance() {
std::lock_guard<std::mutex> lock(mtx);
if(instance_.expired()){
std::shared_ptr<A> sp = std::shared_ptr<A>(new A());
instance_ = sp;
return sp;
}
else{
return instance_.lock();
}
}
~A(){
std::cout << "A 析构" << std::endl;
}
};
std::weak_ptr<A> A::instance_;
std::mutex A::mtx;
int main() {
std::shared_ptr<A> a1 = A::getInstance();
std::shared_ptr<A> a2 = A::getInstance();
std::cout << "a1 use_count: " << a1.use_count() << std::endl;
std::cout << "a2 use_count: " << a2.use_count() << std::endl;
return 0;
}

这里所有的指针都指向了一个对象,并且每当main函数结束的时候,就会自动销毁对象,如果是在别的作用域,也会如此.之后每次增加调用getindance()都是增加对象的share_ptr并未真正增加对象


cpp智能指针
http://example.com/2027/03/19/cpp_智能指针/
Author
fox
Posted on
March 19, 2027
Licensed under