Effective C++ 条款 02 - 04
条款 02:尽量以 const enum inline 代替 #define
1. #define 的一些毛病
#define 定义的内容不出现符号表中,编译器看不见,报错时难以追踪(用 const 就没有这个问题); #define 定义变量有可能会导致代码膨胀(用 const 就没有这个问题); #define 没有作用域的概念,自然也没有 private 等访问权限的概念,破坏了封装性(用 inline 就没有这个问题)。
2. 用 const 代替 #define:
注意点 1:当在头文件中定义 const 来代替 #define 时,需要注意别忘记将指针也定义成 const(const char *const str;
); 注意点 2:类内的 static const
类型的成员的声明和定义问题:1) 非 int 类型的,在类内声明,在类外定义并初始化; 2) int 类型的,只要不需要对这个成员取地址,就可以直接在类内声明的时候给定一个值而不需要再次在类外定义(有一些编译器要求必须对 static 类型的值在类外定义,这种情况下,对于 static const int
,依然可以在类内声明的时候给值,但是需要在类外再定义一次,注意在类外定义的时候不能给值,因为 const 只能给一次值)。
class Clz {
private:
static const double dnum;
};
const double Clz::dnum = 0.12; // 除了 const int 类型的以外,任何 static 变量都需要在类内声明,在类外定义并赋初始值。
class GamePlayer {
pivate:
static const int numTurns = 5; // static const int 可以在声明式中给值,只要不用对这个变量取地址,就不需要在类外再定义一次了。
int scores[numTurns];
};
const int GamePlayer::numTurns; // 个别的编译器还需要在类外再定义一次(定义中不能再赋值了)
3. 用 enum 代替 #define:
使用场景:旧式编译器绝不允许在类内声明时给 static 类型的变量赋值,即使它同时是 const int 类型,然而类中的一些成员必须要拥有一个类内的常量值,例如类中定义的数组必须有一个 const int 的数组大小。这时可以使用 enum hack
来完成这种 “in class 初值设定”。示例:
class GamePlayer {
private:
enum { numTurns = 5 };
int scores[numTurns];
}
4. 用 inline 代替 #define
宏函数的问题太多了,比如二义性,尽量不要用它,用 template inline
代替。例如:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
可以用下面的方式来改写:
template<typename T>
inline void CALL_WITH_MAX(const T& a, const T& b) {
f(a > b ? a : b);
}
条款 03:尽可能使用 const
1. 迭代器和 const
const std::vector<int>::iterator
:迭代器是常量,迭代器不可更改,类似于 * const p
std::vector<int>::const_iterator
:迭代器指向的 int 是常量,迭代器可以更改,不可以用它更改它指向的 int,类似于 const * p
2. 返回值类型设为 const 有什么用?
class Clz {
friend Clz operator+(const Clz& clz1, const Clz& clz2); // 没有用 const 作为返回值
}
int main() {
Clz clz1, clz2, clz3;
if (clz1 + clz2 = clz3) { ... } // 实际上是想要用 == 号,但是误用了 = 号;
// 如果 operator+ 的返回类型是 const 的,编译器就会可以自动帮我们找到这个错误;
// “尽量与内置类型保持一致” 的原则。
}
3. const 成员函数
为什么要有 const 成员函数:
让接口更清晰直观,告诉用户 “这些函数是不改变成员的函数”; 让 const 对象也能调用类的函数,从而实现 pass-by-reference-to-const
。
bitwise constness
和 logical constness
:
bitwise constness
:完全从编译器的角度出发的 constness,可能会出现一些违反直觉的行为。例如,如果类中有一个指针类型的成员,也许我们实际想要的是不能修改指针所指向的值,但是对编译器来说,指针指向的值并不属于这个类,只有指针才属于这个类,所以即使我们修改了指针指向的值,编译也能通过。logical constness
:从用户逻辑的角度出发,可以使用 mutable
通知编译器让一些非 const 的成员可以被 const 函数修改。用 const 修饰成员函数的一个作用是 “为了让接口更直观明朗”,例如取 size 操作在用户看来就应该是 const 的,但是如果写成一个缓存的形式,那么在取 size 操作中会改变 size 的值,这时就可以使用 const + mutable
的形式。示例:
class TextBlock {
public:
size_t getSize() const; // getSize 用户接口
private:
mutable bool isSizeValid; // mutable
mutable size_t size; // mutable
size_t doGetSize() const; // 真正的 getSize
}
size_t TextBlock::getSize() const {
if (isSizeValid == false) {
isSizeValid = true; // 由于 isSizeValid 和 size 是 mutable,所以可以在 const 函数中改变它们的值
size = doGetSize();
}
return size;
}
重载:在 non-const 成员函数中调用 const 成员函数:
作用:减少代码重复。 注意事项:不要在 const 成员函数中调用重载的 non-const 成员函数,因为去掉 const 对象的 const 是一个很危险的行为。
class Clz {
public:
const int& func() const { return i; }
int& func() {
return const_cast<int&>( ( static_cast<const Clz>(*this) ).func() ) );
// 用 static_cast 将 this 转变为 const;
// 此时调用 func 会调用 const 版本的 func;
// 返回 const int& 类型的值;
// 用 const_cast 去掉返回值的 const 约束。
}
private:
int i;
}
条款 04:确定对象在被使用前已先被初始化
1. 成员初始化列表
类的自定义类型成员变量会调用该变量的默认构造函数初始化,而类的内置类型成员不会自动初始化(未定义行为)。 注意不要混淆初始化和赋值。类的构造函数在进入函数体之前已经在初始化列表处完成了初始化动作,如果没有提供初始化列表,那么实际上进行了一个隐含的初始化过程,函数体内的是赋值而不是初始化。 对于类的自定义类型的成员来说,先默认初始化再赋值总是比直接用值初始化的开销要大的,因此最好使用初始化列表。 对于类的内置类型的成员来说,初始化和赋值的开销基本相同。然而,为了保持一致性,最好也使用初始化列表。 即使是对于内置类型成员,也有一些情况下必须使用初始化列表:如果成员变量是 const 或引用,必须用初始化列表初始化
。 如果类有多个构造函数,并且有一些重复的 “赋值表现得像初始化一样好” 的初始化过程,可以将这些赋值写在一个 private 函数里,然后在多个构造函数体中调用这个 private 函数。这种方式一般用于 成员变量的值由文件或数据库读入
的情况。
2. 成员初始化次序
基类的早于派生类的 类内的成员按照声明的顺序进行初始化
3. 成员初始化次序的一种不明确情况
成员初始化次序的一种不明确情况:不同编译单元内的非局部静态对象的初始化顺序是不确定的
非局部静态对象:静态对象包括——全局对象、namespace 内的对象、class 内的 static 对象、函数内的 static 对象。其中除了函数内的 static 对象以外的静态对象都属于非局部静态对象
。 解释:例如在两个编译单元内各存在一个类 A 和 类 B,其中 类 A 的构造函数中对类 B 对象进行了调用(即,使用到了另一个类的对象)。在这两个编译单元的各自全局作用域中内分别定义对象 A 和对象 B。但是我们无法保证在对象 A 的构造函数调用之前对象 B 已经定义完成了,因为这两个对象都是非局部静态对象,初始化顺序是不确定的。 如何解决这个问题?—— reference-returning function
,即,不直接使用非局部静态对象,而是把它封装在一个专属的函数内。在需要使用这个对象的地方,调用这个函数,这个函数内部会定义并初始化对象,并将这个对象的引用作为返回值返回。这一设计的理论基础就是 函数内的局部静态对象会在该函数被调用期间首次遇到这个对象时被初始化
,保证了函数返回的引用的对象一定已经被初始化了。实际上这就是单例模式的常见实现手法
。 reference-returning function 往往目的十分单纯:两行,第一行定义并初始化一个对象,第二行返回一个引用。因此它十分适合定义成 inline
函数,尤其在它可能会被频繁调用的时候。 reference-returning function 会有 多线程问题
,一个解决方案就是,在程序的单线程启动期间,手动调用所有 reference-returning function (就是饿汉模式吧)。
代码示例(错误版本):
// 编译单元 1
class ClzA {
public:
void func() { ... }
}
ClzA ca;
// 编译单元 2
class ClzB {
public:
ClzB() { ca.func(); }
}
ClzB cb; // 在 cb 构造时,会用到 ca 对象来调用 func(),而这时 ca 对象可能还没被初始化呢。。
代码示例(正确版本):
// 编译单元 1
class ClzA {
public:
void func() { ... }
}
ClzA& getCA() {
static ClzA ca;
return ca;
}
// 编译单元 2
class ClzB {
public:
ClzB() { getCA().func(); } // 这时不是用 ca 对象进行调用,
// 而是用专属函数 getCA() 获得 ca 对象后再进行调用,
// 可以保证 ca 此时一定已经被初始化了。
}
ClzB& getCB() {
static ClzB cb;
return cb;
}