首页 > 程序开发 > 软件开发 > C++ >

Effective C++读书笔记(10)

2012-02-04

条款16:成对使用new和delete时要采取相同形式Use the same form in corresponding uses ofnew and delete当你对一个指针使用 delete,唯一能够让delete知道内存中是否存在一个数组大小记录的方法就是...

条款16:成对使用new和delete时要采取相同形式

Use the same form in corresponding uses ofnew and delete

当你对一个指针使用 delete,唯一能够让delete知道内存中是否存在一个“数组大小记录”的方法就是由你来告诉它。如果你在使用的 delete 中加入了方括号,delete 就认定那个指针指向一个数组。否则,就认定指向一个单一的对象。

std::string *stringPtr1 = new std::string;

std::string *stringPtr2 = newstd::string[100];

...

delete stringPtr1;

delete [] stringPtr2;

对 stringPtr1 使用了delete []形式和对 stringPtr2 没有使用delete []形式都会发生令人不愉快的未定义行为。

规则很简单:如果你在 new 表达式中使用了[],你也必须在相应的 delete表达式中使用[];如果你在 new 表达式中没有使用 [],在匹配的delete 表达式中也不要使用[]。

当你写的一个类中包含一个指向动态分配的内存的指针,而且提供了多个构造函数的时候,这条规则尤其重要,因为那时你必须小心地在所有的构造函数中使用相同形式的new初始化那个指针成员。否则你怎么知道在析构函数中应该使用哪种形式的delete呢?

这个规则对于喜好typedef的人也很值得注目,因为这意味着一个 typedef 的作者必须说清楚,当用new创建一个 typedef 类型的对象时,应该使用哪种形式的delete。例如,考虑以下typedef:

typedef std::string AddressLines[4]; // 每个人的地址有4行,每行是一个string

std::string *pal = new AddressLines;

// 注意"new AddressLines"返回一个string*,就像“new string[4]”一样

delete pal; // 行为未有定义!

delete [] pal; // fine

最好尽量不要对数组形式做typedef动作。这很容易达成,因为C++标准程序库包含 string 和 vector,而且那些模板将对动态分配数组的需要减少到几乎为零。例如,这里,AddressLines可以被定义为一个string的vector,也就是说,类型为 vector<string>。

· 如果你在 new表达式中使用了[],你必须在对应的 delete表达式中使用[]。如果你在new表达式中没有使用[],你也不必在对应的 delete表达式中不使用[]。

条款17:以独立语句将newed对象置入智能指针

Store newed objects in smart pointers instandalone statements

假设我们有一个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:

int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

不要忘记使用对象管理资源的至理名言,processWidget为处理动态分配的 Widget使用了一个智能指针(在此采用tr1::shared_ptr)。现在考虑一个对processWidget 的调用:

processWidget(new Widget, priority());

以上调用不能通过编译。tr1::shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,所以不能从一个由"new Widget"返回的原始指针隐式转换到 processWidget 所需要的 tr1::shared_ptr。如果写成这样就可以通过编译:

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

令人惊讶的是,尽管我们在这里使用了对象管理资源,这个调用还是可能泄漏资源。

在编译器能生成一个对 processWidget 的调用之前,必须首先核算即将被传递的各个实参。在调用 processWidget之前,编译器必须为这三件事情生成代码:

· 调用 priority。

· 执行 "new Widget"。

· 调用 tr1::shared_ptr 的构造函数。

C++ 编译器允许在一个相当大的范围内决定这三件事被完成的顺序(这里与 Java 和 C# 等语言的处理方式不同,那些语言里函数参数总是按照一个特定顺序被计算)。可以确定的是"new Widget"一定在 tr1::shared_ptr 构造函数被调用之前执行,因为这个表达式的结果要作为一个参数传递给 tr1::shared_ptr 的构造函数,但是 priority 的调用可以排在第一第二或第三个执行。如果编译器选择第二个执行它(或许能生成更有效率的代码),我们最终得到这样一个操作顺序:

1. 执行 "new Widget"。

2. 调用 priority。

3. 调用 tr1::shared_ptr 的构造函数。

现在如果对 priority 的调用引发一个异常将发生什么?在这种情况下,从 "new Widget" 返回的指针被丢失,因为它没有被存入我们期望能阻止资源泄漏的tr1::shared_ptr。由于一个异常可能插入“资源被创建(经由newWidget)”和“资源被转换为资源管理对象”两个时间点之间,所以调用processWidget可能会发生一次泄漏。

避免这类问题的方法很简单:使用分离语句,分别写出(1)创建Widget,(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget:

std::tr1::shared_ptr<Widget> pw(newWidget);

// 在单独语句内以智能指针存储newed所得对象

processWidget(pw, priority()); // 这个调用动作绝不至于造成泄漏

这样做之所以行得通,是因为编译器对于跨越语句的各项操作没有重新排列的自由(只有在语句内才拥有那个自由度)。"new Widget" 表达式以及tr1::shared_ptr构造函数调用这两个动作,与 priority的调用在不同的语句中,所以编译器不得在它们之间任意选择执行次序。

· 以独立语句中将 new 出来的对象存入智能指针。如果疏忽了这一点,当异常发生时,有可能导致难以察觉的资源泄漏。

摘自 pandawuwyj的专栏

相关文章
最新文章
热点推荐