Effective C++ 读书笔记

从北京回来,决定花三天时间把这本书看掉。有两个原因,第一个是我希望主要精通的语言还是选择C++,因为其功能强大,且速度快;优秀的程序员就应该精通C++才可以。其次是在即将要学习的机器学习和深度学习的领域中,C++是后期算法工程化的重要一环。

如果是看完这本书,我觉得三天就够了。如果是精研每个细节就要花个一周了,我决定先快速的get到重点的内容,在接下来写代码的过程中再实践出来。

本书架构

这本书被冠以盛名,源自于其内容的独特性、优质性。
书中不是详细介绍C++的各种特性,而是从大量的工程经验中思考c++编写代码的优秀习惯。

本书全局来看,由55条准则构成。这55条准则被由浅入深的分成了8个模块,还有一个杂项模块作为第9个模块。
下面我就这些准则和书中的内容做个整理。

1. 让自己习惯C++

View C++ as a federation of languages

C++ 今天已经是一个多重范型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。
因此这是一个语言的联邦,几乎各种语言的编程模式都可以在C++中得到体现。
最主要的是,做到这些并且保持着速度。

Prefer const, enum, inline to #define

主要就是讲了#define的种种缺点。
从我自身的使用上来说,简短的单文件程序中,使用define还是可以的,但是看别人的代码中就非常的困难了。因此我自己也是不倾向于使用define来定义一些东西。

常量
对于常量我们可以使用const来定义,而不是使用define。这样不仅安全,而且编译后的代码更少。
define的一个比较严重的问题就是作用域的问题,其并不重视作用域,在编译的时候,对此后的内容均有效,除非遇到undefine。但是一个常量是可以有作用域的限制的。
除了使用const还可以用enum来定义一个常量,

1
2
3
4
class Game{
private:
enum{ NumTurns=5 };
}

这样就定义了一个5的代名词,但是并不会被取到地址,也不会被改变。如果使用者企图调用这个的地址会在编译时就被报错,而不是像const那样在运行时报错出来。

函数
很多人会define一个小函数,来达到函数的作用又减小了函数调用的开销。但是这样写,一个是不直观,容易错;还有就是括号要很多,也容易出错。用inline是一样的效果。

Use const whenever possible

关于const的语法的细节,可以参考之前的相关的内容。
使用const可以避免很多错误,与上面一样,这些错误很可能会发生在运行时。但是如果你适当的用了const,就会在编译的时候被避免。

1
2
CLASS a, b ,c;
if( a*b = c)

内置类型会编译报错没有问题,但是对于自定义的类型,就会出现意想不到的错误。

确保使用的变量都被初始化了

这里有一个很有趣的东西,叫做冒号表达式,没错,就是那个冒号表达式。与一般的赋值初始化不同的是,冒号表达式更快速。

1
2
3
4
5
6
7
8
class A{
Som a, b;
A(Som _a, Som _b){
a = _a;
b = _b;
}
A(Som _a, Som _b):a(_a), b(_b){}
}

对于第一种写法,需要先调用每个类的默认构造函数,随后进行复值。但是默认构造函数就浪费了。
对于第二种,直接调用其copy构造函数,所以更快。

然后书中讨论了初始化顺序的问题,在类中的成员是严格按照其定义的顺序进行初始化的,即使你在初始化序列中写的顺序与定义顺序不一致也没有关系。
但是在一些全局的non-local static 变量中,其初始化的顺序是绝对不可预知的,也就是一些你定义在全局,namespace中的静态变量,其初始化的顺序是完全不确定的。
这会引发严重的灾难,会导致你使用到没有初始化的对象。
解决的办法就是不用non-local static变量。将这些变量的定义转移到函数中去,你甚至可以专门写一个函数用来给这个变量初始化,可以保证的是,函数在没有被调用的时候是不会被初始化的,并且在首次调用的时候一定会初始化。这样就可以保证得到的对象是经过初始化的了。
这样做的后果,突出的就是表现为使用函数来访问一个静态变量而原来是直接用变量本身访问。

Constructs, Destructors, and Assignment Operators

Know what functions C++ silently writes and calls

编译器会帮你写哪些函数:

1
2
3
4
5
6
7
8
9
class Empty{
public :
Empty(){}
Empty(const Empty& rhs){}
~Empty(){}

Empty& operator=(const Empty& rhs){}

}

上面这四个函数是默认的,但是如果你写了一个构造函数以后,这个默认的构造函数就不会帮你写了,需要你自己写默认构造。

Exolicitly disallow the use of compiler-generated function you do not want.

如何避免编译器帮你自动生成一些你不想要的函数,自己声明一个,不要定义,然后private起来。

Declare destructors virtual in polymorphic base classes.

这是因为会产生局部销毁的后果而出现内存泄漏。

这是因为,在多态继承的类中,我们可以用一个基类的指针来操作一个子类,当我要进行析构的时候,我们会delete pt (pt is a pointer to base class), 但是如果基类不是个虚函数的析构函数,就会造成局部的析构,产生内存泄漏。

如果你的类是个多态基类,就要有个virtual的析构函数,否则不要生命析构函数。

Prevent exception from leaving destructors.

对于一个C++程序来说,如果有两个异常同时报出来就有可能导致程序发生不明确的行为或是终止,因此,我们一定要捕获这样的异常。
如果在析构函数中产生了一个异常,这非常的尴尬,因为用户无法获知这个问题,从而无法处理,并且导致未明确的行为。
一个比较好的设计就是将可能出现一场的操作放到一个函数中,让用户显示的可以调用并处理异常,同时在析构函数中检验是否已经处理完,如果此时没有处理,只好在析构函数中调用这个函数并吞下这个异常。

Never call virtual function during construction or destruction

因为基类的初始化的问题,在初始化的时候,虚函数可能还没有定义,因此是不会被调用到子类的函数中的。

让赋值操作符返回一个*this 的引用

在operator= 中处理自我赋值

在赋值的时候很容易会出现赋值给自己的请况,此时请注意在赋值的时候处理好释放空间的时机,以保证不会出现野指针等问题。

Copy all parts of an object

在复制一个对象的时候,要保证复制了每一个对像并调用了每个成员的该调用的函数。

Resource Management

程序使用了一些资源,在不用的时候要及时的还给系统,这些资源包括:内存、文件描述器、互斥锁、图形界面的字形、笔刷、数据库链接、sockets等。

Use object to manage resources

这个习惯要求我们把释放资源的任务交给析构函数来做,这样当对象析构的时候自动释放资源。此外,文中提到使用auto_ptr智能指针。
智能指针保证只有一个智能指针指向一个资源,在赋值发生的时候,原指针会被置为空。
使用智能指针是为了防止你多个指针指向一个对象的时候,被其中一个释放,随后其他指针就变成了野指针。

在资源管理类中小心copy行为

Talk is not cheap.