Effective C++
Scott Meyers
屋檐下的水滴--读书笔记系列
导读 Introduction
1. 所谓declaration,是用来将一个object、function、class或template的类型告诉编译器,它不带细节信息。所谓definition,是用来将细节信息提供给编译器。对object而言,其定义式是编译器为它配置内存的地点;对function或function template而言,其定义式提供函数本体function body;对class或class template而言,其定义式必须列出该class或template的所有members。
2. 所谓default constructor是指不需任何参数就可被调用的constructor,不是没有任何参数,就是每个参数都有默认值。通常在定义对象数组时,就会需要一个default constructor。如果该类没有提供default constructor,通常的做法是定义一个指针数组,然后利用new将每个指针一一初始化;在该方法无效的情况下,可以使用placement new方法。
3. 所谓copy constructor是以某对象作为另一同种类型对象的初值,或许它最重要的用途就是用来定义何谓以by value方式传递和返回对象。事实上,只要编译器决定产生中介的临时性对象,就会需要一些copy constructor调用动作,重点是:pass-by-value便意味着调用“copy constructor”。
4. 初始化initialization行为发生在对象初次获得一个值的时候。对于带有constructors的classes或structs,初始化总是经由调用某个constructor达成。对象的assignment动作发生于已初始化的对象被赋予新值的时候。纯粹从操作观点看,initialization和assignment 之间的差异在于前者由constructor执行,后者由operator =执行。C++ 严格区分此二者,原因是上述两个函数所考虑的事情不同:constructors通常必须检验其参数的有效性,而大部份assignment运算符不必如此,因为其参数已经构造完成,必然是合法的。另一方面,assignment动作的目标对象并非是尚未建构完成的对象,而是可能已经拥有配置得来的资源。在新资源可被赋值过去之前,旧资源通常必须先行释放。
5. C++的两个新特征:
bool类型:其值不是true就是false,语言内建的关系运算符、条件判断式的返回类型都是bool。若编译器尚未实现该类型,有两种选择:
enum bool { false, true }; | bool与int是不同类型,允许bool与int间的函数重载,但内建关系运算符依然返回int; |
typedef int bool; const bool false = 0; const bool true = 1; | bool与int成为同种类型,兼容于传统的C/C++语意,移植到支持bool的平台上后行为不变,但不允许int与bool间的函数重载; |
四个转型运算符:static_cast、const_cast、dynamic_cast、reinterpret_cast。它们更容易在程序代码中被识别出来,编译器更容易诊断出错误的运用。
2002-6-23
改变旧有的C习惯 Shifting from C to C++
C基本上只是C++的一个子集,其许多技巧在C++中已经不合时宜。例如以reference to pointer取代pointer to pinter。某些C习惯用法与C++的精神相互矛盾。
条款1:尽量以const和liline取代#define(以compiler取代preprocessor)
理由1: #define定义的常量名称可能在编译之前就被preprocessor移走,因此不会出现于symbol table中,从而就没有机会被编译器看见。这样的结果是会给debug工作带来不便。不如改用const定义常量。
理由2: #define实现的带有实参的宏,虽然不必付出函数调用所需的成本,但用户使用时极易出错。不如使用inline function。
注意1.常量指针的定义,如:const char * const authorName = “Scott Meyers”。
注意2.class专属常量,即一个const static member,要注意在implementation文件中定义它。
注意3.不能完全舍弃preprocessor,因为#include、#ifdef、#ifudef在编译控制过程中还扮演着重要角色。
条款2:尽量以<iostream>取代<stdio.h>
尽管scanf和printf可移植而且高效率,但是它们家族都还不够完美。尤其是它们都不具备type-safe性质,也都不可扩充。而type safety 和extensibility正是C++的基石组成部分。再者,scanf和printf函数家族将变量与控制读写的格式化信息分离开来,读写形式不够简单统一。
注意1.有些iostream的实现效率不如相应的C stream,故不同选择可能会给程序带来很大的不同。这只是对一些特殊的实现而言;
注意2.在标准化的过程中,iostream库在底层做了很多修改,所以对那些要求最大可移植性的应用程序来说,会发现不同版本遵循标准的程度也不同。
注意3.iostream库的类有构造函数而<stdio.h>里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会带来隐患,用标准C库会更简单实用。
条款3:尽量用new和delete而不用malloc和free
malloc和free对constructors 和 destructors一无所知,由此引发的问题将是对象的初始化难度以及内存泄漏。将malloc/free和new/delete混合使用其结果未定义[1],会带来很多麻烦。
条款4:尽量使用c++风格的注释形式
理由:C的多行注释/*…*/不支持嵌套使用。
内存管理 Memory Management
对C++程序员而言,把事情作对,意味着正确使用new和delete。而要让它们更有效率则意味着定制自己的operator new和operator delete。
条款5:使用相同形式的new和delete
使用new时会有两件事情发生:内存通过operator new被配置,然后一个(或)多个constructor(s)针对此内存被调用。使用delete时也会有两件事情发生:一个(或)多个的destructor(s)针对此内存被调用,然后内存通过operator delete被释放。
delete的最大问题在于不知道即将释放的内存内究竟存在多少对象,必须由程序员指出。故delete是否使用[ ]一定要与new是否使用[ ]保持一致,否则结果未定义,最大的可能是memory leak。
1.含有pointer data member并供应多个constructors的class,应在constructors中使用相同形式的new(包括不使用new)将pointer member初始化。否则,在destructor中就不知道该使用什么形式的delete。
2.最好不要对数组类型使用typedef动作!不然,要加倍小心以保证delete与new的使用形式相同。
条款6:记得在destructor中以delete对付pointer members
1.在class中每加上一个pointer member时,几乎总要相应做以下每件事:
n 在每个constructors中将该指针初始化。若没有任何一个constructor会为该指针分配内存,那么该指针应初始化为0。
n 在assignment运算符中将该指针原有的内存删除,重新配置一块。
n 在destructor中delete这个指针。
2.delete一个null pointer是安全的,什么也不做。delete一个指向合法内存的pointer后,该pointer并不为null pointer,再次delete该pointer将是非法操作。
3.不要以delete来对付一个未曾以new完成初始化的pointer。除了smart pointer objects之外,几乎绝对不要delete一个传递而来的指针。
条款7:为内存不足的状况作准备
1. operator new 无法配置出所需内存时将丢出一个std::bad_alloc exception。使用”nothrow objects”形式的new则可以像老的编译器一样,直接返回0。
2. operator new在无法满足需求而丢出exception前会先调用client专属的错误处理函数,该函数通常称为new-handler。operator new会不断重复调用new-handler函数,直至找到足够的内存为止。client必须调用set_new_handler来指定这个new-handler。在<new>中大致定义如下:
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw(); //返回值指向上次指定的new_handler
可以利用new-handler实现一个简单的错误处理策略。
3. 一个设计良好的new-handler函数必须实现下面功能中的一种:
n 产生更多的可用内存。一个方法是:程序启动时分配大的内存块,在第一次调用new-handler时释放。释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。
n 安装另一个不同的new-handler函数。如果当前的new-handler函数无法产生更多可用内存,它知道另一个new-handler可以提供更多的资源,这样它就可以安装另一个new-handler来取代它。另一个变通策略是让new-handler可以改变自己的运行行为,使得下次调用时可以做不同的事,方法是使new-handler可以修改那些影响它自身行为的static或global数据。
n 卸除new-handler。也就是传递null指针给set_new_handler。没有安装new-handler,operator new分配内存不成功时就会抛出一个标准的std::bad_alloc类型的异常。
n 抛出std::bad_alloc或从std::bad_alloc继承的其他类型的exception。这样的exceptions不会被operator new捕捉,所以它们会被送到最初提出内存请求的地方。抛出别的不同类型的exception会违反operator new的异常规范。规范中的缺省行为是调用abort,所以new-handler要抛出一个exception时,一定要确信它是从std::bad_alloc继承来的。
n 不返回,直接调用abort或exit。abort、exit可以在标准c库和c++库中找到。
4. 要想为class X加上“set_new_handler支持能力”,只需令X继承自NewHandlerSupport class template。
template<class T> // "mixin-style" base class for class-specific
class NewHandlerSupport {
public: // set_new_handler support
static new_handler set_new_handler(new_handler p);
static void * operator new(size_t size);
private:
static new_handler currentHandler;
};
template<class T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p)
{
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<class T>
void * NewHandlerSupport<T>::operator new(size_t size)
{
new_handler globalHandler = std::set_new_handler(currentHandler);
void *memory;
try {
memory = ::operator new(size);
}
catch (std::bad_alloc&) {
std::set_new_handler(globalHandler);
throw;
}
std::set_new_handler(globalHandler);
return memory;
}
// this sets each currentHandler to 0
template<class T>
new_handler NewHandlerSupport<T>::currentHandler;
条款8:撰写operator new和operator delete时应遵循的公约
撰写自己的内存管理函数时,其行为一定要与缺省行为保持一致。撰写operator new公约:正确的返回值,内存不足时调用错误处理函数new_handler,处理好0字节内存请求,避免隐藏标准形式的new。撰写operator delete公约:保证删除一个null指针永远是安全的。
1. 一个non-member operator new 的伪码:
void * operator new(size_t size) // your operator new might
{ // take additional params
if (size == 0) { // handle 0-byte requests
size = 1; // by treating them as
} // 1-byte requests
while (1) {
attempt to allocate size
bytes;
if (the allocation was successful)
return (a pointer to the memory);
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
2. 一个non-member operator delete 的伪码:
void operator delete(void *rawMemory)
{
if (rawMemory == 0) return; // do nothing if the null pointer is being deleted
deallocate the memory pointed to by rawMemory;
return;
}
3. operator new、operator delete 可被subclasses继承,有可能一个base class的operator new或operator delete会被用来配置或释放其derived class object,最好的办法是把错误数量的内存管理请求转给标准operator new或operator delete来处理,像下面这样:
class Base { // same as before, but now
public: // op. delete is declared
static void * operator new(size_t size);
static void operator delete(void *rawMemory, size_t size);
...
};
void * Base::operator new(size_t size)
{
if (size != sizeof(Base)) // if size is "wrong,"
return ::operator new(size); // have standard operator new handle the request
... // otherwise handle the request here
}
void Base::operator delete(void *rawMemory, size_t size)
{
if (rawMemory == 0) return; // check for null pointer
if (size != sizeof(Base)) { // if size is "wrong,"
::operator delete(rawMemory); // have standard operator
return; // delete handle the request
}
deallocate the memory pointed to by rawMemory;
return;
}
4. 撰写member function operator new[ ]唯一要记住的是配置生鲜内存raw memory,因为不知道数组中每个对象元素的大小,因此不可对数组尚未存在的对象做任何其它动作。
条款9:避免遮掩了标准形式的new
问题:内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部和全局声明的两个相同名字的函数来说,类的成员函数会隐藏掉全局函数:
class X {
public:
void f();
// operator new allowing specification of a new-handling function
static void * operator new(size_t size, new_handler p);
};
void specialErrorHandler(); // definition is elsewhere
X *px1 = new (specialErrorHandler) X; // calls X::operator new
X *px2 = new X; // error!
方案1:写一个class专属的operator new,令它和标准new有相同的调用方式。可以用一个高效的inline函数封装实现:
class x {
public:
void f();
static void * operator new(size_t size, new_handler p);
static void * operator new(size_t size)
{ return ::operator new(size); }
};
方案2:为operator new的每一个额外参数提供缺省值:
class X {
public:
void f();
static void * operator new(size_t size, new_handler p = 0);
};
条款10:如果写了operator new,请对应写operator delete
1. 自己撰写operator new和operator delete,通常是为了效率。因为缺省版的operator new是一种通用内存分配器,它须能分配任意大小的内存块,同样,operator delete也要可以释放任意大小的内存块。operator delete想知道释放的内存块有多大,就必须知道当初operator new分配的内存块有多大。一种常用的方法,就是在operator new返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。
2. 以下是定制operator new和operator delete的一段源代码:
class Airplane { // modified class — now supports
public: // custom memory management
static void * operator new(size_t size);
static void operator delete(void *deadObject, size_t size);
...
private:
union {
AirplaneRep *rep; // for objects in use
Airplane *next; // for objects on free list
};
static const int BLOCK_SIZE;
static Airplane *headOfFreeList;
};
Airplane *Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;
void * Airplane::operator new(size_t size)
{
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane *p = headOfFreeList;
if (p)
headOfFreeList = p->next;
else {
Airplane *newBlock =
static_cast<Airplane*>(::operator new(BLOCK_SIZE *sizeof(Airplane)));
for (int i = 1; i < BLOCK_SIZE-1; ++i)
newBlock[i].next = &newBlock[i+1];
newBlock[BLOCK_SIZE-1].next = 0;
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
void Airplane::operator delete(void *deadObject, size_t size)
{
if (deadObject == 0) return;
if (size != sizeof(Airplane)){
::operator delete(deadObject);
return;
}
Airplane *carcass = static_cast<Airplane*>(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
3. 以上代码体现了内存池的概念,并非memory leak。
4. 如果要删除的对象是从一个没有虚析构函数的类继承而来的,那传给operator delete的size_t值可能不正确。这就是必须保证基类必须要有虚析构函数的原因之一。
构造函数、析构函数和Assignment运算符
条款11: 为需要动态分配内存的class声明一个copy constructor和一个assignment运算符
1. 只要程序中有pass-by-value的动作,就会调用copy constructor。实际不是这样,关键在编译器。
2. 由memberwise assignment动作产生的指针别名pointer aliasing问题,可能导致 memory leak以及指向同一块内存的指针被delete多次<该结果未定义>。
3. 针对别名问题的解决之道:如果class拥有任何指针,撰写自己的copy constructor和assignment operator,在这些函数中,可以将指针所指之数据结构复制一份,使每个对象拥有属于自己的一份拷贝;也可以实现某种reference-counting策略,追踪记录指向某个数据结构的对象个数。
条款12: 在constructor中尽量以initialization取代assignment
1. const members和reference members不能够被赋值,只能够被初始化,必须使用initialization.
2. 对象的构造分两个阶段:初始化data members和执行constructor。对于自定义类型member data,使用member initialization list成本只有一次constructor函数调用,而在constructor中使用operator = 会先调用default constructor,然后再调用assignment运算符。
3. 内建类型的non-const和non-reference对象,initialization和assignment并无操作上的差异。
4. static class member绝不应该在class的constructor中被初始化。
条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
1. nonstatic data members在class内的初始化次序与它们的声明次序相同,而与它们在member initialization list中出现的次序完全无关;而它们的destructors则总是以相反的次序被调用。
2. base class data members永远在derived class data members之前初始化。如果使用多重继承,base classes将以被继承的次序来初始化,同样与在member initialization list中出现的次序无关。
条款14: 确定基类有虚析构函数
1. 当经由一个base class pointer删除一个derived class object,而此base class有一个nonvirtual destructor时,其结果未定义,最可能的情况是在执行期未调用derived class的destructor。
2. 若class未含任何虚拟函数,往往意味着它并不是要被当作base class使用,此时令其destructor为virtual通常只是浪费空间——容纳不必要的vptr以及vtbl。
3. 有时希望将一个class定义为抽象类,但却没有适当的函数可选为pure virtual function,此时可声明一个pure virtual destructor。不过,必须为此destructor提供定义式!因为virtual destructor的运作方式是派生程度最深的class,其destructor最先被调用,然后是base class的destructor被调用。这意味着编译器会对该抽象类的destructor产生一个调用操作。
4. inline函数并不是一个独立存在的函数,如果将一个virtual destructor声明为inline,虽可以避免函数调用所需的额外负担,但编译器将必须在某个地方产生一个out-of-line函数副本。<vtbl中相关项正是指向该副本>
条款15: 让operator = 返回*this的引用
理由:如果不这样做,就会妨碍assignments串链,或是妨碍隐式类型转换,或者兼而有之。
定义:classname & classname::operator = (const classname & rhs){……return *this;}
条款16: 在operator = 中对所有data members赋值
1. class加入新的data member时,assignment运算符应同步修改!
2. derived class的assignment运算符有义务处理其base class的assignment动作!两种途径:
Base::operator = (rhs); //如果assignment运算符由编译器生成,有些编译器可能不支持;
static_cast<Base&>(*this) = rhs; //是指向Base object的reference,不是Base object本身!
3. 实现devived class的copy constructors时一定要确保调用base class的copy constructor而不是default constructor!这只需在devived class的成员初始化列表中为base class指定初值为rhs即可。
条款17: 在operator = 中检查是否“自己给自己赋值”
理由:效率、确保正确性。
1. assignment运算符在为其左侧对象配置新资源时,通常必须先释放该对象已有的资源。如果不作该项检查,极有可能使用已被自己强行释放的资源。
2. 对象等同object identity的判定:两个对象是否有相同的值或相同的地址。
3. 别名aliasing问题和对象等同object identity问题特别容易发生在reference和pointer身上。
4. 任何时候,在可能出现别名问题的函数中,必须将该可能性纳入考虑!
类与函数之设计与声明
设计良好的class,是一种挑战。关键在于,使得自定义的class所导出的类型和语言内建的类型并无区别,这才是优秀的设计。
条款18: 努力使接口神具而型微
1. 所谓“神具而型微”,是指通过该接口用户可以做他们合理要求的任何事情,但是尽可能让函数个数少,不会任何重复功能的member functions。
2. 接口函数愈多,客户愈不容易了解它;大型class接口的可维护性较差;长长的class定义,会导致长长的头文件,进一步导致长长的编译时间,从而影响整个项目的开发。
3. 对所有实用目的而言,friend函数是class接口的一部分。这意味着friend函数也应该被纳入class接口的神具而型微考虑。
条款19: 分清成员函数,非成员函数和友元函数
1. 区分成员函数,非成员函数和友元函数的规则:
虚拟函数必须是成员函数;
operator >>和operator <<必须是非成员函数;
需要对最左端的参数进行类型转换的函数必须是非成员函数;
非成员函数如需访问class的非公有成员,必须成为class的友元函数;
其它情况下都声明为成员函数。
2. 比较先进的编译器会在必要的时刻为每个函数的每一个参数身上执行隐式类型转换implicit type conversion,但explicit constructors不能作为隐式类型转换使用。
3. 尽量避免friend函数。
条款20: 避免public接口出现数据成员
理由:一致性、精确的存取控制、函数抽象性。
条款21: 尽可能使用const
1. 必须知道:
char *p = "Hello"; // non-const pointer,non-const data
const char *p = "Hello"; // non-const pointer,const data
char * const p = "Hello"; // const pointer, non-const data
const char * const p = "Hello"; // const pointer, const data
2. const最具威力的用途是作用于函数声明之上,它可以用来修饰函数的返回值、个别参数,以及整个成员函数。const member function的目的是为了指明仅可由const对象调用。
3. C++的一个重要性质:member functions即使是常量性有所不同,也可以重载。
4. mutable关键词施行于nonstatic const data member之上,可以有效解放其bitwise constness方面的束缚。
条款22: 尽量用“传引用”而不用“传值”
1. C语言中的每样东西都是passed by value。C++把pass-by-value当作缺省行为,除非另行指定,否则函数参数都是以实参的副本作为初值,而调用端所获得的亦是函数传回值的一个副本。
2. 对象以by value的方式传递,其实际意义是由该对象的copy constructor决定的,这可能会使pass-by-value成为代价很高的动作。同时,对象的destructor也必然会被调用。以by reference的方式传递参数,不会有任何的constructor或destructor会被调用,因为没有必要产生任何新对象。它的另一个好处是可避免所谓的“切割slicing问题”。
3. Passing by reference是一件美妙的事情,但会导致某种复杂性。最知名的问题就是别名aliasing问题。
4. reference的底层几乎都是以指针完成,因此passing by reference通常意味着传递的是指针。对于小尺寸对象,传值可能比传引用的效率更高一些。
条款23: 当你必须返回一个object时,不要试图返回一个reference
所谓reference只是一个符号、一个名称,一个既有对象的名称。任何时候看到一个reference,都应该立即问自己,它的另一个名称是什么,那个“既有对象”是否存在,如果不存在,请不要试图返回reference。
条款24: 在函数重载和参数缺省化之间谨慎抉择
一般而言,如果有合理的缺省值,而且只需要一个算法,那么最好是选择参数缺省化,否则使用函数重载。
条款25: 避免对指针类型和数值类型进行重载
理由:C++世界中有一种独特的情况,人们认为某个调用动作应该被视为模棱两可,可编译器却不这么认为,因为编译器必须作出抉择。
条款26: 当心潜在的模棱两可ambiguity状态
有很多可能及各种情况,在程序和程序库中隐藏着模棱两可状态。一个好的软件开发这应该时刻保持警惕,将它的出现几率降至最低。
例1:
class B; // forward declaration for class B
class A {
public:
A(const B&); // an A can be constructed from a B
};
class B {
public:
operator A() const; // a B can be converted to an A
};
void f(const A&);
B b;
f(b); // error! — ambiguous
例2:
void f(int);
void f(char);
double d = 6.02;
f(d); // error! — ambiguous
f(static_cast<int>(d)); // fine, calls f(int)
f(static_cast<char>(d)); // fine, calls f(char)
例3:
class Base1 {
public:
int doIt();
};
class Base2 {
public:
存取限制不能解除“因多继承而来的成员”的模棱两可状态!因此,即使此处doIt声明为private也不能改变模棱两可的事实。
void doIt();
};
class Derived: public Base1, // Derived doesn't declare
public Base2 { // a function called doIt
...
};
Derived d;
d.doIt(); // error! — ambiguous
d.Base1::doIt(); // fine, calls Base1::doIt
d.Base2::doIt(); // fine, calls Base2::doIt
条款27: 如果不想使用编译器暗自产生的member functions,就显式拒绝它
将相应的member functions声明为 private而且不要定义它们。
条款28: 尝试切割全局名字空间global namespace
client可以通过三种方式来访问名字空间里的符号:将名字空间中的所有符号全部引入某一用户空间scope;将个别符号引入某一用户空间;每次通过修饰符显式地使用某个符号。
类与函数之实现
条款29:避免传回内部数据的handles
1. 不允许通过一个const对象直接或间接调用一个non-const member function。
2. 问题常常发生在返回一个指向内部data的pointer或者reference的const member function身上,client可以通过这个暴露的handle来修改一个const对象的内部资料。这实质上违反了抽象性abstraction。解决问题的方法是要么让member function成为non-const,要么就不让它传回任何handle。
3. non-const member function传回handles也会导致麻烦,特别是返回暂时对象时。要知道:暂时对象的生命短暂,只能维持到“调用函数“之表达式结束时。此时,很可能出现dangling handles现象,因为该handles所指向的对象因暂时对象的死亡而消逝!
2002-6-23
条款30:避免成员函数返回这样的non-const pointer或reference,它们指向比该函数的存取级别还要低的members
1. 目的是拒绝让这些members的存取级别获得晋升从而让clients随意访问它们而不受存取级别的限制。
2. 特别注意:面对指针,要担心的不只是data members,还有member functions,因为函数有可能返回一个pointer to member function,而访问函数指针是没有存取限制的!
3. 如果一定要返回指向较低存取级别members的pointer或reference,请让它们成为const。
条款31:千万不要返回函数内局部对象或new得指针所指对象的reference
1. 函数返回时,控制权离开函数,函数内部的局部对象会自动析构。
2. 如果函数返回new得指针的所指对象,client必须在使用后delete通过operator &获得的对象指针。然而,有时这种指针是无法获得的,例如在一个链式表达式中,而且也没有理由强制client这样做。
3. 返回一个指向local static 对象的reference同样无法正确运作。如果someFunc是这样的函数,那么someFun(a) == someFun(b)恒为真!
条款32:尽量延缓变量定义式的出现
1. 不止应该延缓变量的定义,直到非得使用该变量为止,而且应该直到能给予它一个初值为止。
2. 理由:可以改善效率、增加清晰度,还可以降低变量的说明需求。
2002-6-24
条款33:明智地运用inlining
1. 不仅仅是免除函数调用成本,编译器最佳化机制通常用来浓缩那些不含函数调用动作的程序代码,所以当你inlining一个函数时,或许编译器就有能力在函数本体身上执行某种最佳化。
2. inline函数背后的整个观念是,将对此函数的调用动作以函数代码来取代,因此也导致了目标代码的增大。inline函数的定义几乎总是放在头文件中,这允许多个编译单元translation units得以含入相同的头文件并获得其中定义的inline函数。
3. inline指令就像register指令一样,只是对编译器的一种提示,而不是一个强制命令。一个表面上的inline函数,是否真是inline,必须视编译器而定。如果此函数被拒绝inline化,它将被视为一个non-inline函数,对此函数的调用动作就像正常函数调用动作一样被处理。在旧规则下,在每一个调用该函数的编译单元中的目标文件中都会产生一个该函数的定义,而且编译器视该函数为static从而消除连接时期的问题。此时,若该函数内还定义有local static变量,则该函数的每一个副本中都将拥有该变量的一份副本!在新规则下,不论牵扯的编译单元有几个,只有一个out-of-line的函数副本被产生出来。
4. 有时,编译器也会为一个已经成功inline的函数产生一个函数实体,比如在编译器或者程序员需要取该函数的地址时。
5. inline函数无法随着程序库的升级而升级,其改变将会导致所有用到它的程序全部重新编译。此外,inline函数中的static对象常会展现反直观的行为,因此如果函数含有static对象,通常避免将它声明为inline。还有,大部分除错器 debugger面对inline函数都无能为力。
6. 只要明智地运用,inline函数会是每一个C++程序员的无价之宝,作为软件开发者,其目标便是要识别出可以有效增进程序效率的百分之二十的程序代码然后将它inline,或是尽量拉扯直到每块代码都高效运作。
2002-6-27
条款34:将文件之间的编译依赖关系compilation dependencies降至最低
问题 由于在接口与实现分离上,C++表现的很不好:class定义式中内含的不只是接口规格,还包括某些实现细节。之所以这样,是因为编译器看到一个class object定义式时,它必须知道要配置多少空间,唯一办法是询问class定义式。这样便在class的定义文件与其含入文件之间形成一种编译依赖,结果只要某个class或者其所倚赖的其它classes有任何一个改变了实现代码,那么含有或者使用该class的所有文件就必须重新编译。
1. 将编译依赖性最小化的关键技术在于以对class声明的依存性来取代对class定义的依存性,即让头文件尽可能自我满足,或至少让它依赖于class的声明而不要依赖于class定义。三项具体建议:
ü 如果object references或者object pointers可以完成任务,就不要使用objects。可以只靠一个类型声明就定义出指向该类型的reference和pointer,但定义出某个类型的object,就要使用该类型的定义。
ü 尽量以class的声明取代class的定义。声明一个会用到某个class的函数,纵使它使用by value方式传递该class的参数或返回值,也是这样。此时,要提供该class定义的是clients。
ü 不要在头文件中再include其它头文件,除非不这样做就无法编译,把这个责任留给clients。尽可能手动声明所需要的classes。
2. Handle classes或Envelope classes是将接口与实现分离的一项有效技术,它们将所有的函数调用转交给相应的Body classes或Letter classes,由后者完成真正的工作。
3. 另一种不同于Handle classes的做法是Protocol class。根据定义,Protocol class没有任何实现代码,其作用是为derived classes指定一个接口,它往往没有data members,也没有constructors,只有一个virtual destructor和一组用来表示接口的纯虚拟函数。Protocol class的clients必须用pointers和references来写程序,因为Protocol classes不可能被实体化。Factory functions或称为virtual constructors通常扮演constructor的角色,它们传回一个指针,指向动态配置而来的对象,该对象是真正被实体化的derived classes对象。最好让Factory functions成为Protocol classes的static函数。
4. 实现Protocol class有两种最平常的机制:一种从Protocol classes继承接口规格,然后实现出接口中的所有函数;第二种则涉及到多重继承。
5. 不论Handle classes和Protocol classes都无法获得inline函数的好处,所有实用性的inline函数都要求处理实现细节,而Handle classes和Protocol classes的设计目的正是用来避免实现细节曝光。
2002-6-29
继承关系与面向对象设计Inheritance and Object-Oriented Design
条款35:确定你的public inheritance模塑出isa关系
1. 以C++完成面向对象程序设计,最重要的一个规则就是public inheritance意味着isa关系。如果令class D以public形式继承了class B,便是告诉编译器:每一个类型为D的对象同时也是一个类型为B的对象,但反之并不成立。但请注意,这并不意味着D数组是一种B数组!
2. 可适用于所有软件的完美设计是不存在的。所谓最佳设计,取决于这个系统现在与将来希望做什么事。为继承体系添加多余的classes,就像在classes之间构造不正确的继承关系一样,都是不良的设计。
2002-6-30
条款36:区分接口继承interface inheritance和实现继承implementation inheritance
1. 有关接口继承interface inheritance和实现继承implementation inheritance的三条:
ü 声明一个纯虚拟函数的目的是为了让derived classes只继承其接口;
ü 声明一个非纯虚拟函数的目的是为了让derived classes继承该函数的接口和缺省行为;
ü 声明非虚拟函数的目的是为了让derived classes继承函数的接口及其实现。
2. 可以为纯虚拟函数提供定义,也就是说可以未它提供一份实现代码。C++并不认为这是错误的,不过只能静态地调用它,即必须指定其完整的class名称。它可以用来实现一种安全机制,为一般非纯虚拟函数提供更安全的缺省行为。
3. 非虚拟函数代表的意义是为不变性凌驾于变异性之上,所以不应该在subclass中重新定义它。
条款37:绝对不要重新定义继承而来的非虚拟函数
从实务的角度来看,非虚拟函数是静态绑定的,如果撰写class D并重新定义继承自class B的非虚拟函数,那么D对象很有可能展现出精神分裂的行径:当该函数被调用时,任何一个D对象都可能表现出B或D的行为,决定因素不在对象本身,而在于指向该对象指指针当初的声明类型。
从理论的角度来看,所谓public inheritance意味着isa的关系,任何D对象都是一个B对象,B的subclasses一定会继承mf的接口与实现,如果D重新定义mf,设计就出现矛盾。 2002-7-1
条款38:绝对不要重新定义继承而来的缺省参数值
首先,重新定义一个继承而来的非虚拟函数永远是错误的行为;其次,虚拟函数是动态绑定的,而缺省参数值是静态绑定的,就是说可能会在调用一个定义于derived class内的虚拟函数时,却使用base class为它指定的缺省参数值!C++这样做完全是为了执行期的效率。
对象的静态类型,是程序声明它时所采用的类型;对象的动态类型,是对象目前所代表的类型,即动态类型可以表现出一个对象的行为模式,它可以在程序执行过程中改变。
2002-7-2
条款39:避免在继承体系中做向下转型cast down动作
1. 为了摆脱downcasts,不论花多少努力都是值得的,因为downcasts既丑陋又容易出错,而且还会导致程序代码难以理解、难以强化、难以维护。
2. 解决downcast的最佳办法是将转型动作以虚拟函数的调用取代,并让每一个虚拟函数有一个“无任何动作”的缺省实现代码,以便应用在并不想要施行该函数的任何classes身上;第二个办法是让类型更明确一些,使得声明式中的指针类型就是真正的指针类型。
3. 万不得已,请使用由dynamic_cast运算符提供的安全向下转型动作sefe downcasting,在转型成功时会传回一个隶属新类型的指针,如果失败则传回null指针。此时downcasting必然导致的if-then-else程序风格,比起使用虚拟函数,实在是拙劣之至,所以万非得已不要出此下策。
4. 任何时候不要写出根据对象类型判断的不同结果而做不同事情的代码!不要在程序中到处放条件判断式或switch语句,让编译器来做这件事吧。
2002-7-7
条款40:通过layering技术来模塑has-a或is-implemented-in-terms-of关系
所谓layering,是以一个class为本,建立另一个class,并令所谓layering class(外层)内含所谓layered class(内层)对象作为data member。某些时候,两个classes不是has-a的关系,此时public inheritance并不适合它们,而layering技术则是实现is-implemented-in-trms-of关系的最佳选择。不过同时也会在这些classes之间产生了一个编译依存关系,利用条款34的技术可以很好的解决这个问题。
条款41:区分inheritance和templates
template用来产生一群classes,其对象类型不会影响class的函数行为;inheritance用于一群classes身上,其中,对象类型会影响class的函数行为。
条款42:明智地运用private inheritance
1. 如果classes之间的继承关系是private,编译器通常不会自动将一个derived class object转换为一个base class object。由private base class继承而来的所有members,在derived class中都会变成private属性。
2. private inheritance意味着implemented-in-terms-of,使用这项技术的原因往往是想采用已经撰写于base class的某些程序代码,而不是因为derived class和base class之间有任何概念关系存在,即private inheritance意味着继承实现部分,接口部分略去。
3. 对于is-implemented-in-terms-of,应该尽可能使用layering,只有在涉及到protected member或虚拟函数时才使用private inheritance,因为唯有通过继承,才得以取用protected members;唯有通过继承,才允许虚拟函数被重新定义。
4. base class舍弃template技术而使用泛型指针void *来有效地遏制代码膨胀现象,并通过将constructors、destructors以及所有接口声明为protected,而将data members声明为private来阻止clients误用这个类;通过private inheritance,derived class继承实现部分代码,再使用template技术来建立类型安全type-safe的接口,同时将所有接口声明为inline来减少执行期成本。这样的设计带来的是最高的效率和最高的类型安全性,是一项巧妙的技术!
2002-7-5
条款43明智地运用多继承multiple inheritance,MI
1. MI的根本问题是产生了一大堆但继承中不存在的复杂性。最根本的复杂性是模棱两可ambiguity,其次是继承时是否应该使用virtual inheritance。
2. 考虑一个class的两个public base classes,如果它们均有一个相同虚拟函数,那么在derived class使用这个函数时,必须明确指定这个函数属于哪个base class,同时还不能在derived class中改写这个函数,不过此时可以通过添加两个classes来将这个class继承体系中单一而模棱两可的函数名称一分为二为两个明确的、操作性质等同的名称。
3. 在钻石形继承体系中,通常要令顶层class为virtual base class,这样底层derived class object中就不会内含多份顶层class的subobjects,但也意味着增加了程序执行时间和空间的额外成本,因为virtual base classes常常是以对象指针来实现,而不是以对象本身来完成。由此可见,要在多继承的情况下完成有效的class设计,程序员似乎得拥有优秀的洞察力才行。然而,决定一个base class是否应该成为virtual缺乏一种完好的高阶定义,该决定通常只取决于“整个”继承体系的结构。在该结构尚未明朗之前,无法做出决定。
4. 非虚拟继承时,base class constructor的实参是在下一层的derived class的成员初始表中指定,然而对于虚拟继承,实参是在派生程度最深most derived的classes的成员初始表中指定。因此,对virtual base执行初始化动作的那个class,在继承体系中可能距离其base相当远,而一旦有新的classes加入此体系,初始化动作可能就要由别的class来担当。解决此问题的好办法是消除传递constructor实参至virtual bases的必要性。最简单的做法是避免virtual base classes拥有data members。
5. 考虑如下钻石形继承体系:
class A{ virtual void mf(){}; }
class B : virtual public A{} // 继承默认的虚拟函数mf
class C : virtual public A{virtual mf(){};} // 改写虚拟函数mf
class D : public B, public C{}
代码D *pd = new D; pb->mf();中,D对象调用的mf会被编译器明确决议为C::mf。
6. 造成MI这么难用是因为,要让所有细节以某种合理的方式共同运作,必然会伴随某种复杂性。其实这些复杂都源于virtual base classes,所以尽量避免使用virtual bases即钻石形继承体系。
2002-7-13
条款44:说出你的意思并了解你所说的每一句话
继承关系与面向对象关系中最重要的一些观念:
ü 共同的base class意味着共同的特征;
ü public inheritance意味着isa;
ü private inheritance意味着is-implemented-in-terms-of;
ü layering意味着has-a或is-implemented-in-terms-of;
在牵涉到public inheritance时,以下几点才成立:
ü 纯虚拟函数意味着只有函数接口会被继承;
ü 一般虚拟函数意味着函数的接口及缺省实现代码会被继承;
ü 非虚拟函数意味着函数的接口和实现代码都会被继承。
2002-7-7
条款45:清楚知道C++编译器默默为我们完成那些函数
1. 一个空的class在编译器处理过它之后就不再为空,如果你写:class Empty{};其意义相当于:
class Empty{
public:
Empty();
Empty(const Empty& rhs);
~Empty();
Empty& operator = (const Empty& rhs);
Empty* operator& ();
const Empty* operator& () const;
}
2. 这些函数只有在需要时,编译器才会定义它们。default constructor和destructor不做任何事情,只是让你得以产生和摧毁这个class的对象,并提供一个便利场所让编译器放置一些用来完成幕后动作的代码;产生出来的destructor并非虚拟函数,除非这个class继承自一个拥有virtual destructor的base class;缺省的address-of运算符只负责传回对象地址;对于缺省copy constructor或assignment运算符,官方规则是对该class的nonstatic data members执行memberwise copy construction或assignment动作。
3. 编译器为class默默产生的assignment运算符只有在其函数代码不但合法而且合理的情况下才会有正确行为,否则编译器会拒绝产生一个operator =,并报告编译错误。因此,如果打算让内含reference member或const member的class支持assignment动作,必须自行定义assignment运算符。如果base classes将标准的assignment运算符声明为private,编译器也会拒绝为其derived class产生assignment运算符。
2002-7-8
杂项讨论 Miscellany
条款46:宁愿编译和连接时出错,也不要执行时才错
不做执行期检验工作,程序会更小更快;尽可能将错误侦测工作交给连接器来做,或最好是交给编译器来做,这样做的好处不仅是程序大小的降低和数度的增加,好包括可信度的提升。相反,在执行期侦测错误,比起在编译期或连接期捕捉它们要麻烦许多。通常,只需稍稍改变设计,就可以在编译期捕捉除可能的执行期错误,这往往要加入新的类型。
条款47:使用non-local static objects之前先确定它已有初值
所谓non-local static objects,是指定义于global、namespace scope、class或者file scope内的static objects。每当在不同的编译单元内定义有non-local static objects时,想要决定它们以适当的次序初始化是极度困难甚至无法办到的事情。最常见的形式是,在多个编译单元中,non-local static objects被隐式的template具现化行为产生出来,这样不但不可能决定正确的初始化次序,甚至不值得我们寻找有可能决定正确次序的特殊情况。
解决问题的方案是不再取用non-local static object,而改用行为类似non-local static objects的对象。这项被称为Singleton pattern的技术很简单:首先,将每个non-local static object移到一个它专属的函数中,在那里将它声明为static;然后,令这个函数传回一个reference,指向这个static object。这项技术的关键是以函数内的static objects取代non-local static objects,其依据是C++虽然对于何时初始化一个non-local static object没有任何明确表示,但它却非常明白的指出函数中的static object的初始化时机。使用这项技术可以保证所获得的references一定指向已经初始化妥当的对象,同时,如果从未调用这样的函数还可以避免对象的构造成本和析构成本。
2002-7-10
条款48:不要对编译器的警告信息视而不见
警告信息天生就和编译器相依相靠,所以轻率地依赖编译期为你找出程序错误,决不是什么好主意。编译器的用途基本上是将C++代码转换为可执行代码,而不是一张安全网。
条款49:尽量让自己熟悉C++标准程序库
1. 为了避免程序员使用的class名称或函数名称与标准程序库所提供的名称相冲突,标准程序库的每一样东西几乎都驻在namespase std之中。由此而来的问题是,世上有无可数计的C++代码依赖那些已使用多年的准标准程序库,那些软件并不知道什么是namespace。以下是C++表头文件的组织状态:
ü 旧有的C++头文件如<iostream.h>尽管不是官方标准,但有可能继续存在。这些头文件的内容不在namespace std中;
ü 新的C++头文件如<iostream>包含的基本功能和对应的旧头文件相同,但其内容在namespace std中。(在标准化的过程中,程序库中有些组件细节稍有修改,所以新旧头文件中的实体不一定完全对应。)
ü 标准C头文件如<stdio.h>继续被支持。这类头文件的内容不在std中。
ü 具有C库功能的新C++头文件具有如<cstdio>这样的名字。它们提供的内容和相应的旧C头文件相同,但全部放在std中。
2. 关于标准程序库,必须知道的第二件事是,几乎每一样东西都是template。string、complex、vector都不是class而是class template;cin的真正类型是basic_istream<char>,string的真正类型是basic_string<char>。
3. C++标准程序库内的主要组件:
ü C标准程序库,它有些小小的改变,但整体而言改变不大。
ü iostreams。和传统的iostream相比,它已被template化了。它的继承体系已被修改,内容被扩充以便可以抛出异常信息,同时它可以支持strings和多国文化。它依旧支持stream buffers、formatters、manipulators、files以及cin、cout、cerr、clog等对象。
ü strings。
ü containers。C++ 标准库为vectors、lists、queues、stacks、deques、maps、sets和bitsets提供了高效的实现。同时,strings也是containers!
ü algorithms。用以轻松操作标准containers,它们大部分都适用于所有containers以及语言内建的数组身上。
ü 国际化internationalization支持。其主要组件是facets和locales。前者描述某一文化的特殊字符集应该如何处理,包括校对规则、日期和时间表示法、信息代码与自然语言之间的映射关系等等。后者含有一整组facets。C++允许多个locales同时在程序库中起作用,所以同一个程序的不同部分可能会采用不同的规则。
ü 数值处理。
ü 诊断功能。标准程序库提供三种方法来记录错误:经由C的assertions、经由错误代码、经由异常信息。
2002-7-11
条款50:加强自己对C++的了解
C++最主要的几个设计目标:与C兼容、高效率、与传统工具和开发环境兼容、解决问题的真正能力,它的目的是成为专业程序员在各种不同领域中解决真正问题的一个威力强大的工具。这些目标可以解释C++语言的许多来龙去脉。
2002-7-13