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

Effective C++读书笔记(16)

2012-02-04

第15篇 http://www.2cto.com/kf/201202/118370.html条款26:尽可能延后变量定义式出现的时间Postpone variable definitions as long as possible只要你定义了一个带有构造函数和析构函数的类型变量,当控...

第15篇 http://www.2cto.com/kf/201202/118370.html

条款26:尽可能延后变量定义式出现的时间

Postpone variable definitions as long as possible

只要你定义了一个带有构造函数和析构函数的类型变量,当控制流程到达变量定义时,你会承受构造成本,而当变量离开作用域时,你会承受析构成本。如果有最终并未被使用的变量造成这一成本,你就要尽你所能去避免它。

不要认为自己不会定义一个不使用的变量。考虑下面这个函数,它计算通行密码的加密版本然后返回,前提是密码够长。如果密码太短,函数就会抛出一个定义在C++标准程序库中的logic_error类型异常:

// 这个函数过早定义变量encrypted
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length() <MinimumPasswordLength) {
throw logic_error("Passwordis too short");
}
... // 必要动作,将一个加密后的密码置入变量encrypted
return encrypted;
}

如果抛出了一个异常,对象encrypted在这个函数中就是无用的。换句话说,即使encryptPassword抛出一个异常,你也要为构造和析构encrypted付出代价。因此你最好将encrypted的定义推迟到你确信你真的需要它的时候,即判断是否会抛出异常之后。

这还不够,因为定义encrypted的时候没有任何初始化参数。这就意味着很多情况下将使用它的缺省构造函数。缺省构造一个对象然后赋值比用你真正需要它持有的值初始化它效率更低。例如,假设encryptPassword的核心部分是在这个函数中完成的:

voidencrypt(std::string& s); // 在其中的适当地点对s加密

那么,encryptPassword可实现如下,即使它还不是最好的方法:

std::stringencryptPassword(const std::string& password)
{
... // 检查length,如前
std::string encrypted; // default-construct encrypted
encrypted = password; // 赋值给encrypted
encrypt(encrypted);
return encrypted;
}

更可取的方法是用password初始化encrypted,从而跳过毫无意义并可能很昂贵的缺省构造:

std::stringencrypted(password); // 通过copy构造函数定义并初始化

这就是本条款的标题的真正含义。你不仅应该推迟一个变量的定义直到你不得不用它的最后一刻,而且应该试图推迟它的定义直到得到了它的初始化参数。通过这样的做法,你可以避免构造和析构无用对象,而且还可以避免不必要的缺省构造。



对于循环,如果一个变量仅仅在一个循环内使用,是循环外面定义它并在每次循环迭代时赋值给它更好,还是在循环内部定义这个变量更好?

//Approach A: 定义于循环外
Widget w;
for (int i = 0; i < n; ++i){
w = 取决于i的某个值;
...
}

// Approach B: 定义于循环内
for (int i = 0; i < n; ++i) {
Widget w(取决于i的某个值);
...
}

对于Widget的操作而言,就是下面这两个方法的成本:

· 方法A:1个构造函数+ 1个析构函数+ n个赋值。

· 方法B:n个构造函数+ n个析构函数。

对于那些赋值的成本低于一个构造函数/析构函数对的成本的类,方法A通常更高效,特别是在n变得很大的情况下。否则,方法B可能更好一些。此外,方法A与方法B相比,使得名字w 在一个较大的区域(包含循环的那个区域)内均可见,这可能会破坏程序的易理解性和可维护性。因此得出结论,除非你确信以下两点:(1)赋值比构造函数/析构函数对成本更低,(2)你正在涉及你代码中性能敏感的部分,否则你应该默认使用方法B。

· 尽可能延后变量定义式的出现。这样可以增加程序的清晰度并提高程序的性能。



条款27:尽量少做转型动作(1)

Minimize casting

强制转型破坏了类型系统。它会引起各种各样的麻烦,其中一些容易被察觉,另一些则格外地隐晦。强制转型在C,C#和Java中比在C++中更有必要,危险也更少。在C++语言中,强制转型是一个必须全神贯注的特性。

C风格(C-style)强制转型如下:

(T) expression // 将expression转型为T

函数风格(Function-style)强制转型使用这样的语法:

T(expression) // 将expression转型为T

这两种形式之间没有本质不同,纯粹就是一个把括号放在哪的问题。这两种形式被称为旧风格(old-style)的强制转型。

C++ 同时提供了四种新的强制转型形式(通常称为新风格的或C++风格的强制转型):

(因对强制转换不熟悉,且书上的解释自觉不够清晰,以下内容多摘自网络)

1)static_cast < type-id > ( expression )

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。编译器隐式执行任何类型转换都可由static_cast显示完成。它主要有如下几种用法:

  ①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换:

  进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;

  进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

  ②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  ③把空指针转换成目标类型的空指针。

  ④把任何类型的表达式转换成void类型。

注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。



2)dynamic_cast < type-id > ( expression )

该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void*;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;

在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。



3)const_cast < type_id > ( expression )

  该运算符用来修改类型的const或volatile属性。除了const或volatile修饰之外,type_id和expression的类型是一样的。

  ①常量指针被转化成非常量指针,并且仍然指向原来的对象;

  ②常量引用被转换成非常量引用,并且仍然指向原来的对象;

  ③常量对象被转换成非常量对象。

Volatile和const类似。

Volatile:同const、static一样,这是一个类型修饰符。一个使用volatile修饰的变量,比如volatileint i; 每次对该变量的直接引用,都会访问内存,而不是从寄存器中读取(如果其已经在寄存器中)。这样一来,volatile似乎没什么用处,反倒会使数据的读取相对变慢很多,如果没有volatile,编译器可能会优化你的程序,使得数据从寄存器中读取,从而加快程序的运行。但如果这个变量是同其它进程/线程共享的,就可能造成数据的不一致。多线程情况下,你可以使用互斥机制来保证对共享数据访问的原子性。但是,在单片机等嵌入式环境中,硬件经常不会有这种互斥机制的支持,这时某些共享的数据(比如端口)就可能会产生不一致的情况。而使用volatile就会使编译器不对代码进行优化,每次对该变量的访问都会从内存中读取。

4)reinterpret_cast < type_id > ( expression )

该操作符修改了操作数类型,但仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换。例如:

int *n= new int;

  double*d=reinterpret_cast<double*> (n);

  在进行计算以后, d 包含无用值. 这是因为reinterpret_cast仅仅是复制n比特位到d, 没有进行必要的分析。

这个关键词在我们需要把类型映射回原有类型时用到它。我们映射到的类型仅仅是为了故弄玄虚和其他目的,这是所有映射中最危险的。因此,需要谨慎使用reinterpret_cast.



旧风格的强制转型依然合法,但是新的形式更可取。首先,在代码中它们更容易识别,简化了在代码中寻找类型系统被破坏的地方的过程。第二,更精确地指定每一个强制转型的目的,使得编译器诊断使用错误成为可能。例如,如果你试图将常量性去掉,除非使用新式转换的const_cast,否则无法通过编译。

唯一使用旧式转换的时机是当我要调用一个explicit构造函数传递一个对象给一个函数的时候,例如:

class Widget {
public:
explicit Widget(int size);
...
};

void doSomeWork(const Widget& w);

doSomeWork(Widget(15)); // 以int加上函数风格的转型动作创建Widget

doSomeWork(static_cast<Widget>(15)); //以int加上C++风格的转型动作创建Widget


摘自 pandawuwyj的专栏
相关文章
最新文章
热点推荐