[toc]

Resource Management

  • 如内存,互斥锁,数据库连接,网络sockets……
  • 重要的是,当你不再使用它了,应该将它还给系统。

13. Use objects to manage resources.

  • 获得资源后立刻放进管理对象(managing object) 内。“资源取得时机便是初始化时机” (Resource Acquisition Is Initialization; RAII) 。

  • 管理对象(managing object) 运用析构函数确保资源被释放。

为什么手动释放堆内存容易出错?

e.g:

Investment * createlnvestment();

void f ()
{
Investment* plnv = createlnvestment();

……

delete plnv;

return;

}

//若……中有return导致提前返回,则会导致资源泄露,且不易察觉

  • std::auto_ptr:已被弃用。由于其拷贝时原先的指针会指向null,致潜在的资源泄漏和行为不确定性。auto_ptr 被 C++11 中引入的 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
#include <memory>
#include <iostream>

class Test {
public:
Test() { std::cout << "Test Created\n"; }
~Test() { std::cout << "Test Destroyed\n"; }
void greet() { std::cout << "Hello, World!\n"; }
};

int main() {
{
std::unique_ptr<Test> ptr1(new Test());
//Test Created
ptr1->greet();
//Hello, World!
//Test Destroyed
} //离开作用域,自动删除其管理的对象,防止内存泄露



std::unique_ptr<Test> ptr2(new Test());
//Test Created
std::unique_ptr<Test> ptr3 = std::move(ptr2); //通过移动语义修改对象的所有权

if (!ptr2) {
std::cout << "ptr2 is now empty.\n"; //ptr2值为nullptr
//ptr2 is now empty.
}
if (!ptr3) {
std::cout << "ptr3 is now empty.\n";
//
}


ptr3->greet();
//Hello, World!
//Test Destroyed
return 0;
}

  • “引用计数智能指针”(Reference Counted Smart Pointer),即std::shared_ptr,现已加入std标准库豪华套餐!环状引用问题可用weak_ptr解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>

class Test {
public:
Test() { std::cout << "Test Created\n"; }
~Test() { std::cout << "Test Destroyed\n"; }
};

int main() {
std::shared_ptr<Test> ptr1 = std::make_shared<Test>();
{
std::shared_ptr<Test> ptr2 = ptr1; // 引用计数增加
std::cout << "Inside block: ptr1 and ptr2 are both pointing to the object.\n";
// 当 ptr2 离开作用域并被销毁,引用计数减少
}
std::cout << "Outside block: only ptr1 is pointing to the object.\n";
// 当 ptr1 也被销毁,引用计数为0,对象被删除
return 0;
}

请记住

  • 防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes 分别是std::shared _ptrunique_ptr 。前者通常是较佳选择,因为其copy 行为比较直观。

14. Think carefully about copying behavior in resource-managing classes

对于RAII对象的复制,我们在大多数时候有一下两种可能:

  1. 禁止复制,如数据库连接具有唯一性的特性,类中堆内存可能因执行多次delete导致程序发生不明确行为。
  2. 对底层资源祭出“引用计数法”

若当引用次数为0时,我们还想要做其他行为。那么则可以为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
#include <iostream>

class Entity
{
public:
void log()
{
std::cout << "Log Function" << std::endl;
}
Entity()
{
std::cout << "Constructor!" << std::endl;
}
~Entity()
{
std::cout << "Destroctor!" << std::endl;
}

static void deleter(Entity* ptr)
{
std::cout << "Deleter!" << std::endl;
delete ptr;
}

};

int main()
{
auto e = std::shared_ptr<Entity>(new Entity,Entity::deleter);
e->log();
return 0;
}
Constructor!
Log Function
Deleter!
Destroctor!
//调用顺序

请记住

  • 复制RAII 对象必须一并复制它所管理的资源,所以资源的copying 行为决定RAII 对象的copying 行为
  • 普遍而常见的RAII class copying 行为是:抑制copying施行引用计数法(reference counting)。不过其他行为也都可能被实现。

15. Provide access to raw resources in resource-managing classes

//TODO

请记住

16. Use the same form in corresponding uses of new and delete

  • newdelete实际是由operator newoperator delete函数实现的。
  • 如果你在new 表达式中使用[],必须在相应的delete 表达式中也使用[]。如果你在new 表达式中不使用[],一定不要在相应的delete 表达式中使用[]。

内存布局:

image-20240408171300509

我们最好尽量不要对数组形式做typedefdefine动作,而是采用vectortemplates代替它:

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>
typedef std::string Son[4]; //假设有四个儿子

int main()
{
std::string* Randolfluo = new Son;
Randolfluo[0] = "123";
Randolfluo[1] = "456";

delete Randolfluo; //应为delete[] Randolfluo;
return 0;
}

//我们使用valgrind来分析下:
cpptools valgrind --leak-check=yes ./a.out
User
==4172== Invalid free() / delete / delete[] / realloc()
==4172== at 0x484BB6F: operator delete(void*, unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==4172== by 0x1092A7: main (in /home/randolfluo/cpptools/a.out)
//在主函数,程序尝试释放一个无效的内存地址。
==4172== Address 0x4dd6c88 is 8 bytes inside a block of size 136 alloc'd
==4172== at 0x484A2F3: operator new[](unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==4172== by 0x109223: main (in /home/randolfluo/cpptools/a.out)
// 在主函数,程序尝试释放的地址位于一个大小为136字节的块内,而这个块是通过调用operator new[]进行分配的。
//可以看到,程序发生了内存泄露,原因是new和delete的操作符不匹配


//使用vector:
#include <iostream>
#include<vector>

typedef std::vector<std::string> Son;

int main()
{
Son son(4);
son.push_back("123");
son.push_back("456");
return 0;
}


typedefdefine的区别:

  • typedef是C++的关键字,用于为现有的类型定义一个新的名称(别名)。它在编译时处理,由编译器解释和应用。typedef只能用于类型的别名,不能用于定义常量或宏函数。

  • #define是C和C++中的预处理指令,用于定义宏。它在预处理阶段处理,即在编译之前,文本替换就已经发生了。#define可以用来定义常量、宏函数,或者类型的别名,

请记住

  • 如果你在new 表达式中使用[],必须在相应的delete 表达式中也使用[]。如果你在new 表达式中不使用[],一定不要在相应的delete 表达式中使用[]。

17. Store newed objects in smart pointers in standalone statements

  • 其实就是C++的编译器可以对参数的执行顺序进行改变——进行程序优化。JavaC#则会按照特定次序完成函数参数的核算。
1
2
3
4
5
6
7
8
9
10
11
12
13
int priority() ;		//优先级函数,构造函数是个explicit构造函数
void processWidget(std::shared_ptr<Widget> pw, int priority); //进行带优先级的处理

//理想调用顺序:调用priority->执行“new Widget”->std::shared_ptr

processWidget(new Widget, priority()); //因原始指针和智能指针不匹配,error!
processWidget (std::shared_ptr<Widget>(new Widget), priority()); //正确调用,但是会因为参数调用顺序而产生异常
//如:调用执行“new Widget”->priority->std::shared_ptr new返回的指针会丢失


std::shared_ptr<Widget> pw(new Widget); //在单独语句内以智能指针存储newed 所得对象。
processWidge (pw, priority()); //这个调用动作绝不至于造成泄漏。

  • 以上之所以行得通,因为编译器对于“跨越语句的各项操作“没有重新排列的自由(只有在语句内它才拥有那个自由度)。

请记住

  • 以独立语句将newed 对象存储千(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。