您的位置:首页 > 编程语言 > C语言/C++

《Effective C++(第三版)》读书笔记

2016-11-09 16:13 267 查看
结合《Effective C++(第三版)》书和一些技术博客总结的读书笔记,后续还会更新完善
一.让自己习惯C++

条款01:视C++为一个语言联邦
为了更好的理解C++,我们将C++分解为四个主要次语言:

C。说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。

Object-Oreinted C++。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...

Template C++。这是C++泛型编程部分。

STL。STL是个template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...

请记住:

这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

条款02:尽量以const,enum,inline替换#define
&
4000
nbsp;这个条款或许可以改为“宁可 以编译器替换预处理器”。即尽量少用预处理。
编译过程:.c文件--预处理-->.i文件--编译-->.o文件--链接-->bin文件
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是以#号开头的代码行。
例:#define ASPECT_RATIO
1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未进入记号表(symbol table)。
替换:const double
AspectRatio = 1.653;
好处应该有:多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。
常量替换#define两点注意:

定义常量指针:

const char
*authorName = “Shenzi”;
cosnt std::string
authorName("Shenzi");

类专属常量:

static const int
NumTurns = 5;//static 静态常量 所有的对象只有一份拷贝。
万一你的编译器不允许在类的声明中设定静态整型的初值,我们可以通过枚举类型予以补偿:
enum {
NumTurns = 5 };
*取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获取一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。
例:#define CALL_WITH_MAX(a,b)
f((a) > (b)) ? (a) : (b))
宏看起来像函数,但不会招致函数调用带来的额外开销,而是一种简单的替换。
替换:
template<typename T>
inline void
callWithMax(cosnt T &a, cosnt T &b)
{
f(a > b ? a : b);
}
callWithMax是个真正的函数,它遵循作用域和访问规则。
请记住:

对于单纯常量,最好以const对象或enums替换#defines;

对于形似函数的宏,最好改用inline函数替换#defines。

条款03:尽可能使用const
const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。
关键字const多才多艺:
例:
char greeting[] = "Hello";
char *p = greeting; //指针p及所指的字符串都可改变;
const char
*p = greeting; //指针p本身可以改变,如p = &Anyother;p所指的字符串不可改变;
char * cosnt p
= greeting; //指针p不可改变,所指对象可改变;
const char
* const p = greeting; //指针p及所致对象都不可改变;
说明:

如果关键字const出现在星号*左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
STL:
C++的STL中使用迭代器作为接口,它定义了普通的迭代器,如vector<T>::iterator,也定义了指向常量的迭代器,如vector<T>::const_iterator,初学者可能想当然地认为const
vector<T>::iterator等价于vector<T>::const_iterator,其实不是这样的,const vector<T>::iterator表示这个迭代器的指向不可以更改,即表示的是常量迭代器。
以下几点注意:

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而不至于放弃安全性和高效性。

const成员函数使class接口比较容易被理解,它们使“操作const对象”成为可能;

当const的变量传递给非const的变量会不安全,而当非const的变量传递给const变量时,不存在安全问题

成员函数后面[b]有无const是可以构成成员函数的重载的,const修饰成员函数的参数(只有指针和引用类型的参数)也是可以构成重载的

声明为const的成员函数(const加在后面),不可改变非静态成员变量,在成员变量声明之前添加mutable可让其在const成员函数中可被改变。const加在成员函数的前面修饰返回类型

请记住:

将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;

编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的车辆”(conceptual constness);

当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

如果两个成员函数只是常量性不同,可以被重载。

条款04:确定对象被使用前已先被初始化
永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其它东西,初始化责任落在构造函数身上,确保每一个构造函数都将对象的每一个成员初始化。
赋值和初始化:
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。所以应将成员变量的初始化置于构造函数的初始化列表中。
ABEntry::ABEntry(const std::string& name, const std::string&
address,
const std::list<PhoneNumber>& phones)
{
theName = name; //这些都是赋值,而非初始化
theAddress = address; //这些成员变量在进入函数体之前已调用默认构造函数,接着又调用赋值函数,
thePhones = phones; //即要经过两次的函数调用。

numTimesConsulted = 0;
}

ABEntry::ABEntry(const std::string& name, const std::string&
address,
const std::list<PhoneNumber>& phones)
: theName(name), //这些才是初始化
theAddress(address), //这些成员变量只用相应的值进行拷贝构造函数,所以通常效率更高。
thePhones(phones),
numTimesConsulted(0)
{ }
所以,对于非内置类型变量的初始化应在初始化列表中完成,以提高效率。而对于内置类型对象,如numTimesConsulted(int),其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化表来初始化。如果成员变量是const或reference,它们就一定需要初值,不能被赋值。
C++有着十分固定的“成员初始化次序”。基类总是在派生类之前被初始化,而类的成员变量总是以其说明次序被初始化。所以:当在成员初始化列表中列各成员时,最好总是以其声明次序为次序。
请记住:

为内置对象进行手工初始化,因为C++不保证初始化它们;

构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;

为免除“跨编译单元之初始化次序”问题,请以local static对象替换全局static对象。

二.构造/析构/赋值运算
几乎你写的每个类都会有一或多个构造函数、一个析构函数、一个拷贝赋值操作符。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及整个类。所以确保它们行为正确是生死攸关的大事。

条款05:了解C++默默编写并调用哪些函数
class Empty
{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
如果你自己没声明,编译器就会为类声明(编译器版本的)一个拷贝构造函数,一个拷贝赋值操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会成为你声明一个默认构造函数和两个取址运算符。所有这些函数都是public且inline。
有需求,编译器才会创建它们。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数(要不然它们该在哪里被调用呢?)。
注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数。
至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
如一个类声明了一个构造函数(无论有没参数),编译器就不再为它创建默认构造函数。
编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。

如果某个base class将copy assignment operator声明为private,编译器拒绝为其derived classes生成一个copy assignment operator
请记住:

编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符、两个取址操作符,以及析构函数。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝
由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。通过这个小“伎俩”可以阻止人们在外部调用它,但是类中的成员函数和友元函数还是可以调用private函数。解决方法可能是在一个专门为了阻止拷贝动作而设计的基类:

class Uncopyable

{

protected:

Uncopyable(){}

~Uncopyable(){}

private:

Uncopyable(const Uncopyable&);

Uncopyable& operator= (const Uncopyable&);

};

class HomeForSale: public Uncopyable

{…};
请记住:

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像noncopyable这样的基类也是一种做法。

条款07:为多态基类声明virtual析构函数
当基类的指针指向派生类的对象的时候,当我们使用完,对其调用delete的时候,其结果将是未有定义——基类成分通常会被销毁,而派生类的成分可能还留在堆里。这可是形成资源泄漏、败坏之数据结构、在调试器上消费许多时间。
消除以上问题的做法很简单:给基类一个virtual析构函数。此后删除派生类对象就会如你想要的那般。
任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为实现virtual函数,需要额外的开销(指向虚函数表的指针vptr)。
STL容器都不带virtual析构函数,所以最好别派生它们。
请记住:

带有多态性质的基类应该声明一个virtual析构函数。如果一个类带有任何virtual函数,它就应该拥有一个virtual析构函数。

一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。

条款08:别让异常逃离析构函数

当异常发生时,如果异常发生在一个try块内部,程序就会跳出该try块,并逐层寻找匹配的catch,跳出try块的过程中,会销毁该try内创建的对象并调用析构函数,如果调用析构函数的过程中又发生异常,程序就会调用标准库terminate函数(terminate函数调用abort函数)结束执行
C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。
避免异常逃离析构函数:

方法一:在析构函数内设置try块,一旦发生异常,立即终止程序

方法二:吞下func发生的异常

方法三:前两者都无法对"导致func抛出异常"做出什么反应,另一个方法是避免func()在析构函数内执行,由客户来调用func函数,为避免客户忘记执行,需设立flag标记客户是否调用,如果客户没有调用,在析构函数内调用该函数
请记住:

析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:绝不在构造和析构过程中调用virtual函数
因为:基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?
唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。
解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
请记住:

在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。

//如果我在一个基类的构造函数内调用基类的虚函数,会失去多态性。

条款10:令operator= 返回一个reference to *this
对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:
int x, y, z;
x = y = z = 15;
为了实现“连续赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
即:
Widget & operator
= (const Widget &rhs)
{
...
return *this;
}
所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。
请记住:

令赋值操作符返回一个reference to *this。

条款11:在operator =中处理“自我赋值”
先举几个自我赋值的例子:
例:Widget w;
w = w;
a[i] = a[j]; //i == j or i != j
*px = *py;// px,py指向同个地址;
以上情况都是对“值”的赋值,但我们涉及对“指针”和“引用”进行赋值操作的时候,才是我们真正要考虑的问题了。
看下面的例子:
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
//这里对pb指向内存对象进行delete,试想 *this == rhs?情况会如何
pb = new Bitmap(*rhs.pb); //如果*this
== rhs,那么这里还能new吗?“大事不妙”。
return *this;
}
也许以下代码能解决以上问题:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this; //解决了自我赋值的问题。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码。”,以上代码同样存在异常安全问题。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一个复本
delete pOrig; //删除原先的pb
return *this; //这样既解决了自我赋值,又解决了异常安全问题。自我赋值,将pb所指对象换了个存储地址。
}
请记住:

确保当对象自我赋值时operator =有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款12:复制对象时勿忘其每一个成员
还记得条款5中提到编译器在必要时会为我们提供拷贝构造函数和拷贝赋值函数,它们也许工作的不错,但有时候我们需要自己编写自己的拷贝构造函数和拷贝赋值函数。如果这样,我们应确保对“每一个”成员进行拷贝(复制)。
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。
在派生类的构造函数,拷贝构造函数和拷贝赋值操作符中应当显式调用基类相对应的函数,否则编译器可能又“自作聪明了”。
当你编写一个copying函数,请确保:
(1)复制所有local成员变量;
(2)调用所有基类内的适当copying函数。
错误用法:
(1).令copy assignment操作符调用copy构造函数是错误的,因为在这就像试图构造一个已存在的对象。
(2).令copy构造函数调用copy assignment操作符同样是错误的。构造函数用来初始化对象,而assignment操作符只实行与已初始化的对象身上。对一个尚未构造好的对象赋值,就像在一个尚未初始化的对象身上做“只对已初始化对象才有意义”的事意义。
注意:

消除copy构造函数与copy assignment操作符重复代码的做法是:建立一个新的成员函数给两者调用。这样的函数往往是private而且被命名为init。这个策略可以安全消除copy构造函数与copy assignment操作符之间的代码重复。
请记住:

Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成员”;

不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

三.资源管理
所谓资源就是,一旦用了它,将来必须还给系统。C++程序中最常使用的资源就好似动态分配内存(如果你new了,却忘了delete,会导致内存泄露),但内存只是你必须管理的众多资源之一。其它常见的有文件描述符(file
descriptors)、互斥器(mutex)、图形界面中的字形和画刷。数据库连接以及网络sockets。当你不使用它们时,记得还给系统。
当你考虑到异常、函数内多重回传路径、程序维护员改动软件却没能充分理解随之而来的冲击,那么资源管理就显得复杂的多。

条款13:以对象管理资源
例:
void f()
{
Investment *pInv = createInvestment();
... //这里存在诸多“不定因素”,可能造成delete
pInv;得不到执行,这可能就存在潜在的内存泄露。
delete pInv;
}
解决方法:把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。
auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
“以对象管理资源”的两个关键想法:

获得资源后立刻放进管理对象内(如auto_ptr)。每一笔资源都在获得的同时立刻被放进管理对象中。“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。

管理对象运用析构函数确保资源被释放。即一旦对象被销毁,其析构函数被自动调用来释放资源。

由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。所以auto_ptr若通过copying函数复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权!
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“有你没我,有我没你”。
auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;SCSP)、它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。
void f()
{
...
std::tr1::shared_ptr<Investment>
pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr<Investment>
pInv2(pInv1); //pInv1,pInv2指向同一个对象;
pInv1 = pInv2; //同上,无变化
...
} //函数退出,pInv1,pInv2被销毁,它们所指的对象也被自动释放。
auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr是个潜在危险,资源得不到释放。也许boost::scoped_array和boost::shared_array能提供帮助。还有,vector和string几乎总是可以取代动态分配而得的数组。
请记住:

为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。

条款14:在资源管理类中小心拷贝行为
RAII:资源取得时机便是初始化时机(Resource Acquisition Is Initialization) 区别:RTTI
我们在条款13中讨论的资源表现在堆上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。
例:
void lock(Mutex *pm); //锁定pm所指的互斥量
unlock(Mutex *pm); //将pm解除锁定
我们建立的资源管理类可能会是这样:
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
但是,如果Lock对象被复制,会发生什么事???
“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择以下两种可能:

禁止复制。如果复制动作对RAII类并不合理,你便应该禁止之。禁止类的copying函数参见条款6。

对底层资源使用”引用计数法“。有时候我们又希望保有资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的”被引用计数“递增。tr1::shared_ptr便是如此。

通常只要内含一个tr1::shared_ptr成员变量,RAII类便可实现”引用计数“行为。
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm, unlock)
//由于tr1::shared_ptr缺省行为是”当引用计数为0时删除其所指物“,幸运的是,我们可以指定”引用计数“为9时被调用的所谓”删除器“,即第二个参数unlock
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
本例中,并没说明析构函数,因为没有必要。编译器为我们生成的析构函数会自动调用其non-static成员变量(mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥量”引用计数“为0时自动调用tr1::shared_ptr的删除器(unlock)。
Copying函有可能被编译器自动创建出来,因此除非编译器所生成版本做了你想要做的事,否则你得自己编写它们。
请记住:

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用计数法。不过其它行为也可能被实现。

条款15:在资源管理类中提供对原始资源的访问
前几个条款提到的资源管理类很棒。它们是你对抗资源泄漏的堡垒。但这个世界并不完美,许多APIs直接指涉资源,这时候我们需要直接访问原始资源。
这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源。有两种做法可以达成目标:显示转换和隐式转换。
tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式转换,也就是返回智能指针内部的原始指针(的复件)。就像所有智能指针一样,tr1::shared_ptr和auto_ptr也重载了指针取值操作符(解引用)(operator->和operator*),它们允许隐式转换至底部原始指针。(即在对智能指针对象实施->和*操作时,实际被转换为被封装的资源的指针。)
class Font
{
public:
...
FontHandle get() const //FontHandle 是资源;显式转换函数
{
return f;
}
operator FontHandle() const //隐式转换 这个值得注意,可能引起“非故意之类型转换”
{
return f;
}
...
};
是否该提供一个显示转换函数(例如get成员函数)将RAII类转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII类被设计执行的特定工作,以及它被使用的情况。
显式转换可能是比较受欢迎的路子,但是需要不停的get;而隐式转换又可能引起“非故意之类型转换”。
请记住:

APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理之资源”的方法。

对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

条款16:成对使用new和delete时要采取相同形式
先看下一下代码:
std::string *stringArray = new std::string[100];
...
delete stringArray;
使用了new动态申请了资源,也调用了delete释放了资源。但这代码存在“不明确行为”。stringArray对象中的99个不太可能被适当删除,因为它们的析构函数很可能没被调用。
当我们使用new,有两件事情发生:第一,内存被分配出来;第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放。delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。
解决以上问题事实上很简单:如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。
最好尽量不要对数组形式作typedefs动作。因为这样容易引起delete操作的“疑惑”(需不需要[]呢???)。
请记住:

如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

条款17:以独立语句将newed对象置入智能指针
为了避免资源泄漏的危险,最好在单独语句内以智能指针存储newed所得对象。
即:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw,
int priority);
std::tr1::shared_ptr<Widget> pw(new Widget); //即在传入函数之前对智能指针初始化,而不是在传入参数中
//对其初始化,因为那样可能引起操作序列的问题。
processWidget(pw, priority());
请记住:

以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。

四.设计与声明
所谓软件设计,是“令软件做出你希望它做的事情”的步骤和做法,通常以颇为一般性的构想开始,最终变成十足的细节,以允许特殊接口的开发。

条款18:让接口容易被正确使用,不易被误用
理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码不该通过编译;如果代码通过了编译,它的作为就该是客户所想要的。
欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。
许多客户端错误可以因为导入新类型而获得预防。在防范“不值得拥有的代码”上,类型系统是你的主要同盟国。
struct Day
{
explicit Day(int
d) //explicit 避免隐式的转换。
:val(d) {}
int val;
};
对日期进行类似的类型封装,能有效地避免不恰当的日期赋值。
“除非有好的理由,否则应该尽量令你的类型(定义的类)的行为与内置类型一致”。
在资源管理方面,也许我们应该“先发制人”,即让函数返回一个资源的指针改为返回一个智能指针。
例如:
std::tr1::shared_ptr<Investment> createInvestment();
这便实质上强迫客户将返回值存储于一个tr1::shared_ptr内,几乎消除了忘记删除底部Investment对象的可能性。
tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时被调用的“删除器”。但我们自己制定第二个参数,当然这是安全的。但是留给客户,那也许存在危险。
std::tr1::shared_ptr<Investment> //tr1::shared_ptr构造函数坚持第一个参数必须是个指针。
pInv(static_cast<Investment*>(0),
getRidOfInvestment);
tr1::shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的“cross-DLL
problem”。因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。
请记住:

好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。

“促进正确使用”的办法包括:接口的一致性,以及与内置类型的行为兼容。

“阻止误用”的办法包括:建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥量等等。

条款19:设计class犹如设计type
C++就像在其它面向对象编程语言一样,当你定义一个新class,也就定义了一个新type。这意味着你并不只是类的设计者,更是类型的设计者。重载函数和操作符、控制内存的分配和归还、定义对象的初始化和终结......全部在你手上。
设计优秀的类是一项艰巨的工作,因为涉及好的类型是一项艰巨的工作。好的类型有自然的语法,直观的语义,以及一或多个高效实现品。
设计一个良好的类,或者称作类型,考虑一下设计规范:

新类型的对象应该如何被创建和销毁?

对象的初始化和对象的赋值该有什么样的差别?

新类型的对象如果被passed by value(值传递),意味着什么?

什么是新类型的“合法值”?

你的新类型需要配合某个继承图系吗?

你的新类型需要什么样的转换?

什么样的操作符和函数对此新类型而言是合理的?

什么样的标准函数应该驳回?

谁该取用新类型的成员?

什么是新类型的“未声明接口”?

你的新类型有多少一般化?

你真的需要一个新类型吗?

请记住:

Class的设计就是type的设计。在定义一个新的type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。

条款20:宁以pass-by-reference-to-const替代psss-by-value
缺省情况下C++以by value方式传递对象至函数。除非你另外指定,否则函数参数都是以实际实参的副本为初值,而调用端所获得的亦是返回值的一个副本。这些副本由对象的拷贝构造函数产生。
所以在以对象为by value时,可能会调用相应的构造函数(成员对象的构造、基类对象的构造),然后调用对应的析构函数。所以以by
value的形式开销还是比较大的。
如果我们用pass-by-reference-to-const,例如:
bool validateStudent(const Student& s);
//const,希望别对传入对象进行不恰当的修改;
这种传递方式效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。
以传引用方式传递参数也可以避免对象切割问题:即当一个派生类对象以传值的方式传递并被视为一个基类对象,基类对象的拷贝构造函数会被调用,而“造成此对象的行为像个派生类对象”的那些特化性质全被切割掉了,仅仅留下了基类对象。这一般不是你想要的。
所以我们一般的做法应该是这样:内置对象和STL的迭代器和函数对象,我们一般以传值的方式传递,而其它的任何东西都以传引用的方式传递。
请记住:

尽量以pass-by-reference-to-const替代pass-by-value。前者通常比较高效,并可避免切割问题。

以上规则并不使用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

条款21:必须返回对象时,别妄想返回其reference
当我们领悟条款20中传值的开销后,总是避免于少用传值,然而在返回对象时,要格外小心了,因为你可能:传递一些引用或指针指向其实已经不存在的对象。这可不是件好事。
任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?
函数创建新对象的途径有二:在栈空间和堆空间
栈上:即在函数内的局部变量。局部变量在函数返回后就没有存在的意义,若还对它“念念不忘”,将带来灾难性后果。所以传引用在栈上不可行。
堆上:在堆上构造一个对象,并返回。看似可行,也埋下了资源泄漏的危险。谁该对这对象实施delete呢?别把这种对资源的管理寄托完全寄托于用户。所以传引用在堆上不可行。
可能还有一种想法:把“让返回的引用指向一个被定义于函数内部的静态对象”。出于我们对多线程安全性的疑虑,以及当线程中两个函数对单份对象的处理也可能带来不可测行为。所以静态对象也是不可行的。
一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。
编译器实现者实行最优化,用以改善产出码的效率却不改变其观察的行为。所以我们还是老老实实的返回一个对象吧。
请记住:

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

条款22:将成员变量声明为private
为什么不将成员变量声明为public呢?
<1>.如果变量都是private,那么用户只能通过函数来获得这个变量,而不用考虑“.”或者“->”后面的东西用不用加上“()”。
<2>.使用函数,可以对变量进行精确地控制:有的变量不许访问,有的只读,有的可以读写,甚至是可以“只写”。而且在函数中,可以处理用户输入的不合理的参数。
<3>.封装性。封装性意味着,当你有新的想法需要修改时,只需要改变函数内部的实现细节。只要函数的接口没有改变,那么所有使用这个函数的代码就不需要做改变。而假如你使用的数据,那么整个代码的修改量就会大很多。简而言之,public意味着不封装,所有public下的东西都是别人用的,你不能轻易修改它们。
而对于protected,前两点依然适用。而对于第三点,可以从另一个角度来说明:如果public变量被取消那么所有使用它的代码都会被取消。对于protected,则破坏的是他的派生类。这都是无法衡量的。而如果private变量被取消,我们只要修改本类中调用这个变量的函数就行了。
请记住:

切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保护,并提供class作者以充分的实现弹性。

protected并不比public更具封装性。

条款23:宁以non-member、non-friend替换member函数
一般我们相当然以为类中的成员函数更具封装性,而实际上并不是那么一回事,因为成员函数不仅可以访问private成员变量,也可以取用private函数、enums、typedefs等等。而非成员非友元函数能实现更大的封装性,因为它只能访问public函数。
将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。需要做的就是添加更多non-member
non-friend函数到此命名空间内。
请记住:

宁可拿non-member non-friend函数替代member函数。这样做可以增加封装性、包裹弹性和机能扩充性。

条款24:若所有参数皆需类型转换,请为此采用non-member函数
通常,令类支持隐式类型转换通常是个糟糕的主意。当然这条规则有其例外,最常见的例外是在建立数值类型时。
例:
const Rational operator*(const Rational& rhs) const;
如果定义一个有理数类,并实现*操作符为成员函数,如上所示;那么考虑一下调用:
Rational oneHalf(1, 2);
result = oneHalf *
2; // 正确,2被隐式转换为Rational(2,1)
//编译器眼中应该是这样:const Rational temp(2); result = oneHalf * temp;
result = 2 *
oneHalf; // 错误,2可不被认为是Rational对象;因此无法调用operator*。可见,这样并不准确,因为乘法(*)应该满足交换律,不是吗?
所以,支持混合式算术运算的可行之道应该是:让operator*成为一个non-member函数,允许编译器在每一个实参上执行隐式类型转换:
class Rational
{
... // contains no operator*
};
const Rational operator*(const Rational&
lhs, Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth *
2;
result = 2 *
oneFourth; //这下两个都工作的很好,通过隐式转换实现
成员函数的方面是非成员函数,而不是友元函数。
可以用类中的public接口实现的函数,最好就是非成员函数,而不是采用友元函数。
请记住:

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

条款25:考虑写出一个不抛异常的swap函数
模板的全特化和偏特化:
模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。
模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。
先看类模板:
[cpp] view
plain copy

template<typename T1, typename T2>

class Test

{

public:

Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}

private:

T1 a;

T2 b;

};

template<>

class Test<int , char>

{

public:

Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}

private:

int a;

char b;

};

template <typename T2>

class Test<char, T2>

{

public:

Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}

private:

char a;

T2 b;

};

那么下面3句依次调用类模板、全特化与偏特化:
[cpp] view
plain copy

Test<double , double> t1(0.1,0.2);

Test<int , char> t2(1,'A');

Test<char, bool> t3('A',true);

而对于函数模板,却只有全特化,不能偏特化:
[cpp] view
plain copy

//模板函数

template<typename T1, typename T2>

void fun(T1 a , T2 b)

{

cout<<"模板函数"<<endl;

}

//全特化

template<>

void fun<int ,char >(int a, char b)

{

cout<<"全特化"<<endl;

}

//函数不存在偏特化:下面的代码是错误的

/*

template<typename T2>

void fun<char,T2>(char a, T2 b)

{

cout<<"偏特化"<<endl;

}

*/

至于为什么函数不能偏特化,似乎不是因为语言实现不了,而是因为偏特化的功能可以通过函数的重载完成。
SWAP:

//自定义命名空间

namespacemy

{

//类的具体实现

template <typename T1>

class TestImpl

{

public:

TestImpl(T1 i):ival(i){}

virtual T1 getVal(){return ival;}

private:

T1 ival;

};

//指针

template <typename T1>

class Test

{

public:

Test(T1 i):p(new TestImpl(i)){}

~Test(){delete p;}

Test operator=(const Test rhs)

{

*p = *(rhs.p);

}

T1 getVal()

{

return this->p->getVal();

}

void swap(Test& other)

{

using std::swap;

swap(p,other.p);

}

private:

TestImpl *p;

};

//重载std::swap

template<typename T>

void swap(Test<T>& a, Test<T>& b)

{

a.swap(b);

}

}

简而言之:我们可以为类B声明一个名为swap的public成员函数执行swap类A数据指针的操作,然后令标准库std特化swap调用该swap函数
请记住:

当std::swap对你的类型效率不高时,提供一个swap成员函数(public),并确定这个函数不抛出异常。

如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap(假如的是类模板,那么,需要重载一个swap函数,然后将这个函数与模板类一起放在一个命名空间中。)。

调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。

为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

五.实现
大多数情况下,适当提出你的类定义以及函数声明,是花费最多心力的两件事。尽管如此,还是有很多东西需要小心:太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长建置时间。

条款26:尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以应该尽量避免这种情形。
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted1;
if (password.length() < MinimumPasswordLength)
{
throw logic_error("Password is too short"); //注意:可能抛出异常
}
string encrypted2;
...
return encrypted;
}
如上代码,encrypted在2处定义是个不错的选择,因为如果抛出异常,那么encrypted的构造和析构可是做了无用功啊!
还有一点要注意:“通过默认构造函数构造出一个对象然后对它赋值”比“直接在构造函数时制定初值”效率差。
“尽可能延后”的真正意义应该是:你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
//方法A:定义循环外
Widget w;
for (int i = 0; i < n; ++i)
{
w = some
value dependent on i;
...
} //1个构造函数+1个析构函数+n个赋值操作;
//方法B:定义循环内
for (int i = 0; i < n; ++i)
{
Widget w(some
value dependent on i);
...
} //n个构造函数+n个析构函数
除非:1.你知道赋值成本比“构造+析构”成本低;2.你正在处理代码中效率高度敏感的部分,否则应该使用方法B。
请记住:

尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。

条款27:尽量少做转型动作
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
C风格的转型动作看起来像这样:
(T)expression //将expression转型为T
函数风格的转型动作看起来像这样:
T(expression) //将expression转型为T
C++还提供四种新式转型:
const_cast:通常被用来将对象的常量性转除;即去掉const。
dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
static_cast:用来强迫隐式转换,例如将non-const转型为const,int转型为double等等。
尽量使用新式转型:

它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程。

各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

请记住:

如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。

如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。

宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。

条款28:避免返回handls指向对象内部成分
struct RectData
{
Point ulhc;
Point lrhc;
};
class Rectangle
{
public:
...
Point& upperLeft() const {
return pData->ulhc; }1//const只对函数内进行保护,函数返回后呢??
Point& lowerRight() const {
return pData->lrhc; }2 //const只对函数内进行保护,函数返回后呢??
private:
std::tr1::shared_ptr<RectData> pData;
...
};
1,2两函数都返回引用,指向private内部数据,调用者于是可通过这些引用更改内部数据!这严重破坏了数据的封装性,对私有成员进行直接操作?太不可思意了!
const Point& upperLeft()
const { return pData->ulhc; }3
const Point& lowerRight()
const { return pData->lrhc; }4
或者将1,2改为3,4,这就限制了客户的“涂改权”,只有“读取权”。
但终究“返回一个handle代表对象内部成分”总是危险的。特别是将返回的指针或引用赋值给其它指针或引用,那么久造成了“悬空”。
请记住:

避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。

条款29:为“异常安全”而努力是值得的
请记住:

异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。

函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30:透彻了解inlining的里里外外
如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。
记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式:在类内部定义的函数都默认为inline函数,甚至包括内部定义的友元函数。明确声明inline函数的做法则是在其定义式前加上关键字inline。
Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。
Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template煤油理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
一个表面上看似inline的函数是否真实inline,取决于你的建置环境,主要取决于编译器。
有的时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。
对程序开发而言,将上述所有考虑牢记在新很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。
这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。
请记住:

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。

不要只因为function templates出现在头文件,就将它们声明为inline。

条款31:将文件间的编译依存关系降至最低

Handle class

简而言之,将类的接口和实现分离。

接口里面存储一个指针,指向实现类,然后客户调用接口。

这样当实现改变时候,客户不用重编译。

Interface class

简而言之,将类抽象为接口,然后客户调用接口,这样当类的实现改变时不会受影响。

类的实现通过继承抽象基类完成。

客户调用的构造接口是static,因为它定义在抽象基类中,而抽象基类不能实例化,

只能在实例化派生类时部分实例化,所以定义成static。static的函数属于整个类,

可以通过类名::函数名的方式调用,具体构造函数在派生类中定义。

另一种制作Handle class的办法是,令Person称为抽象基类,称为:

Interface Class (Person2.h Person2.cpp RealPerson2.h)

Person类定义成抽象基类,成为接口。这个class的客户必须以Person的指针或引用来撰写应用程序。

Interface class的客户通常调用factory(工厂)函数或virtual构造函数。它们返回指针(或智能指针),指向

动态分配所得对象,而该对象支持Interface class的接口。这样的函数又往往在Interface class内被

声明为static。

支持Interface class接口的那个具象类(concrete classes)必须被定义出来,而且真正的构造函数必须被调用。

它提供继承而来的virtual函数的实现。

如果使用object references或object pointers可以完成任务,就不要使用object。因为,使用references或pointers只需要一个声明,而定义objects需要使用该类型的定义。

如果可以,尽量以class声明式替换class定义式但是,当声明函数使用某个class时,即使是by
value方式传递该类型参数/返回值,都不需要class定义。但是在使用这些函数时,这些classes在调用函数前一定要先曝光。客户终究是要知道classes的定义式,但是这样做的意义在于:将提供class定义式(通过#include完成)的义务,从函数声明所在头文件,转移到函数调用的客户文件。

为声明式和定义式提供两个不同的头文件。程序中,不应该让客户给出前置声明,程序作者一般提供两个头文件,一个用于声明式,一个用于定义式。在C++标准库的头文件中(条款54),<iosfwd>内含iostream各组件的声明式,其对应定义分布在不同文件件,包括<sstream>,<streambuf>,<fstream>,<iostream>。

总结:避免大量依赖性编译的解决方案就是:在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件
请记住:

支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。

程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

六.继承与面向对象设计
如果你了解C++各种特性的意义,你会发现,你对OOP的看法改变了。它不再是一项用来划分语言特性的仪典,而是可以让你通过它说出你对软件系统的想法。一旦你知道该通过它说些什么,移转至C++世界也就不再是可怕的高要求了。

条款32:确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是:public
inheritance(公有继承)意味is-a(是一种)的关系。
如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化得概念,而D比B表现出更特殊化的概念。你主张:“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)
好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
请记住:

“public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。

条款33:避免遮掩继承而来的名称
C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否是相同或不同的类型,并不重要。即,只要名称相同就覆盖基类相应的成员,不管是类型,参数个数,都无关紧要。派生类的作用域嵌套在基类的作用域内。
C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。
如果你真的需要用到基类的被名称遮掩的函数,可以使用using声明式,引入基类的成员函数。
请记住:

derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。

条款34:区分接口继承和实现继承
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。

成员函数的接口总是会被继承。

声明一个纯虚函数的目的是为了让派生类只继承函数接口。

声明一个虚函数的目的是让派生类继承该函数的接口和缺省实现,派生类也可以重新实现。

声明一个非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现。

请记住:

接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。

pure virtual函数只具体制定接口继承。

简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。

non-virtual函数具体制定接口继承以及强制性实现继承。

条款35:考虑virtual函数以外的其它选择
请记住:

virtual函数的替代方案包括NVI手法(通过public non-virtual成员函数调用private virtual函数)及Strategy设计模式(构造函数接受一个函数指针,指向一个函数)的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。

将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。

tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

条款36:绝不重新定义继承而来的non-virtual函数
如果在实现类D时,对基类B非虚函数mf重写。当D的对象在调用mf时,行为有可能象B,也有可能象D,决定因素跟对象本身没有关系,反而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。
请记住:

绝对不要重新定义继承而来的non-virtual函数。

条款37:绝不重新定义继承而来的缺省参数值
对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数系动态绑定(dynamically
bound),而缺省参数值确实静态绑定(statically bound)。意思是你可能会在“调用一个定义于派生类内的虚函数”的同时,却使用基类为它所指定的缺省参数值。
静态类型:申明时所采用的类型
动态类型:目前所指对象的类型
请记住:

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

条款38:通过符合塑模出has-a或“根据某物实现出”
请记住:

复合(composition)的意义和public继承完全不同。

在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。

条款39:明智而审慎地使用private继承

private继承意味着:根据某物实现。

private继承的特点:①.编译器不会将一个private继承而来的派生类对象转化为一个基类对象。这意味着,priavte继承不再是is-a关系②.基类的public和protected成员在派生类中全为private属性。

通常情况下,使用复合来实现,只有当protected成员或virtual函数牵扯进来时使用private继承

private继承可以造成空基类最优化:

class Empty{};

class HoldsAnInt1{
private:
int x_;
Empty empty_;
};

class HoldsAnInt2:private Empty{
private:
int x_;
};

sizeof(HoldsAnInt1)=8,而sizeof(HoldsAnInt2)=4
请记住:

Private继承意味着is implemented in terms of,它通常比复合的级别低(即优先使用复合),但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

与复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

条款40:明智而审慎地使用多重继承

多重继承往往会因为函数重名而导致二义性

在使用虚继承之后,构造函数的调用顺序也变了:不再是派生类调用自己的基类,依次向上层调用了,而是从最高的基类开始,向下调用。

总的来说,多重继承可能会引起很多问题:二义性、构造、析构函数的顺序,类的大小、访问速度等等。但有时的确也会使用到它:

有一个接口类A,我们需要实现它;恰巧我们有一个现成的类B,这个类B的功能跟我们要实现的类C相似,但是我们需要对它(B)的虚函数进行重写,以达到我们希望的功能,于是我们public继承了A类的接口,又private继承了B类的实现,并对其中的某些实现重定义了。这时候,多重继承真的就派上了用场。
请记住:

多重继承比单一继承更复杂。它可能导致新的歧义性,以及对virtual继承的需要。

virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。

多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class”和”private继承某个协助实现的class”的两两组合。

七.模板与泛型编程

条款41:了解隐式接口和编译期多态

显式接口由函数签名式构成,隐式接口由有效的表达式组成。

以不同的template参数具现化function template”会导致调用不同的参数,这便是编译期多态。简言之,运行时多态是决定“哪一个virtual函数应该被绑定”,而编译期多态决定“哪一个重载函数应该被调用”。
请记住:

class和template都支持接口与多态;

对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;

对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

条款42:了解typename的双重意义

在模板的的声明中,class与typename是没有什么区别的。

模板中依赖于模板参数的名称称为从属名称(dependent name), 当一个从属名称嵌套在一个类里面时,称为嵌套从属名称(nested dependent name)。

嵌套从属名称是需要用typename声明的,其他的名称是不可以用typename声明的。

如果去掉typename 就不行了,为什么呢?因为编译器总是会假设你遇到的不是一个类型就会把指针类型*当做乘号,然后就错了。

但是总有例外:在继承的基类列表中,以及构造函数的基类初始化列表中不能有typename。
请记住:

声明template参数时,前缀关键字class与typename可以互换

请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或者member initialization list内使用typename

条款43:学习处理模板化基类内的名称
请记住:

可在derived class template内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成(使用using声明/明确指出被调函数位于基类中)。

条款44:将与参数无关的代码抽离templates
请记住:

Template生成多个classes与多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或者class成员变量替换template参数。

因类型而造成的代码膨胀,也可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

条款45:运用成员函数模板接受所有兼容类型

在class内申明泛化copy构造函数(member template)并不会阻止编译器生成它们自己的copy构造函数(non-template),故你想要控制构造的方方面面,你必须同时声明泛化copy构造和普通copy构造。赋值操作也是一样。
请记住:

请使用member function templates(成员函数模版)生成“可接受所有兼容类型”的函数。

如果你声明member templates用于“泛化copy构造”或“泛化赋值操作符”,你还是需要声明正常的copy构造函数和赋值操作符。

条款46:需要类型转换时请为模板定义非成员函数

class templates并不依赖于template实参推导(template实参推导只施行于function templates身上,同时,template实参推导过程中从不将隐式类型转换函数纳入考虑)所以编译器总是能够在class Rational<T>具现化时得知T。

我们虽然使用friend,却与friend的传统用途“访问class的non-public成分”毫不相干。为了让类型转换可能发生在所有参数身上,我们需要一个non-member函数;为了令这个函数被自动具现化,我们需要将它声明在一个class内部;而在class内部声明non-member函数的唯一办法就是令他成为一个friend。

定义于class内的函数都暗自成为inline,包括friend函数。你可以将这样的inline声明所带来的冲击最小化,做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数。Rational是个template意味着这个辅助函数通常也是一个template。

template<typename T> class Rational;

template<typename T>

const Rational<T> doMultiply (const Rational<T>& lhs, const Rational<T>& rhs);

//声明helper template

template <typename T>

class Rational {

public:

...

friend const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)

{

return doMultiply(lhs, rhs);

}

};

template<typename T>

const Rational<T> doMultiply (const Rational<T>& lhs, const Rational<T>& rhs)

{

return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());

}

请记住:

当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。

条款47:请使用traits classes表现类型信息
请记住:

确认若干你希望将来可取得的类型信息.

为该信息选一个名称(例如iterator_category)

提供一个template和一组特化版本,内含你希望支持的类型相关信息.

条款48:认识template元编程
Template metaprogramming(TMPS,模板元编程)是编写template-based C++程序并执行编译期的过程。
TMP有两个伟大的效力:
第一,它让某些事情更容易.
第二,欲与TMP执行于C++编译期,因此可将工作从运行期转移到编译期.
请记住:

TMP可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率.

TMP可别用来生成'基于政策选择组合'的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码.

八.定制new和delete

条款49:了解new-handle的行为

new_handler函数:当operator new或operator new[]分配内存失败时调用的函数。

set_new_handler函数:返回值为new_handler类型的函数,试图分配更多内存以足够使用,成功时会返回,否则会抛出一个bad_alloc异常(或其派生类对象)或调用cstdlib的abort()或exit()函数强制退出。(set_new_handler函数将它获得的指针存储起来,然后返回调用前存储的指针相同)。

当operator new分配内存失败(不足)时,会先调用一个客户指定的错误处理函数new-handler,然后抛出一个异常(旧式的行为是返回一个空指针)。客户使用set_new_handler来设定该行为。

一个设计良好的new-handler函数必须做以下事情:

让更多内存可被使用。

安装另一个new-handler。

卸除new-handler。

抛出bad_alloc(或派生自bad_alloc)的异常。

不返回。

使用nothrow new只能保证operaor new不抛掷异常,不保证像'new (std:
:nothrow) Widget'这样的表达式不导致异常,其实你没有运用nothrow new的需要.
请记住:

set_new_handler允许客户指定一个函数,在内存分配无法获得满足时调用.

nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能

抛出异常.

条款50:了解new和delete的合理替换时机

何时可在“全局性的”或“class专属的”基础上合理替换缺省的new和delete:

(1)为了检测运用错误。

(2)为了收集动态分配内存的使用统计信息。

(3)为了增加分配和归还的速度。

(4)为了降低缺省内存管理器带来的额外空间开销。

(5)为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。

(6)为了将相关对象成簇集中。

(7)为了获得非传统行为。

static const int signature = 0xDEADBEEF;

typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc) {

using namespace std;

size_t realSize = size + 2 * sizeof(int);

void* pMem = malloc(realSize);

if(!pMem) throw bad_alloc();

//将signature写入内存的最前段落和最后段落

*(static_cast<int*>(pMem)) = signature;

*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;

//返回指针,指向恰位于第一个signature之后的内存位置

return static_cast<Byte*>(pMem) + sizeof(int);

}

请记住:

有许多理由需要写个自定义的new和delete,包括改善效能,对heap运用错误进行调试,收集heap使用信息。

条款51:编写new和delete时需固守常规

<1>.实现一致性operator new必须返回正确的值,内存不足时必须调用new_handling函数,必须有对付零内存的准备,还需要避免不慎掩盖正常形式的new。

<2>.operator new成员函数会被derived classes继承。如果针对class X而设计的operator new,只为大小为sizeof(X)的对象而设计。一旦被继承下去,base class的operator new被调用用以分配derived class对象:

class Base {

public:

static void* operator new(std::size_t size) throw(std::bad_alloc);

};

class Derived : public Base {

...

};

Derived* p = new Derived; //调用base::operator new

如果base class专属的operator new并非被设计用来对付上述情况,处理此情势最佳做法是:“内存申请量错误”的调用改采用标准operator new:

void* Base::operator new(std::size_t size) throw(std::bad_alloc)

{

if (size != sizeof(Base))

return ::operator new(size);

...

}

万一你的class专属operator new将大小有误的分配转交::operator new执行,你必须也将大小有误的删除行为转交::operator delete执行。
请记住:

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请.class专属版本则还应该处理'比正确大小更大的(错误)申请'。

operator delete应该在收到null指针时不做任何事情.class专属版本则还应该处理'比正确大小更大的(错误)申请'。

条款52:写了placement new也要写placement delete

缺省情况下c++在global作用域内提供以下形式的operator new:

void* operator new(std::size_t) throw(std::bad_alloc); //normal new

void* operator new(std::size_t, void*) throw(); //placement new

void* operator new(std::size_t, const std::nothrow_t&) throw();// nothrow new

要想class内自定义的new/delete不屏蔽掉原本的new/delete,请在基类里面申明所有正常形式的new和delete:

class StandardNewDeleteForms{

public:

//normal new/delete

static void* operator new(std::size_t size)throw(std::bad_alloc)

{return ::operator new(size);}

static void operator delete(void* pMemory) throw()

{::operator delete(pMemory);}

//placement new/delete

static void* operator new(std::size_t size, void* ptr) throw()

{return ::operator new(size, ptr);}

static void operator delete(void* pMemory, void* ptr)throw()

{return ::operator delete(pMemory, ptr);}

//nothrow new/delete

static void* operator new(std::size_t size, const std::nothrow_t& nt)throw()

{return ::operator new(size, nt);}

static void operator delete(void* pMemory, const std::nothrow_t&) throw()

{::operator delete(pMemory);}

};

凡是想自定义形式扩充标准形式的客户,可利用继承机制及using声明式取得标准形式:

class Widget: public StandardNewDeleteForms{

public:

using StandardNewDeleteForms::operator new;

using StandardNewDeleteForms::operator delete;

static void* operator new(std::size_t size, std::ostream& logStream)throw(std::bad_alloc);

static void operator delete(void* pMemory, std::ostream& logStream) throw();

};

请记住:

当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。

当你声明placement new和placement delete,请确定不要无意识地遮掩它们的正常版本。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: