条款01:视C++为一个语言联邦
Exception对函数的结构化带来不同的做法 。
Templates带来了新的设计思考方式。
STL定义了一个前所未见的伸展性做法。
C++是一个多重范型编程语言,一个同时支持过程形式,面向对象形式的语言。
为了理解C++,必须认识其主要的次语言,有如下4个:
1.C:当以C++内的C成分工作时,高效编程守则映照出C语言的局限性:没有模板,没有异常,没有重载。。。
2.Object-Oriented C++:类,封装,继承,多态,动态绑定。
3.Template C++:泛型编程部分,带来了TMP模板元编程。
4.STL:STL是个template的程序库,它对容器,迭代器,算法以及函数对象的规约有极佳的紧密配合与协调,然而templates及程序库也可以以其他方法建制出来。
内置类型(C-Like)类型而言,pass-by-value通常比pass-by-reference高效。
用户自定义类型而言,pass-by-reference-to-const往往更好。
STL而言,迭代器和函数对象都是在C指针之上塑造出来的,所以对STL的迭代器和函数对象而言,旧式的C pass-by-value再次适用。
请记住:C++高效编程守则视情况而变化,取决于你使用C++的哪一部分。
条款02:尽量以const,enum,inline替换#define
宁可以编译器替换预处理器,因为或许#define不被视为语言的的一部分。
#define所使用的名称不会进入记号表,难以调试。
使用常量可能比使用#define导致较小的代码,因为预处理器直接做了替换
class GamePlayer
{
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用该常量
};
const int GamePlayer::NumTurns; //常量定义式
通常C++要求你对你所使用的任何东西提供一个定义式,但是如果它是个class专属常量又是static且为整数类型(int,char,bool),则需要特殊处理。只要不取地址,可以声明并使用它们而不提供定义式。但是如果取某个class专属常量的地址,或即使不取地址而编译器坚持要看到一个定义式,则必须额提供一个定义式。此定义式放在实现文件中,且定义时候不需要在设置初始值。
旧式编译器可能不支持,它们不允许static成员在其声明式上获得初值,则将其放入定义式:
class CostEstimate
{
private:
static const double FudgeFactor;
};
const double CostEstimate::FudgeFactor = 1.3f;
唯一的例外是,当class的编译期间需要一个class常量值,万一编译器错误的不允许static整数型常量完成in class初始值设定,可以改用为所谓的“the enmu hack”补偿做法,其理论基础是:一个属于枚举类型的数值可权充int被使用:
class GamePlayer
{
private:
enum { NumTurns = 5 }; //the enum hack
int scores[NumTurns]; //使用该常量
};
有关the enum hack:
1.enum hack行为某方面说比较像#define而不像const,如果取一个const的地址是合法的,但是取一个enmu的地址就不合法,而取一个#define的地址也不合法。
如果不想让别人获得一个point或reference指向你的某个整数常量,enum可以帮助实现这个约束。
2.enum hack是template metaprogramming模板元编程的基础技术。
template inline函数可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全检查。
请记住:
1.对于单纯常量,最好以const对象或enmu替换#define
2.对于形似函数的宏,最好改用inline函数替换#define
条款03:尽可能使用const
const出现在星号左边,表示被指物是常量。
const出现在星号右边,表示指针自身是常量。
const出现在星号两边,表示两者都是常量。
char greeting[] = "hello";
const char* p = greeting; //const data,non-const pointer
char* const p = greeting; //const pointer,non-const data
const char* const p = greeting; //both const
STL迭代器是以指针为根据塑造出来的,所以迭代器的作用就像一个T*指针。声明迭代器为const就像声明指针为const一样(T* const),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果希望迭代器所指的东西不可以被改动(STL模拟一个const *T),则需要一个人const_iterator。
const vector<int>::iterator iter = vec.begin(); //iter类似T* const
*iter = 10;
vector<int>::const_iterator cIter = vec.begin(); //cTter类似const T*
cIter++;
const最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值,参数,函数自身(如果是成员函数)产生关联。
将const实施与成员函数的目的,是为了确认该成员函数可作用于const对象上,有两个原因:
1.可以得知哪个函数可以改动对象,哪个函数不行。
2.使它们操作const对象成为可能
两个成员函数如果只是常量性不同,可以被重载。
class TextBlock
{
public:
TextBlock() {}
TextBlock( string str ) { text = str; }
~TextBlock() {}
public:
const char& operator[] ( size_t postion ) const
{
return text[postion];
}
char& operator[] ( size_t postion )
{
return text[postion];
}
private:
string text;
};
TextBlock tb("hello");
cout << tb[0]; //调用non-const
const TextBlock ctb("world");
cout << ctb[0]; //调用const
cout << tb[0]; //可以读non-const
tb[0] = 'x'; //可以写non-const
cout << ctb[0]; //可以读const
ctb[0] = 'u'; //不可写const
如果返回类型是一个内置类型,那么改动返回值就不合法。如果返回类型是一个引用类型,那么改动就合法。
成员函数为const意味着什么?
1.bitwise constness(physical constness)
成员函数只有在不更改对象任意成员变量(static除外)时才能说是const,也就是说它不能修改对象内任何一个bit。这种论点的好处是很容易侦测违反点:编译器只需要寻找成员变量的赋值动作即可。bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。
但是不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体的说,一个更改了指针所指物的成员函数虽然不能算是const,但如果只有指针隶属于对象,那么称此函数为bitwise constness不会引发编译器异议。
2.logical constness
一个const成员函数可以修改它所处理的对象内的某些bit,但只有在客户端侦测不出的情况下才得如此。
mutable会释放掉non-static成员变量的bitwise constness约束。
实现某一成员函数的机能,并使用它两次。也就是说必须期中一个调用另一个,这促使我们将常量性转除(casting away constness)。
当然,转型(casting)是一个糟糕的想法。
const成员函数做掉了non-const版本该做的事情,唯一的不同是其返回类型多了一个const修饰。这种情况下,如果将返回值的const转换为non-const是安全的,因为不论谁调用non-const都一定首先有一个non-const对象,否则就不能够调用non-const函数。所以令non-const调用其const兄弟食一个避免代码重复的安全做法-----即使过程中需要一个转型的动作。
class TextBlock
{
public:
const char& operator[] ( size_t pos ) const
{
cout << "const" << endl;
return 'a';
}
char& operator[] ( size_t pos )
{
cout << "non-const" << endl;
return
const_cast<char&>(
static_cast<const TextBlock&>(*this)[pos]
);
}
};
1.non-const调用其const兄弟,为了避免递归,使用了static_cast,将*this添加到const
2.从const版本中移除const返回值,使用了const_case,且移除const的那个动作只能由const_cast完成。
反向做法:const版本调用non-const是不允许的。因为const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数无此承诺。如果在const中调用non-const,就冒了曾经承诺不改动那个对象被改动了的风险。所以这是一个错误的行为。
请记住:
1.编译器强制实施bitwise constness,但我们写代码应该使用概念上的常量性。
2.当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象呗使用前已被初始化
读取未初始化的值会导致不明确的行为,某些平台上,仅仅只是读取未初始化的值就可能令程序终止运行。更可能的情况是读入一些半随机bits,污染了正在进行读取动作的那个对象,最终导致不可预测的程序行为,以及许多不愉快的调试过程。
最佳处理方法:永远在使用对象之前将它初始化,对于无任何成员的内置类型,必须手动完成;对于内置类型意外的任何其他东西,初始化责任落在构造函数上。规则很简单,确保每一个构造函数都将对象的每一个成员初始化。
对成员变量的初始化动作发生了进入构造函数本体之前。
如果成员变量是const或ref,它们就一定要初值,而不是初始化。
关于不同编译单元内定义的non-local static对象的初始化次序问题:
global对象,定义于namespace作用域的对象,class内,file作用域内的static对象时non-local static对象。
函数内的,成为local static对象。
编译单元,指的产出单一目标文件的那些源码,基本上它是单一源码问价加上其包含的头文件。
真正的问题是:如果某个编译单元内的某个non-local static对象的初始化使用了另一个编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。因为C++对于定义于不同编译单元内的non-local static对象的初始化次序没有明确的定义。
解决方案:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static),这些函数返回一个ref指向它所含的对象,而用户调用这些函数,而不直接涉及这些对象。换句话说,non-local static对象被local static对象替换了。这也是Singleton模式的一个常见实现手法。
这个手法的基础是:函数内的local static对象会在”该函数被调用期间","首次遇上该对象定义式"时候被初始化。所以如果以函数调用返回一个ref指向local static对象替换直接访问non-loca static对象,你就获得了保证,保证你所获得的那个ref将指向一个经历初始化的对象。更棒的是,如果从未调用non-local static对象的仿真函数,就绝不会引发构造函数和析构函数的成本。而真正的non-local static对象则不会。
class FileSystem {...};
FileSystem& tfs()
{
static FileSytem fs;
reuturn &fs;
}
class Directory{...};
Directory::Directory()
{
tfs().....;
}
Directory& tmpDir()
{
static Directory td;
reuturn &td;
}
这种结构下的ref-returning函数往往十分简单,第一行定义并初始化,第二行返回。这些使它们成为了inlining的候选人,尤其是被频繁调用的时候。
多线程中的问题:
任何一种non-const static对象,不论是local还是non-local,在多线程环境中等待某事发生都会有不确定性。
解决方案:在程序的单线程启动阶段手工调用所有的referecce-return函数。
请记住:
1.为免除跨编译单元的初始化次序问题,请以local static替换non-local static。