Effective C++:改善程序与设计的55个具体做法
- 二、构造/析构/赋值运算
- 05 Know what functions C++ silently writes and calls.
- 06 Explicitly disallow the use of compiler-generated functions you do not want.
- 07 为多态基类声明虚析构函数 Declare destructors virtual in polymorphic base classes
- 08 Prevent exceptions from leaving destructors
- 09 Never call virtual functions during construction or destruction
- 10 令operator返回一个reference to* this
- 11 在operator=中处理“自我赋值”
- 12 Copy all parts of an object
- 三、资源管理
- 四、设计与声明
- 18 Make interfaces easy to use correctly and hard to use incrorrectly.
- 19 设计class犹如设计type
- 20 宁以pass-by-reference-to-const替换pass-by-value
- 21 Don't try to return a reference when you must return an object
- 22 Declare data members private
- 23 Prefer non-member non-friend functions to member functions
- 24 若所有参数皆需类型转换,请为此采用non-member函数
- 25 考虑写出一个不抛出异常的swap函数
- 五、Implementations 实现
- 六、继承与面向对象设计
- 七、模板与泛型编程
- 八、定制new和delete
- 九、杂项讨论
二、构造/析构/赋值运算
05 Know what functions C++ silently writes and calls.
- 编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
- 编译器产生的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
- 编译器创建的copy构造函数和copy assignment操作符,编译器创建的版本只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象。
- silent copy assignment拒绝编译内含reference成员的赋值操作,以及内含const成员的赋值操作。
- 如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符,毕竟编译器为derived classes所生的copy assignment操作符想象中可以处理base class成分,但他们当然无法调用derived class无权调用的成员函数。
06 Explicitly disallow the use of compiler-generated functions you do not want.
为驳回编译器自动提供的功能,可将相应的成员函数声明为private并且不予实现,更好的做法如上图。
07 为多态基类声明虚析构函数 Declare destructors virtual in polymorphic base classes
- virtual函数的目的是允许derived class的实现得以客制化。
- 如果想要一个抽象class,但是没有纯虚函数可用,可以将其析构函数声明为pure virtual.
- 纯虚函数可以定义,并在子类使用。
- 带多态性质的基类应该声明一个virtual析构函数,如果class带有任何virtual函数,它就应该拥有一个virtual析构函数;反之如果不是作为base clases使用,或者不是为了具备多态性,就不应该声明为virtual析构函数。
08 Prevent exceptions from leaving destructors
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。
09 Never call virtual functions during construction or destruction
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class.
10 令operator返回一个reference to* this
- 令赋值(assignment)操作符返回一个reference to * this.
11 在operator=中处理“自我赋值”
- 这里的自我赋值问题是,operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap,他也销毁rhs的bitmap。而后会导致持有一个指针指向一个被删除的对象。
- 这个版本具备“自我赋值安全性”,但扔不具备“异常安全性”。更明确的说如果“new Bitmap“导致异常(不论因为分配时内存不足或因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针执行一块被删除的Bitmap。
- 让operator=具备“异常安全性”往往自动获得“自我赋值安全”的回报。现在,如果new分配失败,则会抛出异常(bad_alloc)并跳过后面代码,pb(及其栖身的Widget)保持原状。
- copy and swap技术
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
- 确定任何函数如果操作一个以上的对象,其中多个对象时同一对象时,其行为仍然正确。
12 Copy all parts of an object
- 复制对象时请勿忘每一个成分
- copying函数(opy构造函数和copyassignment)应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应当将共同的机能放在第三个函数中,并由两个coping函数共同调用。
三、资源管理
13 Use objects to manage resources
- 为确保确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,我们便可倚赖c++的“析构函数自动调用机制”确保资源被释放。
class Investment{ ... };
Investment* createInvestment(); //工厂函数,返回动态分配对象
void f()
{
Inerstment* pInv = createInvestment();
...
delete pInv;
}
- 获得资源后立刻放进管理对象内,“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII)
- 管理对象运用析构函数确保资源被释放。
- 受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它,而STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr,而shared_ptr的复制行为“一如预期”,它可以被用于STL容器有一集其他“auto_ptr之非正统复制行为并不适用”的语境上。
- 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常被使用的RAII classes分别是tr1:shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它指向null。
14 在资源管理类中小心copying行为
- 复制RAII对象必须一并复制他所管理的资源,所以资源的copy行为决定RAII对象的copying行为。
- 普遍而常见的RAII class copying行为是:有抑制copying、施行引用计数法(reference counting)。
15 在资源管理中提供对原始资源的访问
- APIs往往要求访问原始资源,所以内一个RAII class应该提供一个“取得其所管理之资源”的办法。
- 对原始资源的访问可能经由显示转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
16 成对使用new 和delete时要采取相同形式
- 如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
17 以独立语句将newed对象置入智能指针
- 如果下行获得这样的操作序列,首先执行"new Widget",调用priority,最后tr1::shared_ptr构造函数,万一对priority的调用导致异常,在此情况下"new Widget"返回的指针将会遗失,因为它未被置入tr1::shared_ptr内,所以在此调用过程中可能引发资源泄露,因为在资源被创建和资源被转换为管理对象两个时间点之间有可能发生异常干扰。
- 以独立语句将newed对象存储于(置入)智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
四、设计与声明
18 Make interfaces easy to use correctly and hard to use incrorrectly.
- 好的接口很容易被正确使用,不容易被误用。
- 促进正确使用的办法爆剧哦接口的一致性,以及与内置类型的行为兼容。
- 阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- shared_ptr支持定制删除器,这可防范DLL问题,可被用来自动解锁互斥锁。
19 设计class犹如设计type
- 新type的对象应该如歌被创建和销毁?
这会影响到class的构造函数和析构函数以及内存分配函数和释放函数(operator new, operator new[], operator delete和operator delete[])的设计。 - 对象的初始化和对象的复制该有什么样的区别?
取决于构造函数和赋值操作符的行为,以及其间的差异。 - 新type的对象如果被passed by value(以值传递),意味着什么?
copy构造函数用来定义一个type的pass-by-value该如何实现。 - 什么是新type的“合法值”?
对class的成员变量而言,通常只有某些数值集是有效的,那些数值集决定了calss必须维护的约束条件,决定了成员函数(特别是构造函数、赋值操作符和所谓“setter”函数)必须进行错误检查工作,它也影响函数抛出的异常、以及函数异常明系列。 - 新type需要配合某个继承图系吗?
如果继承自某些class,就会收到那些class的设计束缚,特别是收到它们的函数是virtual或non-virtual的影响,如果允许其他classes继承该class,那会影响声明的函数尤其是析构函数是否为virtual。 - type需要什么样的转换?
- 什么样的操作符和函数对此新type而言是合理的?
为class声明哪些函数。 - 什么样的标准标准函数应该驳回?
哪些必须声明为private. - 谁该取用新type的成员?
哪个成员为 public,哪个为protected,哪个为 private。哪一个classes和functions应该是friends,以及将它们嵌套于另一个之内是否合理。 - 什么是新type的未声明接口?
或许其实并非定义一个新type,而是定义一整个types家族,如此应该定义一个新的class。 - 真的需要定义一个新type吗?
如果只是定义新的derived class以便为既有的calss添加技能,那么说不定单纯定义一个或多个non-member函数或templates更能够达到目标。
20 宁以pass-by-reference-to-const替换pass-by-value
- 尽量以pass-by-reference-to-const替换pass-by-value,这样可避免形参构造和析构带来的消耗。
- 编译器的底层,references往往以指针来实现,因此pass by reference通常真正传递的是指针。如下代码,在printNameAndDisplay函数内不论传递过来的对象原本是什么类型,参数w就像一个Window对象,因此在printNameAndDisplay内调用的总是Window::display,绝对不会是WindowWithScrollBars::display,所以使用pass-by-reference-to-const,可避免切割问题。
class Window{
public:
...
std::string name() const;
virtual void display() const;
};
class WindowWithScrollBars: public Window {
public:
...
virtual void display() const;
};
void printNameAndDisplay(Window w) //不正确吗,参数可能被切割
{
std::cout << w.name();
w.display();
}
void printNameAndDisplay(const Window& w) //很好,参数不会被切割
{
std::cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
- 并不适用于内置类型,以及STL的迭代器和函数对象,对他们而言,pass-by-value往往比较适当。
21 Don’t try to return a reference when you must return an object
- 绝不要返回pointer或reference指向一个local stack对象,如果这样,那么函数返回后该pointer或reference将指向已经成空、发臭、败坏的残骸。
- 绝不要返回执行reference执行一个heap-allocated对象,因为没有合理的办法对此返回值进行delete调用。
- 绝不要返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。在单线程环境中合理返回执行一个local static对象的做法就是将其放在函数中作为non-local static并返回reference。
22 Declare data members private
- 为什么不采用public成员变量?
如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每样东西都是函数,客户就不需要在打算访问class成员时迷惑的试着记住是否该使用小括号。
使用函数可以对成员变量的处理有更精确的控制,可以实现不准访问、只读访问、读写访问、惟写访问。 - 封装。
将成员变量隐藏在函数接口的背后,可以为所有可能的实现提供弹性,例如可使得成员变量被读或者被写时轻松通知其他对象,可以验证class的约束条件以及函数的前提和事后状态、可以在多线程环境中执行同步控制等等。
封装的重要性还远不止于此,如果对客户隐藏成员变量,可以确保class的约束条件总是会获得维护,因为只有成员函数可以影响它们,犹有进者,保留了日后变更实现的权利。如果不隐藏它们,会发现及时拥有class原始码,改变任何public事物的能力环视极端受到束缚,因为会破坏太多客户码。public意味着不封装,而几乎可以说不封装意味着不可改变,特别事多被广泛使用的classes而言。被广泛使用的classes是最需要封装的一个族群,因为它们最能从改采用一个较佳实现版本中获益。protected成员变量的论点十分类似,实际上它和public成员变量的论点相同,看起来不是那么一回事,因为语法一致性和细微划分之访问控制等理论显然也适用于protected数据。但封装呢?protected成员变量的封装性并非高过public成员变量。某些东西的封装性与当其内容改变时可能造成的代码破坏量成反比,因此成员变量的封装性与成员变量的内容改变时所破坏的代码数量成反比,取消一个public成员变量所有使用它的客户码都会被破坏,取消一个protected成员变量所有使用它的derived classes都会被破坏,这些都是个不可知的大量。 - 切记将成员变量生命为private。这可赋予客户端访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class坐着以充分的实现弹性。
- protected并不比public更具有封装性。
23 Prefer non-member non-friend functions to member functions
- 面向对象守则要求数据应该尽可能被封装。如果某些东西被封装,他就不再可见。愈多东西被封装,愈少人可以看到它。而愈少人看到它,我们就有愈大的弹性去变化它,因为我们的改变仅仅直接影响看到改变的那些人事物。因此,愈多东西被封装,我们改变那些东西的能力也就愈大。这就是我们首先推崇封装的原因:它使我们能够改变事物而只影响有限客户。现在考虑对象内的数据。愈少代码可以看到数据(也就是访问它),愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等等。
- 成员变量应该是private,因为如果它们不是,就有无限量的函数可以访问它们,它们也就毫无封装性。能够访问 private 成员变量的函数只有class 的member函数加上friend函数而已。如果要你在一个member函数(它不只可以访问class内的private数据,也可以取用private函数、enums、typedefs等等)和一个non-member,non-friend函数(它无法访问上述任何东西)之间做抉择,而且两者提供相同机能,那么,导致较大封装性的是non-member non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量。这就解释了为什么clearBrowser(一个non-member non-friend函数)比clearEverything(一个member函数)更受欢迎的原因:它导致class有较大的封装性。
- 习惯用namespace分割组件,而class必须要保证完整性。
- 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
24 若所有参数皆需类型转换,请为此采用non-member函数
- 如果需要为某个函数的所有参数(包括被this指针所指向的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
25 考虑写出一个不抛出异常的swap函数
- 类模板和函数模板都可以被全特化;
类模板能偏特化,不能被重载;
函数模板全特化,不能被偏特化。 - 调用优先级从高到低进行排序是:
全特化类>偏特化类>主版本模板类 - 当std::swap对类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果提供一个member swap,也应该提供一个non-member swap用来调用前者。对于classes,也请特化std::swap.
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
- 为“用户定义类型”进行std templates全特化是最好的,但千万不要尝试在std内加入某些对std而言全新的东西。
五、Implementations 实现
26 尽可能延后变量定义式的出现时间
- 尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序效率。
27 尽可能少做转型动作
- const_cast通常被用来讲对象的常量性转除,也是唯一有此能力的C+±style转型操作符。(去除const转换)
- dynamic_cast主要用来执行“安全的向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。他是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。(良性转换)
- reinterpret_cast意图执行低级转型,实际动作及结果可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类转型在低级代码以外很少见。(重新解释转换,对dynamic_cast补充,风险高)
- static_cast用来强迫隐式转换,例如将non-const对象转化为const对象,或者将int转为double,等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为type指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const,这个只有const才办得到。(类继承层次间转换,保证安全)
- 对象的布局方式和他们的地址计算方式随编译器的不同而不同,那意味着“由于知道对象如何布局”而设计的转型,在某一平台行得通,在其他平台不一定行得通。
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。
- 宁可使用C+±style新式转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
28 避免返回handles执行对象内部成分
- 成员变量的封装性最多只等于“返回其reference”的函数的访问级别。
- 如果const成员函数传出去一个reference,后者所指数据与对象自身关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
- 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
29 为“异常安全”而努力是值得的
- “异常安全”有两个条件,不泄露任何资源,不允许数据败坏。
- 异常安全函数提供一下是哪个保证之一:
基本承诺:如果一场被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知,如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。 - 异常安全函数及时发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
30 透彻了解inlining的里里外外
- 如果写的template没有理由要求它具现的每一个函数都是inlined,就应该避免 将这个template声明为inline。
- inline是个申请,编译器可加以忽略。大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。这不该令你惊讶,因为virtual意味着“等待,直到运行期才确定调用哪个函数”,而inline意味“执行前,先将调用动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,编译器将拒绝将函数本体inlining。
- 一个表面上看似inline的函数是否真的inline,取决于建置环境。主要取决于编译器。大多数编译器提供一个诊断级别:如果无法将函数inline化,会给出一个警告信息。
- 有时候编译器虽然有意愿inlining某个函数,还是可能为该函数生成一个函数本体。举个例子,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。毕竟编译器没有能力提出一个指针指向并不存在的函数。
- 编译器通常不对“通过函数指针而进行的调用”实施inlining,这意味着inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。即使从未使用函数指针,“未被成功inlined”的inlined函数还是有可能,因为程序员并非唯一要求函数指针的人,有时候编译器会生成构造函数和析构函数的outline副本,如此一来它们就可以获取指针指向那些函数,在array内部元素的构造和析构过程中使用。
- inline函数修改将会影响程序用到的地方必须全部重新编译,而如果是non-line函数,甚至如果采用动态连接,升级版函数可以不知不觉被应用程序吸纳。
- 调试器面对inline函数束手无策,毕竟不能在一个并不存在的函数内设立断点。,在许多建置环境中仅仅只能“在调试版程序中禁止发生inlining”。
- 平均而言一个程序往往将80%的执行时间花费在20%的代码上头,这是一个重要的法则,因为它提醒你,作为一个软件开发者,目标是要找出这可以有效增进程序整体效率的20%代码,然后将它inline或竭尽所能地将它瘦身。
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
10.不要因为function templates出现在头文件,就将它们生命为inline。
31 将文件间的编译依存关系降至最低
- 使用类的前置声明,将接口从实现中分离,以声明的依存性替换定义的依存性。
- 编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。
- 如果使用object references或object pointers可以完成任务,就不要使用objects。可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某类型的objects,就需要用该类型的定义式。
- 如果能够,尽量以class声明式替换class定义式。当声明一个函数而它用到某个class时,并不需要该class定义,几遍函数以by value方式传递该类型的参数(或返回值)亦然。
class Date; //class 声明式
Date today(); //by-val返回
void clearAppointments(Date d); //需要传入Date形参
- 为声明式和定义式提供不同的头文件。
- 支持“编译依存最小化”的一般构想是相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classes和interface classes。
- 程序胡库头文件应该以:完全且仅有声明式的形式存在。这种做法不论是否涉及templates都适用。
六、继承与面向对象设计
32 确定你的public继承塑膜出is-a关系
- “public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上。因为每一个derived class对象也都是一个base class对象。
33 避免遮掩继承而来的名称
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
- 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。
34 区分接口继承和实现继承
- 声明简朴的(非纯)impure virtual函数的目的是让dericed classes继承该函数的接口和缺省实现。
- 声明non-virtual函数的目的是为了令dericed classes继承函数的接口及一份强制性实现。
- pure virtual函数、simple(impure)virtual函数、non-virtual函数之间的差异,使得可以精确指定想要dericed classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。
- 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class接口。
- pure virtual函数只具体指定接口继承。
- 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
- non-virtual函数具体指定接口继承以及强制性实现继承。
35 考虑virtual函数意外的其他选择
- NVI手法的一个优点在于隐身代码注释“做一些事前工作”和“做一些事后工作”之中。那些注释保证在“virtual函数进行真正工作之前和之后”被调用。这意味着外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等。“事后工作”可以保证互斥器接触锁定、验证函数的事后条件、再次验证class约束条件等等。如果让客户直接调用virtual函数,就没有任何办法可以做这些事。
#include <iostream>
using namespace std;
class Base{
public:
void healthValue() {
dohealthValue();
}
private:
virtual void dohealthValue()const{
cout << "Base~~~healthValue\n";
}
};
class Derive: public Base{
private:
virtual void dohealthValue()const{
cout << "Derive~~~healthValue\n";
}
};
int main()
{
Base *b = new Derive();
b->healthValue();
return 0;
}
- 使用non-virtual interface(NVI)手法,那是Template Methed设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。
- 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
- 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式(巧用tr1::bind)。这也是Strategy设计模式的某种表现形式。
- 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
- tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
36 绝不重新定义继承而来的non-virtual函数
- 绝对不要重新定义继承而来的non-virtual函数。
37 绝不重新定义继承而来的缺省参数值
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数是唯一应该覆写的东西,确是动态绑定。
38 通过复合塑膜出has-a或"根据某物实现出"
- 复合的意义和public继承完全不同。
- 在应用域,意味着has-a。在实现域(implementation domain),意味着is-implemented-in-terms-of(根据某物实现出)。
39 明智而审慎地使用private继承
- 如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。
- 如果classes之间的继承关系是private,有private base class继承而来的所有成员,在derived class中都会变成prvate属性,纵使他们在base class中原本是protected或public属性。
- Private继承意味着is-implatemented-in-terms of(根据某物实现出)。如果让class D以private形式继承class B,用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术(这就是为什么继承自一个private base class的每样东西在你的class内都是private:因为它们都只是实现的枝节而已)。
- private继承意味着只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其它意涵了。
- Private继承意味着is-implatemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
- 和复合(composition)不同,private继承可以造成empty base最优化。这时致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
- EBO(empty base optimization;空白基类最优化)
40 明智而审慎地使用多重继承
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
- virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。
七、模板与泛型编程
41 了解隐式接口和编译期多态
1.面向对象编程世界总是以显式接口和运行期多态解决问题。
class Widget{
public:
widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
...
};
void doProessing(Widget& w)
{
if(w.size() > 10 && w!= someNastyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
2.由于w的类型被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找到这个接口(例如在Widget的.h文件中),看看它是什么样子,所以我们称此接口为一个显式接口,也就是它在源码中明确可见。
3. 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism),也就是说将于运行期根据w的动态类型决定究竟是哪一个函数。
4. Templates及泛型编程的世界,与面向对象有根本的不同。在此世界中接口和运行期多态仍然存在,但重要性降低,反倒是隐式接口(implicit interfaces)和编译器多态(compile-time polymorphism)移到前头了。若想知道那是什么,看看当我们将doProcessing从函数转变成函数模板时发生了什么事:
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w!= someNastyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
w必须支持哪一种接口,系由template中执行与w身上的操作来决定。本例看来w的类型T好型必须支持size,normalize和swap成员函数、copy成员函数、copy构造函数(用来建立temp)、不等比较(inequality comparison,用来比较someNasty-Widget)。我们很快会喊道这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口(implicit interface)。
凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功。这样的具现行为发生在编译期。“以不同的tempalte参数具现化function template”会导致调用不同的函数,这便是所谓的编译期多态(complie-time polymorphism)。
5. "运行期多态"和“编译期多态”之间的差异,因为它类似于“哪一个重载函数该被调用”和”哪一个virtual函数该被绑定“(发生在运行期)之间的差异。
6. 显式接口和隐式接口的差异比较新颖,通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
};
其public接口由一个构造函数、一个析构函数、函数size,normalize,swap及其参数类型、返回类型、常量性构成。也包括编译器产生的copy构造函数和copy assignment操作符。另外也可以包括typedefs,以及public成员变量。
隐式接口完全不同,它并不基于函数签名式,二十由有效表达式组成。再次看看doProcessing template一开始的条件:
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w!= someNastyWidget) {
...
}
}
T(w的类型)的隐式接口看来好像有这些约束:
- 它必须提供一个名为size的成员函数,该函数返回一个整数值。
- 他必须支持一个operator!=函数,用来比较两个T对象。这里我们假设someNastyWidget的类型为T。
- 隐式接口仅仅是由一组有效表达式构成,表达式自身可能看起来复杂,但他们的约束条件一般而言相当直接又明确。
- 加诸于template参数身上的隐式接口,就像加诸于class对象身上的显式接口一样真实,而且两者都在编译器完成检查,就像你无法以一种”与class提供之显式接口矛盾“的方式来使用对象(代码将通不过编译),你也无法在template中使用”不支持template所要求之隐式接口“的对象(代码一样通不过编译)。
Remember: - classes和template都支持接口(interface)和多态(polymorphism)。
- 对classes而言接口是显式的(explicit),以函数签名为中心,多态则是通过virtual函数发生于运行期。
- 对template参数而言,接口是隐式(implicit),奠基于有效表达式,多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
42 了解typename的双重定义
- 任何时候想要在template中指涉一个嵌套丛书类型名称,就必须在紧临它之前放置关键字typename。
- tyoename只被用来验明嵌套从属类型名称,其他名称不该有它的存在。
tempalte<typename C> //允许使用"typename"或"class"
void f(const C& container, //不允许使用"typename"
typename C::interator iter);//一定要使用"typename"
- “typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初始列)中作为base class修饰符。
template<typename T>
class Derived: public Base<T>::Nested { //base class list中,不允许使用"typename"
public:
explicit Derived(int x) : Base<T>::Nested(x) //mem,init.list中,不允许使用"typename"
{
typename Base<T>::Nested temp; //嵌套从属类型名称,不在上述两例外情况中,需要加上"typename"
...
}
...
};
- 声明tempalte参数时,前缀关键字class和typename可互换。
- 请使用关键字typename标识嵌套从属类型名称,但不得在base class lists(基类列)或member initialization(成员初始列)内以它作为base class修饰符。
43 学习处理模板化基类内的名称
- 继承一个模板基类时,编译器并不知道它继承什么样的class,其中的template无法确切知道它是什么。虽然我们的眼睛可以看到基类函数确实在base class内,编译器确看不到它们。假设我们特化了基类,删除掉上述想在derived class中调用的base class member function,这时便可以解释上述编译器看不到它们的行为。
- 继承自template base class 的derived class,它知道base class templates有可能被特化,而那个特化版本可能不提供一般性template相同的接口,因此它往往拒绝在templatized base classes(模板化基类)内寻找继承而来的名称。进入template C++后,继承就不像以前那般畅行无阻了。
- 有是哪个办法令C++“不进入templateized base clssses观察”的行为失效。第一:derived class在调用base class函数动作之前加上"this->";第二:使用using声明式;第三:明确指出被调用函数位于base class内(指明作用域)。但这往往是最不让人满意的一个解法,因为如果被调用的是virtual函数,上述的确明确资格修饰会关闭“virtual绑定行为”。
- 从名称可视化的角度出发,上述每一个解法做的事情都相同:对编译器承诺“base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。这样一个承诺是编译器在解析derived class template时需要的。
44 将与参数无关的代码抽离templates
- Templates生成多个classes和多个函数,所以任何template代码都不应该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实时码。
45 运用成员函数模板接受所有兼容类型
- 使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
- 如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
46需要类型转换时请为模板定义非成员函数
1.当我们编写一个class template,而它所提供的之与此template相关的“”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template的friend函数”。
47 请使用traits classes表现类型信息
- 使用template想要在具现化中区分不同场景内部实现,可以在不同类型形参类中typedef 同一个名字,然后重载模板函数可以实现在编译期核定成功(使用if语句需要等到运行期核定)。
- Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现。
- 整合重载技术后,traits classes有可能在编译期对类型执行if…else测试。
48 认识template元编程
- Tempalte metaprogramming(TMP,模板元编程)是编写template-based C++程序并执行编译期的过程。所谓tempalte metaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出也就是templates具现出来的若干C++源码,便会一如往常地被编译。
- Template metaprogramming(TMP。模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
- TMP可被用来生成“基于政策选择组合”(base on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
八、定制new和delete
49 了解new-handle的行为
- 当operator new无法某一内存分配需求时,它会抛出异常。在抛出异常以反映一个未获满足的内存需求前,它会先调用一个客户指定的错误函数,一个所谓的new-handler。为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler,这是声明于的一个标准程序库函数。
- 设计一个良好的new-handler函数必须做到以下事情:
①让更多内存可被使用。
②安装另一个new-handler。
③卸除new-handler。
④抛出bad_alloc。
⑤不返回,通常调用abort或exit。 - static成员必须在class定义式之外被定义。
- 定制Widget的operator new做以下事情:
①调用set_new_handler,告知Widget的错误处理函数,这会将WIdget的new-handler安装位global new-handler。
②调用global operator new,执行实际之内存分配。如果分配失败,global operator new会调用Widget的new-handler,因为那个函数才刚被安装为global new-handler。如果global operator new最终无法分配内存,会抛出一个bad_alloc异常。在此情况下Widget的operator new必须恢复原本的global new-handler,然后再传播该异常。为确保原本的new-handler总是能够被重新安装回去,Widgetjiangglobal new-handler视为资源并遵守资源获取即初始化,运用资源管理对象防止资源泄露。
③如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new-handler,它会自动将Widget’s operator new被调用前的那个global new-handler恢复回来。
/*******************NewHandlerHolder*****************/
class NewHandlerHolder{
public:
explicit NewHandlerHolder(std::new_handler nh): handler(nh){}
~NewHandlerHolder(){std::set_new_handler(handler);}
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
/*******************Widget*****************/
class Widget{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
std::new_handler Widget::currentHandler = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
/*******************override operator new*****************/
void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
/*******************using*****************/
void outOfMem()
{
cerr << "Unable to satisfy request for memory\n";
abort();
}
int main()
{
Widget::set_new_handler(outOfMem);
Widget* pw1 = new Widget;
std::string *ps = new std::string;
Widget::set_new_handler(0);
Widget* pw2 = new Widget;
}
- Widget继承NewHandlerSupport<Widget’>,而且NewHandlerSupport template从未使用其类型参数T。实际上T的确不需要被使用,我们只是希望继承自NewHandlerSupport的每一个class,拥有实体互异的NewHandlerSupport复件(更明确地说是其成员变量)。类型参数T只是用来区分不同的derived class。Template机制会自动为每一个T(NewHandlerSupport赖以具现化的根据)生成一份currenthandler。
/*******************NewHandlerHolder*****************/
class NewHandlerHolder{
public:
explicit NewHandlerHolder(std::new_handler nh): handler(nh){}
~NewHandlerHolder(){std::set_new_handler(handler);}
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
/*******************template NewHandlerSupport*****************/
template<typename T>
class NewHandlerSupport{
public:
static std::new_handler set_new_handler(std::new_handler P) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
/*******************class Widget*****************/
class Widget: public NewHandlerSupport<Widget> {
};
/*******************using*****************/
void outOfMem()
{
cerr << "Unable to satisfy request for memory\n";
abort();
}
int main()
{
Widget::set_new_handler(outOfMem);
Widget* pw1 = new Widget;
std::string *ps = new std::string;
Widget::set_new_handler(0);
Widget* pw2 = new Widget;
}
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
- nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。
50 了解new和delete的合理替换时机
替换编译器提供的operator new或operator delete三个最常见的理由。
①用来检测运用上的错误。
②为了强化效能。
③为了收集使用上的统计数据。
了解何时可在“全局性的”或“class专属的”基础上合理替换缺省的new和delete。
①为了检测运用错误(如前所述)。
②为了收集动态分配内存只使用统计信息(如前所述)。
③为了增加分配和归还速度。
④为了降低缺省内存管理器带来的空间额外开销。
⑤为了弥补缺省分配器中的非最佳齐位。
⑥为了将相关对象成簇集中。
⑦为了获得非传统的行为。
51 编写new和delete时需固守常规
- operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就调用new-handler。它也应该有能力处理0byte申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
- operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
52 写了palcement new 也要写placement delete
- 当写一个palcement operator new,请确定也写出对应的operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄露。
- 当声明placement new和placement delete,请确定不要无意识(非故意)地遮掩它们的正常版本。
九、杂项讨论
53 不要轻忽编译器的警告
- 严肃对待编译器发出的警告信息,努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
- 不啊哟过多依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同,一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。
54 让自己熟悉包括TR1在内的标准程序库
- C++标准库的主要机能由STL、iostreams、locales组成。并包含C99标准程序库。
- TR1添加了智能指针(例如tr1::shared_ptr)、一般化函数指针(tr1::function)、hash-based容器、正则表达式(regular expressions)以及另外10个组件的支持。
- TR1自身只是一份规范。为了获得TR1提供的好处,需要提供一份实物。一个好的实物来源是Boost。
55 让自己熟悉boost
- Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。
- Boost提供许多TR1组件实现品,以及其他许多程序库。