1. 开场白
《Effective C++》这本书一直觉得有些难度,已经反复看了好几次了,每次看都能发现一些以前没有注意到的知识点。建议在看这本书前先看看《C++编程思想》或者是《C++ Primer》,另外,如果看过一些《设计模式》或者《敏捷设计开发》的会稍微好些,书中一些条款涉及了设计模式,虽然并不是在讲设计模式,但是一些设计模式中的思维模式影响了这些条款的内容。
最后,我不知道是我个人语文水平不行的原因,还是说侯捷大大翻译的原因。我总觉得这本书翻译的有些拗口,不像我以前读《STL源码剖析》时候那么有感觉,我后面也读了《More Effective C++》,也是侯捷大大翻译的,感觉理解起来也是很吃力,一些语句得停下来一点点的抠才知道他说的是什么意思。额,不提这些了,接下来就整理一些条款的心得体会。
2. 记录
条款1. 视C++为联邦语言
大意就是C++很难,但是很通用,学会了C++学其他语言都不吃力,要好好学C++。
条款2. 尽量以const,enum,line替换#define
首先说明#define,其作用就是做一些宏定义,这些宏定义在编译过程(更确切的说是预处理过程)中会进行宏展开,而所谓的宏展开就是“文本替换”,在linux下可以使用:gcc -c input.c -o output 查看一个宏展开后的结果,更详细可以翻阅《链接、装载与库》这本书,书中详细介绍了预处理过程。
既然#define定义的内容在编译期间会被宏展开,它好处很明显,可以让代码便于阅读,但是又不会带来函数调用的开销。那它有什么缺点:
- #define没有作用域的概念,也就是说namespace和class的作用域限定对于#define定义的宏是不起作用的,可以看下面的这个代码。
namespace Foo {
#define TEMP "hello world"
}
class Base {
#define NAME "Base"
};
int main() {
cout << NAME << TEMP << endl;
return 1;
}
- #define在预处理过程中处理,不利于调试。#define一旦宏展开,宏名称再也找不到了,那调试的时候并不会告诉我们是宏定义出现了错误。例如下面的这个例子,:
#define MAX(a, b) (a > b ? a : b)
int main() {
int a = 1, b = 1;
cout << MAX(++a, b) << endl;
return 1;
}
这里的++a被替换了两次,所以宏展开以后的结果是:(++a > b ? ++a : b),这样一看就知道有什么问题了,但是没有调试的时候却很不容易发现这种问题。
这样就很容易理解为什么希望用const、enum和inline替换#define了吧。
const和enum可以定义常量而且还可以先定这些常量的作用域范围。
用inline则可以定义函数式的宏,却不会引起上述这样的问题。
条款3. 尽可能使用const
首先需要熟悉这里的const会出现在哪些地方,const虽然总体来说是常量(不愿意被修改),但是在不同的位置中语义又存在一些细微的区别,下面列举了一些const出现的位置,在不同的位置出现的位置,其中关注几点:
- 它修饰的对象是谁?
- 它的作用域范围?
- 它的语义是什么?
在《C++编程思想》一书中对const进行了详细的描述,具体内容可以翻阅这本书,这里就不赘述了。
const int a = 1;
const static int b = 2;
class Base {
public:
const string &getName(const string &index) const {
const int tmp = 10;
}
const static string name = "Base";
const int idx;
const int * const foo;
};
使用const的理由有:
- 避免作为等式左值,虽然稍微熟悉点的程序员都会避免这么做,但是不排除在写条件语句的时候不小心将“==”错误的输入成了“=”,当然我也见过初学者直接将这个作为函数左值的情况出现。
- 告诉调用者以及自己,使用const修饰的这个对象希望被保护起来,我们不愿意其被修改。虽然在这些情况下不使用const也是可以的,但是问题就在于总有那么些人和自己捣乱,与其一次次的和他们说这里不能动,那里不能改,不如事先用const修饰一下,然后其用的时候就知道“哦,这里用const修饰了,我不应该去改它”。
但是const的保护并不“完美”,它没办法避免一些人别有用心的去修改它,感觉它就和互斥锁一样,仅仅只是大家见的“君子协议”,如果真的有人不遵守,那就没办法了。所以,在后面的条款28就告诉我们:既然我们没办法阻止一些人总希望绕过const来修改内部的值,我们就尽量不要将对象内部数据的引用返回给调用者,这样用private保护起来的对象属性,外部就无法获取到了。
条款4. 确定对象使用前已被初始化
在C++中,有几个术语的定义总容易被搞混,比如“声明”、“定义”、“初始化”,这些术语的定义有些精细,如果没有弄清楚这些词具体说的是什么,那看《C++编程思想》《Effective C++》这种书的时候就有些吃力,因为有许多地方作者精确描述了它们,但是我们却没有理解作者在说什么。
“初始化”这个词在不同的场景下的语义有些细微的区别:
- 如果对一个类对象说初始化,意思主要是指这个类已经“准备完毕”,可以正常工作,那这里的准备完毕就有很多含义了,比如tcp客户端的套接字的“准备完毕”--可能指的就是已经与服务器端完成链接,也已经完成服务器端的地址信息配置,也可能是指已经套接字已经申请完成,这里的初始化语义就很丰富了,总体来说,只要这个对象在后面的代码中可以正常调用了,那就是已经初始化完成了;
- 但是对一个对象内部的成员属性的初始化定义就十分精细:在成员初始化列表中完成初始化过程。
作者在条款4中,作者前后讨论的其实就是两类情况下的初始化:类对象实例内部成员属性的初始化;相互依赖的类对象间的初始化。
- 对于类对象内部的成员属性的初始化,第一个需要注意的就是构造函数内部的过程并不是”初始化“,初始化这时候已经完成,确切的初始化应该是在初始化成员列表中的初始化;第二个需要注意的就是,内置对象(int、char、long、double等)的初始化值是不明确的,根据具体的编译器相关,所以最好在初始化列表中进行初始化,对于引用,const变量只能在初始化列表中进行初始化,对于是一个类对象的成员属性,如果不希望调用默认构造函数,那也需要在初始化列表中初始化(为了保证效率);第三个需要注意的就是初始化的顺序问题,基类首先被初始化(如果存在的话,如果是多重继承,那先初始化虚继承,然后再依次初始化),然后就是依据声明的次序依次初始化,如果初始化列表中存在定义,则调用初始化列表中的方法进行初始化,如果没有在初始化列表中初始化,那对于基本类型就根据编译器而定,类对象则