Effective C++ 3nd 笔记——让自己习惯C++
尽量以const,enum,inline替换#define
宏定义在预处理阶段就进行了字符串替换,于是你的宏名有可能没进入符号表;
对于浮点数 ( f l o a t ) (float) (float)来说,使用const比使用宏定义可以导致较小的代码量,因为预处理器盲目的进行宏替换,这可能导致在程序中出现多份目标码。而用const则不会
变量的声明就是将变量的别名告诉编译器,并不带有细节信息
enum hack的好处:可以在某些不支持类数据成员在类内初始化的编译器(一般比较老旧)的情况下对数据成员进行类内初始化(其理论基础就是会把它作为一个枚举常量);可以防止用户取得某些数据的地址(#define也可以);不会导致非必要的内存分配
请记住:
- 对于单纯常量,最好以const对象或enums替换#define
- 对于形似函数的宏,最好改用inline函数替换#define
尽可能使用const
声明STL迭代器为const就像声明指针为const一样,即 T* const;如果想声明为 const T*,则需要使用const_iterator
const最具威力的用法是面对函数声明时的应用,令函数返回一个常量值,往往可以降低因客户错误而造成的以外,而又不至于放弃安全性和高效性
对于不需要改动的参数或本地对象,最好声明为const,这可以避免很多烦恼
成员函数如果是const意味着什么?这有两个流派概念:bitwise constness(physical constness)和logical constness
前者是成员函数不更改对象内的任何一个bit,这也是C++对常量性的定义。但有许多成员函数不具备十足的const性,但也能通过bitwise测试。更具体的说,一个更改了 “指针所有物” 的成员函数虽然不能算是const,但如果只有指针隶属于对象,那么此函数不会引发bitwise编译器错误,考虑以下代码:
class CTextBlock{
public:
...
char &operator[](std::size_t position) const // bitwise 声明,但其实不当
{ return pText[position]; }
private:
char *pText;
};
// 这个 class 不适当的将其 operator[]声明为 const 成员函数,而该函数返却返回一个
// reference 指向对象内部值,则产生以下情况
const CTextBlock cctb("Hello"); // 声明一个常量对象
char *pc = &cctb[0]; // 调用[]取得一个指针,指向cctb的数据
*pc = 'J'; // 现在cctb = "Jello",虽然cctb为const,但实际上我们还是改变了它的值
这种情况就导出了logical constness。主张一个const成员函数可以修改它处理的对象内的某些bit,但只有在客户端侦测不出的情况下才能如此
使用mutable可以释放掉non-static成员变量的bitwise constness约束
可以使用类型转换避免const和non-const成员函数的代码重复,可以使用const_cast将原const成员函数的const性质移除,但要考虑代码的安全情况;non-const函数可以调用const函数,反之则不行
请记住:
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
- 编译器强制实施bitwise constness,但你编写程序时应该使用 “概念上的常量性”
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
确定对象被使用前已先被初始化
首先我们要分清楚赋值和初始化,考虑以下代码:
class PhoneNumber{ ... }; // 随便声明一个类
class ABEntry{
public:
// 定义类的构造函数
ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones){}
private:
std::string thename;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones){
// 这些都是赋值,而非初始化
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
这样虽然可以让ABEntry里的成员变量带有你期望的值,但这并不是最佳做法。C++规定,对象的初始化发生在进入构造函数本体之前,在ABEntry构造函数类,这些成员函数都不是被初始化,而是被赋值。
ABEntry构造函数的一个较佳写法是通过成员初始化列表(member initialization list)替换赋值动作
使用成员初始化列表的结果和上面的结果相同,但效率更高。上面基于赋值的版本,首先会调用默认构造函数为成员变量设初值,然后再立刻对它们附上你所期望的值。这样的话,构造函数所做的一切就浪费了。使用成员初始化列表可以避免这一问题,因为初始化列表中针对各个成员变量而设的实参,被拿去作为各成员变量的构造函数的实参
但需要注意的是,对于内置类型成员(int, double, float…)比如ABEntry类中的numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化列表来进行初始化;如果成员变量是const或references,就一定需要初值,不能被赋值。顺带一提,成员变量的初始化次序只和其的声明次序有关,与在初始化列表中的次序无关。
解决完上面的问题之后,还剩下最后一个问题:不同编译单元内定义的non-local static对象的初始化次序
什么是编译单元:所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码加上其所含入的头文件
这个问题至少涉及到两个源码文件。真正的问题是:
如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为**C++对 “定义于不同编译单元内的non-local static对象” 的初始化次序无明确定义*。考虑以下代码:
// ---------------------------------文件1---------------------------------------
class FileSystem{ // 来自你的程序库
public:
...
std::size_t numDisks() const; // 众多成员函数之一
...
};
extern FileSystem tfs; // 定义一个FileSystem对象,用extern修饰表示其可以在别处引用
// ---------------------------------文件2---------------------------------------
class Directory{
public:
Directory( params ); // 构造函数
...
};
Directory::Drectory( parmas ){
...
std::size_t disks = tfs.numDisks(); // 使用tfs对象
...
};
Directory tempDir( parmas ); // 创建一个Directory对象
此时就会出现问题,除非 tfs 在 tempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs。但 tfs 和 tempDir是不同的人在不同的时间在不同的源码文件中建立的,如何保证 tfs 会先于 tempDir 初始化呢?答案是:无解
多个编译单元内的non-local static对象经由 “模板隐式具现化,implicit template instantiations”形成,不但不可能决定正确的初始化次序,甚至往往不值得寻找 “可决定正确次序” 的特殊情况
幸运的是一个小小的设计便可以完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是 S i n g l e t o n Singleton Singleton(单例设计)模式的一个常见手法
这个手法的基础在于:C++保证,函数内的local static对象会在 “该函数被调用期间,首次遇上该对象的定义” 时被初始化。所以如果你以函数调用替换直接访问non-local static对象,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。作用于上述代码有以下效果:
// ---------------------------------文件1---------------------------------------
class FileSystem{ ... }; // 同前
FileSystem &tfs(){ // 用函数来替换tfs对象
static FileSystem fs;
return fs;
}
// ---------------------------------文件2---------------------------------------
class Directory{ ... }; // 同前
Directory::Directory( parmas ){
...
std:::size_t disks = tfs().numDisks();
...
}
Directory &tempDir(){ // 用来替换tempDir对象
static Directory td;
return td;
}
但是从另一个角度看,这些函数 “内含static” 对象的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下 “等待某事发生” 都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的 “竞速形式”。
运用reference-returning函数防止 “初始化次序问题”,前提是其中有着一个对对象而言合理的初始化次序
请记住:
- 为内置型对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序一致
- 为免除 “跨编译单元的初始化次序” 问题,请以local static对象替换non-local static对象