让自己习惯C++
条款01:将C++看做一个语言联邦
今天的C++已经是个多重范型编程语言,一个同时支持过程形式、面向对象形式、泛型形式、元编程形式的语言。
理解它的最佳方法,就是将C++视为一个由相关语言组成的联邦而非单一语言。在其某个次语言中,各种守则都倾向简单、直观易懂。然而当你从一个次语言移往另一个次语言,守则可能改变。而C++中,共有四个次语言:
- C:说到底C++是以C为基础。区块、语句、预处理器、内置数据类型、数组、指针都来自于C。
- C with Class:这一部分是面向对象设计之古典法则在C++上的最直接实施。
- Template C++: 是C++的泛型编程部分。
- STL:STL是个template程序库,它对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调。
条款02:尽量以const,enum,inline替换#define
这个条款也可以说成:尽量以编译器替换预处理器。
#define ASPECT_RATIO 1.653 //宏定义
const double AspectRatio = 1.653; //以常量替换宏定义
在上述代码中,预处理器在编译器之前将ASPECT_RATIO全部替换为1.653,因此ASPECT_RATIO这个名字编译器无法找到,没有进入记号表内。因此若是宏被定义在一个非我们自己所写头文件内,追踪它也会花上很长的时间。
当我们以常量替换宏定义时,由于AspectRatio是一个常量,一定会被编译器看到,也会进入记号表内。
当我们以常量替换#define时,有两种特殊情况:
① 定义常量指针。一般情况下,常量定义需要放在头文件内,以便被不同的源码含入。因此必须要将指针定义为常量指针才安全。
const char* const s = "Hello,fancy!";
const string s("Hello,fancy!"); //一般情况下,使用const修饰string对象比char指针更好。
② 定义class专属常量。若一个常量是某个类专属的,则它要声明为类的成员,并且多个类共有一个即可,因此将它声明为static。
class GamePlayer{
private:
static const int num = 5; //类的专属常量声明式
int scores[num];
};
需要注意的是,上式仅仅是常量num的声明式而非定义式。在C++中,要求我们对所使用的任何东西提供一个定义式,但在这里是个例外。如果我们定义了一个整数类型(int bool char)的类专属常量,只要我们不取它们的地址,则无需定义。否则,应当在相应的.cpp文件中给出如下定义式。
const int GamePlayer::num; //由于已经赋值,因此不能再次赋值
旧式编译器不允许const修饰的常量在声明时获得初值,倘若我们必须要如此,如类GamePlayer中,需要num来初始化数组scores,此时可以用enum来代替。因为一个属于枚举类型的数值可以当做int被使用。
class GamePlayer{
private:
enum { num = 5 }; //令num成为5的记号名称
int scores[num];
};
enum的行为比较像#define而不是const。#define和enum不会为常量分配内存,因此无法获取常量的地址,因而不能被指针或引用绑定。而一般情况下,const也不会为常量分配内存,但如果有指针和引用绑定const对象时,会为它分配内存。
因此,若想常量被指针或引用绑定,则用const;若禁止常量被指针或引用绑定,则用enum。
另一个常见的#define误用是形似函数的宏。
#define MAX(a,b) f((a) > (b) ? (a) : (b)) //所有实参都要加上小括号
int a = 5,b = 0;
MAX(++a,b); //a自增两次
MAX(++a,b + 10); //a自增一次
由上述代码可见,对形似函数的宏的调用常常会得到错误的结果。此时我们可以用inline函数替代#define。
template<typename T>
inline void max(const T& a,const T&b)
{
f(a > b : a ? b);
}
条款总结:
- 对于单纯常量,最好以const(可被指针或引用绑定)或enum代替#define
- 对于形似函数的宏,最好改用inline函数代替#define
条款03:尽可能使用const
只要某个值需要保持不变,我们就应该用const来修饰它。
迭代器与const
STL迭代器是以指针为根据塑造出来的。我们可以将迭代器理解为一个 T* 的指针。声明迭代器为const就像声明指针为const一样,即T* const
,此时迭代器只可绑定固定对象,而对象内容可修改。当我们禁止迭代器指向内容修改时,应该用只读迭代器const_iterator,即const T*
。
vector<int> vec;
const vector<int>::iterator it1 = vec.begin(); //it1为常量迭代器
vector<int>::iterator const it2 = vec.begin(); //it2为常量迭代器
*it1 = 10; //正确
++it1; //错误
vector<int>::const_iterator it3 = vec.begin(); //it3为只读迭代器
*it3 = 10; //错误
++it3; //正确
函数与const
const最具威力的用法是在函数上的应用。在一个函数的声明式中,const可以和函数返回值、各参数、函数自身产生关联。
函数返回值、形参与const
令函数返回值为const,可以降低用户程序员书写代码时的错误。
class Rational { …… };
const Rational operator*(const Rational& lhs,const Rational& rhs);
if( a * b = c ); //想要比较却输入错误
如上述代码所示,我们将乘运算符的返回值以const修饰,因为想要避免不合法的应用,如if( a * b = c)
,若返回值不以const修饰,将会对其赋值产生很大错误。
对于函数形参而言,我们应该把所有不需要被改变的形参都声明为const。
const成员函数
当我们需要成员函数处理const对象时,便要用const修饰成员函数。我们之前已经了解到,只有类的const成员函数可以被类的常量对象调用。const成员函数非常重要,因为它可以指出哪个函数可以更改对象哪个函数不可以,而且使操作const对象成为可能。
对于const成员函数是什么样的函数,有bitwise constness(物理常量) 和 logical constness(逻辑常量)两种概念 。
bitwise const认为,只有当 成员函数不会更改对象的任何成员变量 时才是const,这也是编译器对常量性的定义。
class CTextBlock{
public:
char& operator[](size_t position) const //bitwise const成员函数
{
return pText[position];
}
private:
char* pText;
};
const CTextBlock ctb("Hello");
char *p = ctb[0];
*p = 'J';
但是,如上述代码所示,当我们有一个指针成员变量时,更改指针所指向内存的内容,不改变指针,编译器不会报错。operator[]函数声明为const函数,函数体内部没有改变成员变量的值,但它的返回类型是char&,返回pText所指向的某个单元,由使用代码可以更改其指向内存的内容。也就是说我们创建了常量对象ctb,并且调用了它的常函数,但最终依然改变了ctb。
这种情况导出了logical const,它认为一个 const成员函数可以修改它所处理的对象内的某些bits ,但只有在客户端侦测不出的情况下才能这样。
class CTextBlock{
public:
...
size_t length() const;
private:
char* pText;
size_t textLength;
bool lengthIsValid; //文本块长度是否有效
};
size_t CTextBlock::length() const{
if(!lengthIsValid)
{
textLength = strlen(pText); //错误,不能给它们赋值
lengthIsValid = true;
}
return textLength;
}
如上述代码所示,我们想要获取一个常量文本块对象的长度,将length()声明为const成员函数,而在当前文本块长度无效的情况下,又不可更改成员变量lenthIsValid和textLength。解决这个问题的办法是用mutable(可变的)关键字,释放掉对非静态成员的物理常量约束。
class CTextBlock{
public:
...
size_t length() const;
private:
char* pText;
mutable size_t textLength;
mutable bool lengthIsValid; //可变的
};
size_t CTextBlock::length() const{
if(!lengthIsValid)
{
textLength = strlen(pText); //正确
lengthIsValid = true;
}
return textLength;
}
在const和非const成员函数中避免重复
举个例子,假设TextBlock内的operator[] 需要执行边界检验、记录访问信息、数据完善性检验等,将这些所有同时放入const成员函数和非const成员函数中,会有大量的重复操作。
class TextBlock{
public:
...
const char& operator[](size_t position) const
{
//边界检验
//记录访问信息
//数据完善性检验
return text[position];
}
char& operator[](size_t position)
{
//边界检验
//记录访问信息
//数据完善性检验
return text[position];
}
private:
string text;
};
当我们看到这些重复代码时,自然会想到,新建一个private函数,将共同操作放入函数内部,再调用该函数。但是函数调用和返回语句也出现了重复。
正确的做法是,实现一个operator[]一次,令其中一个调用另外一个,这促使我们将常量性转除。上述代码中,const成员函数做了非const成员函数该做的一切,除了返回值是个常量。因此我们可以在非const成员函数中调用const成员函数。
class TextBlock{
public:
...
const char& operator[](size_t position) const
{
//边界检验
//记录访问信息
//数据完善性检验
return text[position];
}
char& operator[](size_t position)
{
return const_cast<char&>( //3.将operator[]返回值的const转除
static_cast<const TextBlock&>(*this) //1.令*this转换为const
[position]); //2.调用const operator[]
}
private:
string text;
};
在上述代码中,我们使用了两次显示类型转换,第一次是使用static_cast将*this转换为const *this,以使非const函数不会递归调用自身。第二次是使用const_cast将const成员函数的返回类型转换为char&(也就是转除const)。
条款总结:
- 将某些东西声明为const可帮助编译器侦测出错误用法。
- 编译器强制实施bitwise const,但编写程序时我们应使用logical const。
- 当const成员函数和非const成员函数有等价的实现时,令非const成员函数调用const。
条款04:确定对象被使用前已被初始化
我们知道,对象被定义才可以被使用。但是在一些情况下,对象会被默认初始化;而在另一些情况下, 对象不一定被默认初始化。一般而言,C++中属于C的部分不一定会被默认初始化,不属于C的部分会被默认初始化。比如数组不会默认初始化,而vector会默认初始化。
最好的处理方法就是:在对象使用之前先将它初始化。初始化内置类型时,手工完成;初始化类类型时,由构造函数完成,确保构造函数将每一个成员变量初始化。重要的是不要混淆赋值和初始化。
class PhoneNumber{...};
class ABEntry{
public:
ABEntry(const string& name,const string address,
const list<PhoneNumber>& phones);
private:
string theName;
string theAddress;
list<PhoneNumber> thePhone;
int numTimeConsulted;
};
ABEntry::ABEntry(const string& name,const string address,
const list<PhoneNumber>& phones){
theName = name;
theAddress = address;
thePhone = phones;
numTimeCousulted = 0;
}
在上述代码中,构造函数的行为是赋值行为,而不是初始化行为,我们不建议这样写。它要先调用成员变量的默认构造函数进行初始化,再调用成员变量的拷贝赋值运算符进行赋值,因此效率很低。
ABEntry::ABEntry(const string& name,const string address,
const list<PhoneNumber>& phones):
theName (name),theAddress(address),
thePhone(phones),numTimeCousulted(0){}
这才是构造函数的正确写法,对成员的初始化应放在函数体外执行,对每个成员变量调用其拷贝构造函数进行初始化。而对于const及引用成员,必须使用构造函数进行初始化,不能赋值。总而言之,我们最好在构造函数初始值列表中列出所有成员。
非局部静态变量的初始化次序
所谓static对象,它从被构造出来一直到程序结束才被销毁。局部静态对象是指函数内的static对象,非局部静态对象包括global对象、namespace内的对象、在class内、在file内被声明为static的对象。
C++对定义于不同编译单元的非局部静态变量没有确定的初始化次序。因此,我们要尽量使用局部static变量和类内static变量,避免其他的非局部静态变量。
条款总结:
- 为内置类型手动初始化,因为C++不保证初始化它们。
- 为类类型编写构造函数,将所有成员变量按声明次序列入构造函数初始值列表。
- 尽量使用局部static变量(函数体内的)及类内static变量,避免使用其他非局部静态变量。