不论你的编程背景是什么,C++都可能让你觉得有点儿熟悉。它是一个威力强大的语言,带着众多特性,但是在你可以驾驭其威力并有效运用其特性之前,你必须先习惯C+十的办事方式。本书谈的便是这个。总有某些东西比其他更基础些,本章就是最基本的一些东西。
今天的C++已经是个多重范型编程语言(multiparadigm programming language),一个同时支持过程形式(procedural)、面向对象形式(object-oriented) .函数形式(functional )、泛型形式(generic)、元编程形式(metaprogramming )的语言。这些能力和弹性使C++成为一个无可匹敌的工具,但也可能引发某些迷惑:所有“适当用法”似乎都有例外。我们该如何理解这样一个语言呢?
最简单的方法是将C十十视为一个由相关语言组成的联邦而非单一语言。在其某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。为了理解C++,你必须认识其主要的次语言。幸运的是总共只有四个:
C。说到底C++仍是以C为基础。区块(blocks)、语句(statements )、预处理器(preprocessor)、内置数据类型(built-in data加es)、数组(arrays )、指针(pointers)等统统来自C。许多时候C++对问题的解法其实不过就是较高级的C解法(例如条款2谈到预处理器之外的另一选择,条款13谈到以对象管理资源),但当你以C++内的C成分工作时,高效编程守则映照出C语言的局限:没有模板(templates ),没有异常(exceptions),没有重载(overloading ) .....
Object-Oriented C++。这部分也就是C with Classes所诉求的:classes(包括构造函数和析构函数),封装(encapsulation )、继承(inheritance )、多态( polymorphism )、virtual函数(动态绑定)……等等。这一部分是面向对象设计之古典守则在C一上的最直接实施。
Template C++。这是C++的泛型编程(generic programming)部分,也是大多数程序员经验最少的部分。Template相关考虑与设计己经弥漫整个C+十,良好编程守则中“惟template适用”的特殊条款并不罕见(例如条款46谈到调用template functions时如何协助类型转换)。实际上由于templates威力强大,它们带来崭新的编程范型(programming paradigm ),也就是所谓的template meta programming
STL 。STL是个template程序库,看名称也知道,但它是非常特殊的一个。它对容器(containers )、迭代器(iterators )、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合与协调,然而templates及程序库也可以其他想法建置出来。STL有自己特殊的办事方式,当你伙同STL一起工作,你必须遵守它的规约。
C++高效编程守则视状况而变化,取决于你使用C+十的哪一部分。
这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许#define不被视为语言的一部分。那正是它的问题所在。当你做出这样的事情:
ASPECT_RATIO在预编译的时候被替换掉。不会记录在符号表中,一旦出错则报1.653出错。找不到对应的宏定义。
作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。此外对浮点常量(floating point constant,就像本例)而言,使用常量可能比使用#define导致较小量的码,因为预处理器“盲目地将宏名称ASPECT_RATI。替换为1.653”可能导致目标码(object code)出现多份1.653,若改用常量AspectRatio绝不会出现相同情况。
const double AspectRatio=1.653; //大写名称通常用于宏, //因此在这里改变名称写法。
关于const的意义和使用(特别是当它与指针结合时),条款3有完整的讨论。这里值得先提醒你的是,string对象通常比其前辈char*-based合宜,所以上述的authorns往往定义成这样更好些:
const std::string authorName("Scott Meyers");
第二个值得注意的是class专属常量。为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员(member );而为确保此常量至多只有一份实体,你必须让它成为一个static成员:
class GamePlayer{ private: static const int NumTurns = 5;//常量声明式 int scores[NumTurns];//使用该常量 ... };
这几乎是你在任何时候唯一需要做的事。唯一例外是当你在class编译期间需要一个class常量值,例如在上述的GamePlayer::scores的数组声明式中(是的,编译器坚持必须在编译期间知道数组的大小)。这时候万一你的编译器(错误地)不允许“static整数型class常量”完成“in class初值设定”,可改用所谓的”the enumhack”补偿做法。其理论基础是:“一个属于枚举类型(enumerated type)的数值可权充ints被使用”,于是GamePlayer可定义如下:
class GamePlayer{ private: enum{NumTurns=5};//"the enum hack',一令NumTurns成为5的一个记号名称. int scores[NumTurns];//这就没问题了. };
基于数个理由enum hack值得我们认识。第一,enum hack的行为某方面说比较像#define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个en。的地址就不合法,而取一个#define的地址通常也不合法。
用inline替代宏。
//以a和b的较大值调用f int a=5, b=0; CALL WITH MAX(++a, b); CALL WITH MAX(++a, b+10); //a被累加二次 //a被累加一次
幸运的是你不需要对这种无聊事情提供温床。你可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全性(type safety )—只要你写出template inline函数(见条款30):
template<typename T> inline void callWithMax(const T& a, coast T& b) //由于我们不知道T是什么,所以采用 //pass by reference-to-coast. { f (a>b?a:b); }
char greeting[]="Hello"; char* p=greeting; const char* p=greeting; char* const p=greeting;//const pointer, non-const data const char* const p=greeting;//const pointer,const data
const语法虽然变化多端,但并不莫测高深。如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,考虑有理数(rational numbers,详见条款24)的operator*声明式:
class Rational{…}; const Rational operator* (const Rational& lhs, const Rational& rhs);
我不知道为什么会有人想对两个数值的乘积再做一次赋值(assignment ),但
我知道许多程序员会在无意识中那么做,只因为单纯的打字错误(以及一个可被隐式转换为bool的类型):
if (a*b=c)…//呢欧,其实是想做一个比较
如果a和b都是内置类型,这样的代码直截了当就是不合法。而一个“良好的用户自定义类型”的特征是它们避免无端地与内置类型不兼容(见条款18),因此允许对两值乘积做赋值动作也就没什么意思了。将。perator*的回传值声明为const可以预防那个“没意思的赋值动作”,这就是该那么做的原因。
const成员函数
将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这一类成员函数之所以重要,基于两个理由。
第一,它们使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。
第二,它们使“操作const对象”成为可能。这对编写高效代码是个关键.。
许多人漠视一件事实:两个成员函数如果只是常量性(constness)不同,可以被重载。这实在是一个重要的C一特性。
bitwise const阵营的人相信,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。也就是说它不更改对象内的任何一个bit。这种论点的好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness正是C++对常量性(constness)的定义,因此const成员函数不可以更改对象内任何non-static成员变量。
不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。这导致反直观结果。假设我们有一个TextBlock-like class,它将数据存储为char*而不是string,因为它需要和一个不认识string对象的C API沟通:
class CTextBlock{ public: char& operator[](std::size t position) const//bitwise const声明 {return pText[position];} private: char* pText; }; const CTextBlock cctb("Hello");//声明一个常量对象。 char* pc=&cctb[0];//调用const operator []取得一个指针, *pc= 'J';//指向cctb的数据。 //cctb现在有了’'Jello',这样的内容。
存在这种情况:const 对象必须调用const函数,同时某些变量必须更改。这个时候采用mutable.
class CTextBlock{ public: std::size t length()const; private: char* pText; std::size七textLength;//最近一次计算的文本区块长度。 bool length工sValid;//目前的长度是否有效。 }; std::size t CTextBlock::length()const { if(!lengthIsValid){ textLength一std::strlen(pText);//错误!在coast成员函数内 lengthIsValid=true;//不能赋值给textLength }//和lengthIsValid. return textLength; }
length的实现当然不是bitwise const,因为textLength和length工sValid都可能被修改。这两笔数据被修改对const CTextBlock对象而言虽然可接受,但编译器不同意。它们坚持bitwise constness。怎么办?
解决办法很简单:利用C+十的一个与cons七相关的摆动场: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;//会被更改,即使在const成员函数内。 }; std::size t CTextBlock::length()const { if(!lengthIsValid){ textLength=std::strlen(pText); lengthIsValid=true; } return textLength; } };
在const和non-const成员函数中避免重复
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptualconstness)。
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
关于“将对象初始化”这事,C++似乎反复无常。如果你这么写:
int x;
在某些语境下x保证被初始化(为0),但在其他语境中却不保证。如果你这么写:
class Point int x, y; }; Point p;
p的成员变量有时候被初始化(为0),有时候不会。如果你来自其他语言阵营而那儿并不存在“无初值对象”,那么请小心,因为这颇为重要。
为了避免不可预测行为,所以我们必须保证对所有的变量进行初始化。其中分为内置类型初始化,以及内置类型以外的类的初始化。
内置类型初始化
int x=0;//对int进行手工初始化 const char* text="A C-style string";//对指针进行手工初始化 //(亦见条款3) double d; std::cin>>d;//以读取input stream的方式完成初始化.
非内置类型,初始化落在了构造函数身上
class PhoneNumber{…}; class ABEntry{//ABEntry="Address Book Entry" public: ABEntry(const std::string& name, const std::string& address const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNun由er> thePhones; int numTimesConsulted; ABEntry::ABEntry(const std::string& name, const std::string& address const std::list<PhoneNumber>& phones) { theName = name; theAddress=address; thePhones=phones; numTimesConsulted=0; //这些都是赋值(assignments ), //而非初始化(initializations )。 }
这会导致ABEntry对象带有你期望(你指定)的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress和thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作:
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :theName(name), theAddress(address), thePhones(phones),//现在,这些都是初始化(initializations ) numTimesConsulted(0)//现在,构造函数本体不必有任何动作 {}
C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes更早于其derived classes被初始化(见条款12),而class的成员变量总是以其声明次序被初始化。
static对象:其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。
local static对象:函数内的对象。
non-local static对象:区别于local static对象。
编译单元(translation unit):指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。
problem: 如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的not-local static对象”的初始化次序并无明确定义。
eg:
实例可以帮助理解。假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机(local)。由于这个class使世界看起来像个单一文件系统,你可能会产出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem{ public: //来自你的程序库 std::size t numDisks()const; //众多成员函数之一 }; extern FileSystem tfs;//预备给客户使用的对象; //tfs代表’'the file system"
现在假设某些客户建立了一个class用以处理文件系统内的目录(directories) .很自然他们的class会用上theFileSystem对象:
class Directory{ public: Directory(params //由程序库客户建立 }; Directory::Directory(params { std::size t disks=tfs.numDisks();//使用tfs对象 } }; 进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件: Directory tempDir(params); //为临时文件而做出的目录
现在,初始化次序的重要性显现出来了:除非tf。在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non.-local static对象。如何能够确定tfs会在tempDir之前先被初始化?
哩,你无法确定。再说一次,C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。在其最常见形式,也就是多个编译单元内的non-localstatic对象经由“模板隐式具现化,implicit template instantiations”形成(而后者自己可能也是经由“模板隐式具现化”形成),不但不可能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。
解决方法:幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)o这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。Design Patterns迷哥迷姊们想必认出来了,这是Singleton模式的一个常见实现手法。
class FileSystem{...};//同前 FileSystem& tfs()//这个函数用来替换tfs对象;它在 { //FileSystem class中可能是个static o static FileSystem fs;//定义并初始化.个local static对象, return fs;//返回一个reference指向上述对象。 } class Directory{…};//同前 Directory::Directory(params)//同前,但原本的reference to tfs { //现在改为tfs() std::size t disks=tfs().numDisks(); } Directory& tempDir()//这个函数用来替换tempDir对象; { //它在Directory class中可能是个static static Directory td;//定义并初始化local static对象, return td; //返回一个reference指向上述对象。 }
这么修改之后,这个系统程序的客户完全像以前一样地用它,唯一不同的是他们现在使用tfs ()和七e叩Dir()而不再是tfs和tempDir。也就是说他们使用函数返回的“指向static对象”的references,而不再使用stab。对象自身。
在多线程情况下:在程序的单线程启动阶段( single-threaded startup portion)手工调用所有reference-returning函数,这可消除与初始化有关的,’竞速形势(race conditions )。
为内置型对象进行手工初始化,因为c什不保证初始化它们。
构造函数最好使用成员初值列(member initialization list ),而不要在构造函数 本体内使用赋值操作(assignment )。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。