条款2:尽量以const,enmu,inline替换#define

1.使用#define的缺点1:#define的原理是替换,也就是属于编辑范畴。程序开发者无法直接看到编辑后的代码,因此当#define替换引起编译错误时可能会给开发者带来困惑,如果#define定义的位置在其它人写的文件中,那就更让人摸不着头脑了。

2.使用#define的缺点2:#define不提供封装性,#define不重视作用域(除非在某处被#undef)。

3.使用#define的缺点3:可能出现不安全或不可预料的结果。例如在#define中引入“++a”这样的表达式而被该#define在同一条语句中替换产生而出现多处“++a”时。对于这类形似函数的宏,应该使用inline函数替换#define。

 

条款3:尽可能使用const

1.指针情形const修饰语义辨析:如果关键词const出现在星号左边,表示修饰被指物是常量,如果const出现在星号右边,表示指针自身是常量。

1
2
3
char str[] = "Hello";
const char* p = str; //不能通过p修改字符串,但p可以指向其它字符串
char* const p = str; //p只能指向str,但可以通过修改str的内容

2.const好处:允许程序员告诉编译器和其他程序员某值应该保持不变,只要“某值保持不变”这件事是事实,就应该说出来,因为说出来可以获得编译器的襄助。

3.对于使用mutable修饰的变量,即使用const修饰的函数也能修改其值。

 

条款4:确定对象被使用前已被初始化

1.“初始化”与“赋值”容易混淆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A
{
public:
A(const std::string& str, const std::list<int>& lst);
private:
std::string theString;
std::list<int> theList;
int num;
};
/*构造函数*/
A::A(const std::string& str, const std::list<int>& lst)
{
theString = str;
theList = lst;
num = 0;
}

注意!该构造函数内的的theString、theList都不是被初始化,而是被赋值。初始化发生的时间更早,发生于这些成员的default构造函数被自动调用之时。

其实,该构造函数对theString、theList进行了两步操作:1.分别使用string和list类的默认构造函数对theString和theList进行构造。2.分别对theString和theList进行赋值(使用copy assignment操作符)。可见,这是很大的浪费。

更好的方式是使用初始化列表替换赋值动作(单次调用copy构造函数更高效):

1
2
3
4
5
/*构造函数*/
A::A(const std::string& str, const std::list<int>& lst):theString(str),theList(lst)
{
num = 0;
}

2.在同一个编译单元中,C++有十分固定的成员初始化次序:先基类后派生类,每个类的成员变量总是以声明次序被初始化(即使初始化列表中颠倒了次序)。(注:一个编译单元等于一份产出单一目标文件的源码)

3.由于C++对“定义于不同便一单元内的non-local static对象”的初始化次序没有明确定义。可能引发的问题是:某个编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,被用到的这个对象可能尚未被初始化。

用local static对象替换而避免使用non-local static对象可以解决这一问题。

(注:关于non-local static对象和local static对象的概念见C++中local static对象与non-local static对象的概念与区别

 

条款5:了解C++默认编写并调用哪些函数

1.一个empty class经过C++处理过后,编译器就会为它声明一个copy构造函数、一个copy assignment操作符、一个析构函数,且所有这些函数都是public和inline的。也即如果写下代码class Empty{};好像写下代码:

1
2
3
4
5
6
7
8
class Empty{
public:
Empty(){...};
Empty(const Empty& rhs){...} //编译器自动创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象
~Empty(){...} //注意:自动产生的析构函数是non-virtual的!除非Empty的基类的自身声明中有virtual析构函数
Empty& operator=(const Empty& rhs){...}
};

唯有这些函数被需要(被调用)时,它们才会被编译器创建出来

2.如果在类中声明了一个构造函数,编译器就不再为它创建default构造函数。

3.编译器自动创建的copy构造函数对内置类型的处理是“拷贝每一个bit”(包括指针)。

4.如果想要使一个内含reference成员、const成员的class支持赋值操作,必须自己定义copy assignment操作符,编译器不会自动提供。(因为reference对象、const对象不支持一般的赋值操作,即使是在初始化时也必须使用初始化列表)

5.如果base classes将copyassignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。

 

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

如条款5所述,编译器会自动声明一个copy构造函数、一个copy assignment操作符、一个析构函数。但为了不希望某些人尝试调用它们,可以通过显式地将它们声明为private,例如将copy构造函数或copy assignment操作符声明为private。

1
2
3
4
5
6
7
8
class A
{
public:
A(){};
private:
A(const A&);
A& operator=(const A&);
};

 

条款7:为多态基类声明virtual析构函数

1.C++明白指出,当derived class对象由一个base class指针被删除,而如果该base class带着一个non-virtual析构函数,其结果未定义,实际执行时通常是derived成分没被销毁(只调用base类的析构函数),导致内存泄漏、破坏数据结构。而如果将base class中的析构函数定义为virtual的,此后删除derived class对象就会调用derived class的析构函数

2.但如果class并不意图被用作一个base class,将它的析构函数设定为virtual的话,也是一个馊主意。也即如果设计中class不含virtual函数,通常表示它不企图被当做base class。(许多人的心得是:class中至少有一个virtual函数,就也将析构函数声明为virtual)