0%

C++ 中的智能指针

承接移动语义,来更新智能指针,上文说到 unique_ptr 也会用到移动语义的东西。由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete,一旦忘记 delete 或者程序过早退出而没有 delete,导致内存泄漏总是不好的。内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存,从而造成了内存的浪费。

所以,能不能有一种东西的存在,使用 new 创建对象后,对象作用域消失时,能不能自动析构并调用 delete?手动管理过程繁琐且容易出错,那么尝试通过智能指针来实现这一管理过程。

unique_ptr

std::unique_ptr 是通过指针独占式拥有或严格拥有并管理另一对象,建立了所有权的概念,一个智能指针只能拥有一个对象,一个对象也只能被一个智能指针所拥有,这样智能指针消失时,就会 delete 对象,并在 unique_ptr 离开作用域或拥有的最后一个资源被销毁时释放。

赋值时,对象的所有权会转让,也就是移动赋值,因为所有权独享所以删除了拷贝构造,只能通过 move 实现移动所有权。应用场景:对象延迟销毁,可以将对象转移到另一个线程释放,不影响主线程。来看一个例子:

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>

class A{
public:
void show(){
std::cout << "hello" << std::endl;
}
};

void demo(){
auto a = new A();
std::unique_ptr<A> ps (a);
ps->show();
delete a;
// 错误,因为管理的资源被释放
std::cout << (ps.get() == nullptr) << std::endl;
// 移动所有权
auto ps1 = std::move(ps);
ps1->show();
}

int main (){
demo();
return 0;
}

智能指针和指针一样,可以解引用,或者访问类成员。不过智能指针指向的对象必须位于堆区,否则 delete 时会发生错误。

智能指针的构造函数有 explicit 修饰,因此不能发生隐式转换,也就是说,下面的语句是错误的:

1
2
3
double* pr = new double;
unique_ptr<double> ptr;
ptr = pr; // not allowed, implicit conversion

然后顺手看了官网的 API,比较常用的是 releaseswapreset 方法:

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

class A{
public:
int a;
A() = default;
A(int c) : a{c} {};
void show(){
std::cout << this->a << std::endl;
}
};

int main (){
std::unique_ptr<A> up1(new A(1));
std::unique_ptr<A> up2(new A(2));
// 交换对象
up1.swap(up2);
up1->show(); // 2

// deletes managed object, acquires new pointer
up1.reset (new A(3));
up1->show(); // 3
// deletes managed object
up1.reset();

std::unique_ptr<A> up(new A(14));
std::cout << "About to release Foo...\n";
// release 对象给别人
A* fp = up.release();
fp->show(); // 14
return 0;
}

shared_ptr

shared_ptr 使用引用计数,记录多个指针可以指向一个对象,即多个 shared_ptr 共同管理一个对象,仅当最后一个指针消失时,调用 delete

因此,shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针,通过计数实现对象的最后一个拥有着有责任销毁对象,并清理与该对象相关的所有资源;且时间上的开销主要在初始化和拷贝操作上,因为不能像移动语义那样移动所有权。但这也造就了 shared_ptr 独特的应用场景:

  1. 共享权不明,多个对象管理同一个内存。举个例子,实验室的所有人共同拥有空调,空调的所有权不属于某一个人,但最后一个人走的时候要关空调。
  2. 如果一个对象的复制操作很费时,同时我们又需要在函数间传递这个对象,我们往往会选择传递指向这个对象的指针来代替传递对象本身,以此来避免对象的复制操作。既然选择使用指针,那么使用 shared_ptr 是一个更好的选择,即起到了向函数传递对象的作用,又不用为释放对象操心。
  3. shared_ptr 支持在构造的时候传入一个定制删除器,替代 delete 在生命周期结束时调用,以此实现 RAII 的思想,即资源的有效期与持有资源的对象的生命期严格绑定,将资源的获取放在类的构造函数里,资源的释放放在类的析构函数里。在类的生存期结束的时候,析构函数会被自动调用,对应的资源将会释放。
  4. 把指针存入标准库容器时,如果容器中保存的是普通指针,当我们在清空某个容器时,先要释放容器中指针所指向的资源,然后才能清空这些指针本身。如果普通指针替换成相应 shared_ptr,容器的 clear() 清空 shared_ptr,而随着 shared_ptr 的释放,它会自动释放它所管理的资源,而无需我们主动去释放。
1
2
3
4
5
6
7
8
// tranditionally 
FILE *fp = fopen("./1.txt","r");
// ...
// ...
fclose(fp);
//-------
// 通过使用定制删除器, 将删除器作为回调函数传入
shared_ptr fp1(fopen("./1.txt","r"), fclose);

weak_ptr

weak_ptr 允许共享但不拥有某对象,指向 shared_ptr 指针指向的对象的内存,却并不拥有该内存。在使用的时候检查一下指针的有效性。一旦最后一个共享该对象的智能指针失去了所有权,任何 weak_ptrlock() 方法返回 nullptr。因此,在 defaultcopy 构造函数之外,weak_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
#include <iostream>
#include <memory>
using namespace std;

void Check(weak_ptr<int> &wp) {
shared_ptr<int> sp = wp.lock(); // 重新获得shared_ptr对象
if (sp != nullptr) {
cout << "The value is " << *sp << endl;
}
else {
cout << "Pointer is invalid." << endl;
}
}

int main() {
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2 = sp1;
weak_ptr<int> wp = sp1; // 指向sp1所指向的内存

cout << *sp1 << endl;
cout << *sp2 << endl;
Check(wp);

sp1.reset(); // 释放所管理的对象
cout << *sp2 << endl;
Check(wp);

sp2.reset();
Check(wp);

return 0;
}

可以应用于对象可能失效的场景,可打破循环引用 cycles of references 的问题。

内存泄漏

题外话,我们可以通过观察来发现程序是否存在内存泄漏:程序长时间运行后内存占用率一直不断的缓慢的上升,而实际上在你的逻辑中并没有这么多的内存需求。至于手动解决内存泄漏,一般的方法是:

  1. 查询 newdelete,看看内存的申请与释放是不是成对释放的
  2. 如果依旧发生内存泄露,可以通过记录申请与释放的对象数目是否一致来判断。在类中追加一个静态变量 static int count,在构造函数中执行 count++,在析构函数中执行 count--,通过在程序结束前将所有类析构,之后输出静态变量,看 count 的值是否为 0。
  3. 检查类中申请的空间是否完全释放,尤其是存在继承父类的情况,看看子类中是否调用了父类的析构函数,有可能会因为子类析构时没有释放父类中申请的内存空间,这里建议将父类的析构函数声明为虚函数,否则可能不会调用子类的析构函数。
  4. 对于函数中申请的临时空间,认真检查,是否存在提前跳出函数的地方没有释放内存。

参考

  1. 如何解决内存泄漏
  2. shared_ptr 指针释放
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章