当我读完Scott Meyers编写的《Effective C++》以后,我特别想再读一遍,因为这是我看过的关于C++非常好的书籍。这本书介绍了C++编程中需要注意的问题(对于一位合格的C++程序员非常关键的问题),作者以精简的语言进行了描述。第二遍阅读的时候,我打算以笔记的形势记录下来。该书一共分为八个部分:
今天的C++已经是个多重范型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。我们可以把C++视为一个由相关语言组成的联邦而非单一语言。为了理解C++,你必须认识其主要次语言。总共有四个:
当你写出这样的代码时:
#define ASPECT_RATIO 1.653
记号ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了。如果ASPECT_RATIO被定义在一个非你写的头文件中,你肯定对它来自何处毫无概念,于是你讲因为跟踪它而浪费时间。解决之道是以一个常量替换上述的宏(#define):
const double AspectRatio = 1.653;
此外对于浮点常量而言,使用const可能比使用#define导致较小的代码量,因为预处理器“盲目地将ASPECT_RATIO名称替换为1.653”可能导致目标码出现多份1.653,若用const常量则不会出现这种情况。
注意class专属常量。为了将常量的作用域限制在class内,你必须让它成为class的一个成员;而为了确保此常量至多只有一份实体,你必须让它成为一个static成员:
class GamePlayer
{
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns];
...
}
上面的NumTurns是声明式而非定义式,通常C++要求你对你使用的任何东西提供一个定义式。但如果它是个class专属常量又是static同时为整数类型且不取其地址时,则不需要提供定义式。如果坚持使用定义式,用如下方式提供:
const int GamePlayer::NumTurns; //由于声明时已经提供初值,因此定义时不再提供初值。定义应该放在实现文件而非头文件。
但是,当类型不是int型时,编译器不支持“in-class初始值设定”,这时初始值应放在定义式。如下:
//static class 常量声明位于头文件
class CostEstimate
{
private:
static const double FudgeFactor;
...
};
//static class 常量定义位于实现文件
const double CostEstimate::FudgeFactor = 9.78;
另外一个常见的#define误用情况是以它实现宏,宏看起来像函数,但不会招致函数调用带来的额外开销。例如:
#define CALL_MAX(a,b) f((a)>(b)?(a):(b)
int a = 5, b = 0;
CALL_MAX(++a,b); //a被累加两次
CALL_MAX(++a,b+10); //a被累加一次
a的累加次数竟然取决于“它被拿来和谁比较”,当和小于自己的数比较时累加两次,当和大于自己的数比较时累加一次。我们可以用template inline 函数来获得宏带来的效率以及一般函数的所有可预料行为和安全性。
template<typename T>
inline void callWithMax(const T& a, const T& b) //采用pass by reference-to-const
{
f(a > b ? a : b);
}
小结:
- 对于单纯常量,最好以const对象或者enums替换#defines
- 对于形似函数的宏,最好改用inline函数替换#define
const可以用的地方非常多,在类外修饰global或namespace作用域的常量,修饰文件、函数、或区块作用域中被声明为static的对象,也可以修饰类内部的static和non-static成员变量。对于指针,可以指出指针自身、指针所指物,或者两者都是const。
char greeting[] = "Hello";
char* p = greeting; //non-const pointer,non-const data
const char* p = greeting; //non-const pointer,const data
char* const p = greeting; //const pointer,non-const data
const char* const p = greeting; //const pointer,const data
const最具威力的用法是面对函数的应用,在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。
令函数产生一个常量值,往往可以降低因客户而造成的意外,而又不至于放弃安全性和高效性。列如:
class Rational {...};
const Rational operator* (const Rational& lhs, const Rational& rhs);
这里返回const对象可以禁止下面这种现象发生
Rational a, b, c;
...
(a*b) = c;
可能有人会说我不写这样的函数,但是许多程序员会无意识的那么做,例如:
if(a*b = c)... //不小心将==写出=
const成员函数
将const实施于成员函数的目的是为了确认该成员函数可作用于const对象身上。这类成员函数非常重要,有两个理由。第一,它们使类接口比较容易理解,这是因为,得知哪个函数可以改动对象内容而哪个函数不行。第二,它们使“操作const对象”成为可能。这对编写高效的代码非常关键,因为改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术的前提是我们有const成员函数可用来处理取得的const对象。
注意:
(1)const修饰的成员函数不能修改任何的成员变量(mutable修饰的变量除外)
(2)const成员函数不能调用非const成员函数,因为非const成员函数可以会修改成员变量
成员函数如果是const意味着什么?有两个流行概念:bitwise constness(又称physical constness)和logical constness。bitwise const这一阵营的人相信,成员函数只有在不改变对象之任何成员变量(static除外)时才可以说是const。也就是说它不能改变对象的任何一个bit。logical consteness 这一派主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。
对于bitwise const阵营来说,好处是很容易侦测违反点:编译器值需要寻找成员变量的赋值动作即可。不幸的是,许多成员函数虽然不十足具备const性质但仍能通过bitwise测试。例如:
class CTextBlock
{
public:
...
//bitwise const声明,但其实不恰当
char& operator[](std::size_t position) const
{
return pText[position];
}
private:
char* pText;
};
请注意,operator[]实现代码并不更改pText。于是编译器很开心地为operator[]产出目标码。但是看看它允许发生的事情:
const CTextBlock cctb("Hello"); //声明一个常量对象
char* pc = &cctb[0]; //调用const operator[]取得一个指针,指向cctb的数据
*pc = "J"; //cctb变为"Jello"
这种情况导出所谓的logical constness。这一派主张,一个const函数可以修改它所处理对象的某些内容。例如你的CTextBlock class有可能高速缓存文本区块的长度以便应付查询:
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength;
bool LengthIsValid;
};
std::size_t CTtextBlock::length() const
{
if(!LengthIsValid)
{
//错误,在const成员函数内不能赋值给textLength和LengthIsValid
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
上面的代码编译会出错,因为length()函数会修改textLength和LengthIsValid的值。编译器坚持bitwise constness,不同意修改其值。不过解决办法很简单:利用C++的mutable关键字。mutable会释放掉non-static成员变量的bitwise constness约束。如下:
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool LengthIsValid;
};
std::size_t CTtextBlock::length() const
{
if(!LengthIsValid)
{
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
小结:
编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”。
关于“将对象初始化”这事,C++似乎反复无常。如果你这么写:
int x;
在某些语境下x保证被初始化为0,有些语境则不能保证。通常如果你使用C part of C++而且初始化可能招致运行期成本(博主不太懂),那么就不保证初始化。一但进入non-C part of C++,规则就会有变化。这就解释了为什么array(来自C part of C++)不保证其内容被初始化,而vector(来自STL part of C++)确由此保证。关于初始化我们应该:永远在使用对象之前先将它初始化。对于任何成员的内置类型必须手工完成初始化,内置类型以外的则由构造函数完成。
注意赋值和初始化的区别:
class PhoneNumber {...};
class ABEntry
{
public:
ABEntry(const string& name, const string& address, const list & phones);
private:
string theName;
string theAddress;
list thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const string& name, const string& address, const list & phones)
{
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
虽然这样写没有错,但这不是好的C++做法。C++规定,对象的成员变量的初始化动作发生在构造函数本体之前。在ABEntry构造函数内,theName、theAddress和thePhones都不是初始化,而是赋值。但对于怒骂TimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初始值。ABEntry构造函数较好的写法是使用成员初始化列,这种方式效率往往更高。如下:
ABEntry::ABEntry(const string& name, const string& address, const list & phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
不同编译单元内的“non-local static对象”的初始化次序:
假设有一个FileSystem class,它让互联网上的文件看起来像在本机。由于这个class使世界看起来像个单一文件系统,你可能会产生一个特殊的对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem
{
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs;
现在假设某些客户建立了一个class用以处理文件系统内的目录,很自然他们的类会用上theFileSystem对象:
class Directory
{
public:
Directory(params);
...
};
Directory::Directory(params)
{
...
std::size_t disks = tfs.numDisks();
}
//假设客户决定创建一个Directory对象用来放置临时文件:
Directory tempDir(params);
现在,初始化次序的重要性显示出来了:除非tfs在tempDir之前初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但是tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元的non-local static对象。但是C++对于不同的编译单元内的“non-local static对象”的初始化相对次序没有明确的定义。
为了解决上述问题,我们需要将每个non-local static对象搬到自己的专属函数内(该对象在函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户直接调用这些函数,而不需要指涉这些对象。这样做是因为:C++保证,在函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用(返回一个reference指向local static对象)”替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。修改代码如下:
class FileSystem{...}; //同前
FileSystem& tfs() //这个函数替换tfs对象
{
static FileSystem fs;
return fs;
}
class Directory{...} //同前
Directory::Directory(params)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}
小结: