条款01:视C++为一个语言联邦
View c++ as a federation of languages
c++并不是一个带有一组守则的一体语言,它是由四个次语言组成的联邦政府(C, Object-Oriented C++, Template C++, STL),每个次语言都有自己的规约。
当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略时,不要感到惊讶。例如对内置类型而言pass-by-value通常比pass-by-reference更高效。但你从C part of C++迁往Object-Oriented C++,pass-by-reference-to-const往往更好。
请记住:C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。
条款02:尽量以const, enum, inline替换#define
Prefer consts, enums, and inlines to #defines
另一种解释是“用编译器替换预处理器比较好”,因为或许#define不被视为语言的一部分。
当你 #define ASPECT_PATIO 1.653。也许ASPECT_PATIO从未被编译器看见(可能在编译器处理源码之前就被预处理器替换了),从而没有进入记号表(symbol table)。这样可能的问题会有:
- 如果ASPECT_PATIO被定义在一个非自己写的头文件中,你运用此常量得到了一个编译错误信息时,你可能会疑惑1.653是什么,从而浪费时间。
- 记号器调试器可能也会出现相同问题(ASPECT_PATIO没有进入记号表)
解决方法是用一个常量替换上述宏,并有以下好处:
const double AspectRatio = 1.653
- AspectRatio作为一个常量肯定被编译器看到,从而进入记号表。
- 对于浮点常量而言,使用常量可能比#define导致较小量的码。预处理器盲目的将多处宏都替换为1.653。
这里有个需要注意的点是:class专属常量
class GamePlayer{
private:
static const int NumTurns = 5; //常量声明
int scores[NumTurns]; //使用常量
}
通常C++要求对所使用的任何东西提供一个定义式,但class专属常量且为static和整型(int, char, bool)则可以特殊处理。你可以声明并使用它们而无需提供定义式。
const int GamePlayer::NumTurns; //定义式,由于class常量在声明式获得初值,定义时就不可再设初值
Note:无法用#define创建一个class专属常量,因为#define不重视作用域。一旦宏被定义,它在其后的编译过程中均有效。
如果你的编译器不允许“static整数型class常量”完成“in class初值设定”,而你的scores数组声明式又需要。可以改用"the enum hack"补偿做法,“一个属于枚举烈性的数值可权当int使用”。
class GamePlayer{
private:
enum {NumTurns = 5};
int scores[NumTurns];
}
enum hack优点:
- enum hack的行为比较像#define,而不是const。如取const的地址是合法的,但取enum地址不合法(取#define通常也不合法)
- 许多代码都用到了它,例如模板元编程。
把焦点拉回预处理器。另一个常见的#define误用情况是以它实现宏。宏看起来像函数,但不会招致函数调用带来的额外开销。下面这个宏夹带着宏实参,调用函数f:
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
无论合适写出这样的宏,都必须记得给所有实参加上小括号,否则在调用时可能会遇到麻烦。但纵使这样,也会遇到问题:
int a = 5, b = 0;
CALL_WITH_MAX(++a,b); //a被累加二次
CALL_WITH_MAX(++a,b+10); //a被累加一次
因此,最好使用模板内联函数来替代它:
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
有了const,enum和inline,我们对于预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演者重要角色。目前还达不到预处理器全面隐退的时候,但应该给与它更长的假期。
请记住:
- 对于单纯常量,最好以const对象或enums替换#define。
- 对于形似函数的宏,最好改用inline函数替换#define。
条款03:尽可能使用const
Use const whenever possible
- const在*左边,内容是常量。(*xx解引用了)
- const在*右边,指针(地址)是常量。
对于STL迭代器来说,其系以指针为根据塑模出来,因此:
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begain(); //和T* const类似
std::vector<int>::const_iterator iter = vec.begain(); //和const T*类似
const成员函数
将const实施于成员函数的目的是,为了确认该成员函数可作用域const对象身上。
- 使得class接口容易被理解,知道哪个函数可以改动对象,哪个不可以。
- 使“操作const对象”成为可能。
两个成员函数如果只是常量性不同,可以被重载。
class TextBlock{
public:
const char& operator[](std::size_t position) const //operator for const object
char& operator[](std::size_t position) //operator for non-const object
}
TextBlock tb("Hello");
std::cout<< tb[0]; //call non-const operator
const TextBlock tb("Hello");
std::cout<< tb[0]; //call const operator
mutable使得const函数可以修改成员变量.
class CTextBlock{
public:
void length(size_t size) const;
private:
mutable size_t length = 1;
}
void CTextBlock::length(size_t size) const
{
length = size;
}
如果operator[]中有大量代码,那么实现两个具有冗长重复代码的operator[]可能不是个好事情。因此,真正应该做的是实现operator[]的机能一次,并使用两次(用其中一个调另一个)。
这促使我们将常量性转除(casting away constness)。然而就一般守则而言,转型是一个糟糕的想法(条款27),但重复代码也不是什么令人愉快的事情。
class TextBlock{
public:
const char& operator[](std::size_t position) const //operator for const object
{
//a lot of things here
return text[position];
}
char& operator[](std::size_t position) //operator for non-const object
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
}
但请注意,令const版本调用non-const版本以避免重复不是应该发生的事情。
请记住:
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”。
- 当const和non-const成员函数有实质等价的实现时,令non-const版本调用const版本。
条款04:确认对象被使用前已被初始化
Make sure that objects are initialized before they'er used
如果使用C part of C++,而且初始化可能招致运行期成本,那么就不保证发生初始化。non-C parts of C++规则有些变化。这就很好的解释了array不保证其内容被初始化,而vector却有此保证。
Best practice:永远在使用对象之前将它初始化。
- 对于内置类型,你必须手工完成此事。 int x = 0;
- 除内置类型外,初始化责任落在构造函数上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化
这个规则很轻易,但不要混淆了赋值和初始化。
class PhoneNumber { ... }
class ABEntry
{
public:
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
{
theName = name; //这些都是赋值,而不是初始化
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
int numTimesConsulted;
}
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体前(但对numTimesConsulted这个内置类型不一定为真,有可能他就是在这个时候初始化,且内置类型初始化与赋值的成本相同)。
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
:theName(name), theAddress(address), thePhones(phones),numTimesConsulted(0)
{ }
//当你要默认构造一个成员变量,不传入参数即可
ABEntry():theName(), theAddress(), thePhones(),numTimesConsulted()
{ }
以上构造函数的效率较高,其只调用成员变量的一次copy构造函数。而前者会先调用default构造函数再调用copy assignment操作符。
Note:如果成员变量是const或references,它们一定需要初值,不能被赋值。
如果类有多个构造函数,则成员初始化列表会存在多份,这种情况下可以合理在初始化列表遗漏内置类型成员变量,改用赋值操作,并将这些赋值操作移至某个函数(通常private),供所有构造函数调用。
Note:类的成员变量总是以其声明次序被初始化,不受初始化成员列表的影响。
一旦上述知识你都掌握,那只剩一件事需要操心。“不同编译单元内定义non-local static对象”的初始化顺序。
- 在函数内的static对象称为local static对象,其他的static对象称为non-static对象。
- 编译单元是指产出单一目标文件的那些源码,基本上就是单一源码文件加上其所含的头文件。
C++对于定义在不同编译单元内的non-local static对象的初始化次序并无明确定义。因此,若某编译单元的non-local static对象初始化动作使用了另一个non-local static对象,它所使用的这个对象可能尚未被初始化。
可以通过一个小设计来避免这个问题,将每个non-local static对象搬到自己的专属函数内,函数返回一个reference指向它所含的对象,用户调用函数。换句话说,non-local static被local static对象替换了(Singleton模式)。这个手法的保证在于:函数内的local static对象会在“该函数被调用期间” “首次遇上该对象之定义式”时被初始化。
这样做还有一个好处:如果你从未调用这个函数,就不会引发non-local static对象的构造和析构成本。但请注意,在多线程情况下的不确定性(感觉和单例模式类似)。
- 为内置型对象进行手工初始化,因为C++不保证初始化它们。
- 构造函数最好使用初始化成员列表,且其排列顺序应该和它们在类中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。