Effective C++ 总结
2021年整理, 2023年更新一下
条款01: 将C++视为一个语言联邦
C++已经是个多重范型编程语言 (multiparadigm programming language)
同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言。
条款02:尽量以 const,enum,inline替换#define
也可以说是 宁可以编译器替换预处理器
#define
不被视为语言的一部分 当编译出错后你可能很难找到这个错误,从而浪费大量时间
第二、 值得注意的是class专属常量。为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员(member);而为确保此常量至多只有一份实体,你必须让它成为一个static成员:
class Gameplayer {
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns];
...
};
第三、 万一你的编译器(错误地)不允许“static整数型class常量”完成“in class初值设定”,可改用所谓的"the enum hack"补偿做法。其理论基础是:“一个属于枚举类型(enumerated type)的数值可权充ints被使用”
enum hack的行为某方面说比较像#define而不像const
class Gameplayer {
private:
enum {NumTurns = 5};
int scores[NumTurns];
...
};
本文要点
-
对于单纯常量,最好以const对象或enums替换#defines.
-
对于形似函数的宏(macros) ,最好改用inline函数替换#defines.
条款03: 尽可能使用const
如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
作用
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
- 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量。
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char* const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char* const p4 = greeting; // 自身是常量的指针,指向字符数组常量
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常量
void function4(const int& Var); // 引用参数在函数内为常量
// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
const成员函数承诺绝不改变其对象的逻辑状态 (logical state) , non-const成员函数却没有这般承诺。如果在const函数内调用non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。
这就是为什么"const成员函数调用non-const成员函数”是一种错误行为:因为对象有可能因此被改动。
non-const成员函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数并不会带来风险。这就是为什么本例以static_cast作用于*this的原因: 这里并不存在const相关危险。
观点
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性” (conceptual constness)
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04: 确定对象被使用前已先被初始化
第一、 读取未初始化的值会导致不明确的行为。
第二、使用构造函数初始化列表进行初始化为佳
#2
这个构造函数和#1
的最终结果相同,但通常效率较高。基于赋值的那个版 本(本例第一版本)首先调用default构造函数为theName, theAddress和thePhones设初值,然后立刻再对它们赋予新值。 default构造函数的一切作为因此浪费了。
class ABEntry {
public:
ABEntry();
private:
int a;
int b;
int c;
};
# 1
ABEntry::ABEntry()
{
a = 0;
b = 0;
c = 0;
}
#2
ABEntry::ABEntry() : a(0), b(0), c(0)
{}
有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同) ,也一定得使用初值列。是的,如果成员变量是const或references,它们就一定需要初值,不能被赋值。
第三、 防止初始化次序问题 合理运用单例模式
#1
class Filesystem {
public:
...
static Filesystem* fts();
};
Filesystem* Filesystem::GetInstance()
{
static connection_pool connPool;
return &connPool;
}
#2
class Filesystem {
private:
static Filesystem* fts;
Filesystem(){};
public:
static Filesystem* getInstance()
{
if (fts == nullptr)
{
fts = new Filesystem();
}
return fts;
}
};
本文要点
- 为内置型对象进行手工初始化,因为C++不保证初始化它们。
- 构造函数最好使用成员初值列(member initialization list) ,而不要在构造函数
- 本体内使用赋值操作(assignment) 。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
条款05: 了解C++默默编写并调用哪些函数
编译器可以暗自为class创建默认构造函数、拷贝构造函数、拷贝赋值操作符、以及析构函数。记住,这些函数不是类一创建出来就有的,惟有当这些函数被需要(调用),它们才会被编译器创建出来。
当然,如果你在创建类时,自己声明了那些函数,编译器就不会再创建了。
条款06: 若不想使用编译器自动生成的函数,就该明确拒绝
class HomeForSale {
public:
...
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
...
};
本文要点
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且 不予实现。使用像Uncopyable这样的base class也是一种做法。
条款07: 为多态基类声明virtual析构函数
如果base class没有virtual
会直接调用base class析构函数, derived class成员未释放。造成资源泄漏、内存浪费。
无端地将所有classes的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。
欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual函数该被调用。这份信息通常是由一个所谓vptr (virtual table pointer)
指针指 出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);
每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl-编译器在其中寻找适当的函数指针。
第二、 关于虚函数、纯虚函数
- 虚函数,在类成员方法的声明(不是定义)语句前加“virtual”, 如 virtual void func()
- 纯虚函数,在虚函数后加“=0”,如 virtual void func()=0
- 对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为称之为复写Override。
- 对于纯虚函数,子类必须提供纯虚函数的个性化实现。
- 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
- 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
- 构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。
如何选用
- 当基类中的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供缺省备选方案的时候,该方法应该设计为虚函数。
- 当基类中的某个成员方法,必须由子类提供个性化实现的时候,应该设计为纯虚函数。
本文要点
- polymorphic (带多态性质的) base classes应该声明一个virtual析构函数。如果 class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性 (polymorphically) ,就不该声明virtual析构函数。
条款08: 别让异常逃离析构函数
当vector v被销毁,它有责任销毁其内含的所有widgets。假设v内含十个Widgets,而在析构第一个元素期间,有个异常被抛出。其他九个widgets还是应该被销毁(否则它们保存的任何资源都会发生泄漏) ,因此v应该调用它们各个析构函数。但假设在那些调用期间,第二个widget析构函数又抛出异常。
class Widget {
public:
~widget() {
... //假设可能抛出异常
};
};
void dosomething()
{
std:;vector<widget> v;
...
}
但如果你的析构函数必须执行一个动作,如果该调用导致异常, DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。
class DBConnection {
public:
static DBconnection create();
void close();
private:
DBConnection db;
};
有两个方法 但是作用不大
1. 调用abort
如果程序遭遇一个“于析构期间发生的错误”后无法继续执行, “强迫结束程序”是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用abort可以抢先制“不明确行为”于死地。
2. 吞下因调用close而发生的异常
DBconn::~DBconn()
{
try{
db.close();
}
catch(...)
{
std::abort();
}
}
一个较佳策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
class DBConn{
public:
...
void close() //供客户使用的新函数
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed){
try{ //关闭连接(如果客户不那么做的话)
db.close();
}
catch(...){ //如果关闭动作失败,记录下并结束程序或吞下异常
//制作运转记录,记下对close的调用失败;
...
}
}
}
private:
DBConnection db;
bool closed;
};
本文要点
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09: 绝不在构造和析构过程中调用virtual函数
BuyTransaction b;
这时候被调用的logTransaction是 Transaction内的版本,不是Buyrransaction内的版本-即使目前即将建立的对象类型是BuyTransaction。base class构造期间virtual函数绝不会下降到derived classes阶层。
非正式的说法或许比较传神: 在base class构造期间, virtual函数不是virtual函数。
class Transaction {
public:
Transaction( );
virtual void logrransaction() const=0;
...
};
Transaction:: Transaction ()
{
...
logTransaction();
}
class BuyTransaction: public Transaction {
public:
virtual void logrransaction() const;
...
};
class SellTransaction: public Transaction {
public:
virtual void logTransaction() const;
...
};
其他方案可以解决这个问题。一种做法是在class Transaction内将 1ogTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信 息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual logTransaction。
class BuyTransaction: public Transaction {
public:
Buyrransaction ( parameters) : Transaction (createLogstring ( parameters )) {...}
private:
static std::string createLogstring( parameters );
};
本文要点
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
条款10: 令operator=返回一个reference to *this
Widget& operator=(const Widget& rhs) //返回类型是个reference,指向当前对象
{
...
return *this; //返回左侧对象
}
# 实现连锁赋值
int x, y, z;
x = y = z = 15; //赋值连锁形式
本文要点
- 令赋值(assignment)操作符返回一个
reference to *this
条款11: 在operator=中处理“自我赋值”
class Bitmap {...};
class Widget {
...
private:
Bitmap* pb;
};
# 直接赋值不具备自我赋值安全、异常安全性
# 传统不安全做法
# 不具备异常安全性
Widget& Widget::operator= (const Widqet& rhs) {
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
# 保证异常安全性做法
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOring = pb;
pb = new Bitmap(*rhs.pb);
delete pOring;
return *this;
}
# copy-and-swap v1
Widget& Widget::operator=(const Widget& rhs) {
Widget temp (rhs);
swap (temp);
return *this;
}
# copy-and-swap v2
Widget& Widget::operator=(Widget rhs) {
Widget temp (rhs);
swap (temp);
return *this;
}
本文要点
- 确保当对象自我赋值时operator-有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap.
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12: 复制对象时勿忘其每一个成分
如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为。编译器仿佛被冒犯似的,会以一种奇怪的方式回敬: 当你的实现代码几乎必然出错时却不告诉你。
当类含有其他类的成员、类发生继承
class Date { ... };
class Customer {
public:
...
private:
std::string name;
Date lastTransaction;
};
class PriorityCustomer: public Customer {
public:
PrioriuyCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
int priority;
};
Prioritycustomer::PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
logCall("PriorityCustomer copy assignment operator");
priority=rhs.priority;
return *this;
}
它们复制了Prioritycustomer声明的成员变量, 但每个PriorityCustomer还内含它所继承的Customer成员变量复件(副本) ,而那些成员变量却未被复制。
应该让derived class的copying函数调用相应的base class函数
PriorityCustomer::PriorityCustomer(const Prioritycustomer& rhs) : customer(rhs), priority(rhs.priority)
{
logCall ("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const Prioritycustomer& rhs) {
logCall ("PriorityCustomer copy assignment operator");
Customer: : operator= (rhs);
priority=rhs.priority;
return *this;
}
copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。
反方向–令copy构造函数调用copy assignment操作符-同样无意义。构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上。
本文要点
-
本条款题目所说的“复制每一个成分”现在应该很清楚了。当你编写一个copying函数,请确保:
- 复制所有local成员变量
- 调用所有base classes内的适当的 copying函数。
-
Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
-
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。
条款13: 以对象管理资源
所谓资源就是,一旦用了它,将来必须还给系统。C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄漏) ,但内存只是你必须管理的众多资源之一。
程序中过早的return、continus、goto等 略过delete, 泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。
使用智能指针
auto_ptr
在C++11已被弃用
代替方案为 “引用计数型智慧指针"(reference-counting smart pointer;RCSP)
void f() {
std::tr1::shared_ptr<Investment> pInv(createInvestment());
...
}
- 获得资源后立刻放进管理对象( managing object)内。
- 管理对象(managing object)运用析构函数确保资源被释放。
本文要点
- 为防止资源泄漏,请使用RAI1对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常被使用的RAlI classes分别是tr1::shared-ptr和autoptr,前者通常是较佳选择,因为其copy行为比较直观。若选择auto-ptr,复制动作会使它(被复制物)指向null
条款14: 在资源管理类中小心coping行为
Lock ml1 (&m); //锁定m
Lock ml2 (ml1); //将ml1复制到m12身上。这会发生什么事?
当一个RAI1对象被复制 该怎么办:
- 禁止复制
- 对底层资源采用“引用计数法”(智能指针)
- 复制底部资源。(深度拷贝)
- 转移底部资源的拥有权。
本文要点
- 复制RAl1对象必须一并复制它所管理的资源,所以资源的copying行为决定 RAII对象的copying行为。
- 普遍而常见的RAll class copying行为是:抑制copying、施行引用计数法 (reference counting) 。不过其他行为也都可能被实现。
条款15: 在资源管理类中提供对原始资源的访问
最佳设计很可能是坚持条款18的忠告: “让接口容易被正确使用,不易被误用”。
本文要点
- APIs往往要求访问原始资源(raw resources) ,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16: 成对使用new和delete时要采取相同形式
本文要点
- 如果你在new表达式中使用1],必须在相应的delete表达式中也使用1]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]
条款17: 以独立语句将newed对象置入智能指针
void processwidget(std::tr1::shared ptr<widget> pw, priority());
虽然我们在此使用“对象管理式资源” (object-managingresources) ,上述调用却可能泄漏资源。
在调用processWidget之前,编译器必须创建代码,做以下三件事:
- 调用priority
- 执行"new widget"
- 调用trl::sharedptr构造函数
c++并不是特定次序完成函数参数的核算、弹性大。如果priority
出现异常 new widget
返回的指针将会遗失
本文要点
- 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
条款18: 让接口容易被正确使用,不易被误用
std::tr1::shared ptr<Investment> pInv( static cast<Investment*>(0), getRidofInvestment);
本文要点
- 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- tr1::sharedptr支持定制型删除器(custom deleter) 。这可防范DLL问题,可被用来自动解除互斥锁(mutexes,见条款14)等等。
条款19:设计class犹如设计type
在设计class时,要考虑一系列的问题, 包括:
- 新type的对象应该如何被创建和销毁(构造和析构)
- 对象的初始化和对象的赋值该有什么样的差别,这个答案决定你的构造函数和赋值操作符的行为,以及其间的差异。
- 新的type的对象如果被pass by value,意味着什么?记住,拷贝构造函数用来定义一个type的pass-by-value该如何实现。
- 什么是新type的“合法值”?(约束条件)成员函数必须进行错误检查工作。
- 你的新type需要配合某个继承图系吗?(注意虚函数)
- 你的新type需要什么样的转换?(显示转换、类型转换操作符)
- 什么样的操作符和函数对此新type而言是合理的?这决定你为你的class声明哪些函数,哪些是成员函数,哪些不是。
- 什么样的标准函数应该驳回?那些正是必须声明为private者(条款6)。
- 什么是新type的“未声明接口”
- 你的新type有多么一般化?(class template)
- 成员函数和成员变量的可见范围(public/protected/private)
- 是否真的需要一个新type
本文要点
- Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。
条款20:宁以pass-by-reference-to-const替换pass-by-value
bool validatestudent(const student& s);
这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。修订后的这个参数声明中的const是重要的。以by reference方式传递参数也可以避免对象切割问题
本文要点
- 尽量以pass-by-reference-to-const替换pass-by-value,前者通常比较高效,并可避免
- 切割问题(slicing problem) 。 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言, pass-by-value往往比较适当。
条款21: 必须返回对象时,别妄想返回其reference
任何时候看到一个引用声明式,你都应该立刻问自己,它的另一个名称是什么?
虽然函数参数最好用引用值,但函数返回值却不要随便去用引用,这回造成很多问题,比如引用的对象在函数结束后即被销毁,或是需要付出很多成本和代码来保证其不被销毁且不重复,这大概率没有必要,就返回一个值/对象就好了。
本文要点
- 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一 个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
条款22:将成员变量声明为private
封装重要性,可以确保class的约束条件总是会获得维护,因为只有成员函数可以影响它们。
protected并不比public更加具有封装性,因为protected修饰的成员变量一旦修改,也会造成子类的大量修改。
从封装的角度观之,其实只有两种访问权限: private (提供封装)和其他(不提供封装)
本文要点
- 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
- protected并不比public更具封装性。
条款23: 宁以non-member, non-friend替换member函数
提供non-member函数可允许对webBrowser相关机能有较大的包裹弹性 (packaging flexibility) ,而那最终导致较低的编译相依度,增加webBrowser的可延伸性。
void clearBrowser(WebBrowser& wb) {
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
愈少代码可以看到数据(也就是访问它) ,愈多的数据可被封装,而我们也就愈能自由地改变对象数据
在C++,比较自然的做法是让clearBrowser成为一个non-member函数并且位于 WebBrowser所在的同一个namespace (命名空间)内:
namespace WebBrowserStuff{
class WebBrowser{... };
void clearBrowser(WebBrowser& wb);
}
本文要点
- 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包 裹弹性(packaging flexibility)和机能扩充性。
条款24: 若所有参数皆需类型转换,请为此采用non-member函数
class Rational {
public:
const Rational operator*(const Rational& rhs) const;
};
#没问题
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;
result = oneHalf * 2; // result = oneHalf.operator*(2);
#错误
result = 2 * oneHalf; //出错 result = 2.operator*(oneHalf);
编译器知道你正在传递一个int,而函数需要的是Rational;但它也知道只要调用Rational构造函数并赋予你所提供的int,就可以变出一个适当的Rational来。
结论是,只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”–即this对象-的那个隐喻参数,绝不是隐式转换的合格参与者。
class Rational{
...
};
const Rational operator* (const Rational& 1hs, const Rational& rhs) {
return Rational (1hs.numerator() * rhs.numerator( ),
1hs.denominator() * rhs.denominator());
}
#编译通过
Rational oneFourth (1, 4);
Rational result; result = oneFourth * 2;
result = 2 * oneFourth;
operator*
无需设置为friend 因为operator*
可以完全藉由Rational的public接口完成任务
结论是,只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”–即this对象-的那个隐喻参数,绝不是隐式转换的合格参与者。
本文要点
- 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member.
条款25: 考虑写出一个不抛异常的swap函数
namespace WidgetStuff {
class widget {
public:
void swap (Widget& other)
{
using std::swap;
swap (pImpl, other.pImpl);
}
};
template<typename T>
void swap(widget<T>& a, widget<T>& b) {
a.swap(b);
}
}
#调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本。
template<typename T>
void dosomething(T& a, T& b) {
using std::swap;
...
swap(a, b);
}
}
本文要点
- 成员函数swap版本绝不可抛出异常
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对 于classes (而非templates) ,也请特化std::swapo
- 调用swap时应针对std: :swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
条款26: 尽可能延后变量定义式的出现时间
std::string encryptPassword(const std::string& password) {
using namespace std;
string encrypted;
if (password.length() < MinimumPasswordLength) {
throw logic_error ("Password is too short");
}
...
return encrypted;
)
如果函数encryptPassword丢出异常,你仍得付出encrypted的构造成本和析构成本。所以最好延后encrypted的定义式,直到确实需要它。
#1 效率低
std::string encrypted;
encrypted = password;
#高效
std::string encrypted (password);
你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
#做法A: 1个构造函数+1个析构函数+n个赋值操作
#方法A:定义于循环外
widget w;
for (int i=0; i <n; ++i) {
w = //取决于i的某个值;
}
#做法B: n个构造函数+n个析构函数
#方法B:定义于循环内
for (int i=0; i<n; ++i) {
widget w(//取决于i的某个值);
}
- 你知道赋值成本比“构造+析构”成本低
- 你正在处理代码中效率高度敏感(performance-sensitive)的部分,否则你应该使用做法B
本文要点
- 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
条款27: 尽量少做转型动作
- const-cast通常被用来将对象的常量性转除(cast away the constness)
- dynamic-cast主要用来执行“安全向下转型” (safe downcasting) ,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作
- reinterpret-cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。
- static_cast用来强迫隐式转换(implicit conversions)
C不可能发生这种事,Java不可能发生这种事,C#也不可能发生这种事。但C++可能!
class window {
public:
virtual void onResize() {...}
...
};
class Specialwindow: public Window {
public:
virtual void onResize() {
static_cast<window> (*this).onResize ();
...
}
}
解决之道是拿掉转型动作,代之以你真正想说的话。你并不想哄骗编译器将 *this视为一个base class对象,你只是想调用base class版本的onResize函数,令它作用于当前对象身上。
class Specialwindow: public window {
public:
virtual void onResize() {
#调用window:;onresize 作用于*this上
window::onResize();
}
}
本文要点
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic-casts.如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
- 宁可使用C++-style (新式)转型,不要使用旧式转型。前者很容易辨识出来, 而且也比较有着分门别类的职掌。
条款28: 避免返回handles指向对象内部成分
class Point {
public:
Point (int x, int y);
void setx(int newVal);
void setY(int newVal);
...
};
struct RectData {
Point ulhc;
Point lrhc;
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData;
};
#根据条款20 以by reference方式传递用户自定义类型往往比以by value方式传递更高效
#错误: 两个函数都返回references指向private内部数据,调用者于是可通过这些references更改内部数据!
class Rectangle {
public:
Point& upperLeft() const {
return pData->ulhc;
}
Point& lowerRight() const {
return pData->lrhc;
}
};
- 成员变量的封装性最多只等于“返回其 reference"的函数的访问级别。
- 如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
通常我们认为,对象的“内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为protected或private者)也是对象“内部”的一部分。因此也应该留心不要返回它们的handles.这意味你绝对不该令成员函数返回一个指针指向“访问级别较低”的成员函数。
#代码返回类型加上const即可
class Rectangle {
public:
const Point& upperLeft() const {
return pData->ulhc;
}
const Point& lowerRight() const {
return pData->lrhc;
}
...
};
但即使如此, upperLeft和lowerRight还是返回了“代表对象内部”的handles,有可能在其他场合带来问题。更明确地说,它可能导致dangling handles (空悬的号码牌): 这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。
本文要点
- 避免返回handles (包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码 牌” (dangling handles)的可能性降至最低。
条款29: 为“异常安全”而努力是值得的
void PrettyMenu::changeBackground (std::istream& imgSrc) {
using std:: swap;
Lock ml (&mutex) ;
std::tr1::shared ptr<PMImpl>
pNew (new PMImpl (*pImpl));
pNew->bgImage.reset (new Image (imgSrc));
++pNew->imageChanges;
swap (pImpl, pNew);
}
当异常被抛出时,带有异常安全性的函数会:
- 不泄漏任何资源。
- 不允许数据败坏。
异常安全函数(Exception-safe functions)提供以下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
- 强烈保证:如果异常被抛出,程序状态不改变。
- 不抛掷( nothrow)保证,承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。
智能指针使用reset,当new Image(imgSrc)成功后,才会删除旧图像。
class PrettyMenu{
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));//用new Image执行结果设定bgImage内部指针
++imageChanges;
}
第二、将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被称为pimpl idiom
copy-and-swap策略
当“强烈保证”不切实际时,你就必须提供“基本保证”
一般而言并不保证函数具有强烈的异常安全性
void PrettyMenu::changeBackground(std::istream& imgSrc) {
using std::swap;
Lock ml(smutex);
std::tr1::shared ptr<PMImpl>
pNew (new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
如果函数哪还对另外两个函数进行操作:
void someFunc() {
...
f1();
f2();
...
}
-
- 假设f1只提供基本保证,那么为了让someFunc提供强烈保证,我们必须写出代码获得调用f1之前的整个程序状态、捕捉f1的所有可能异常、然后恢复原状态。
-
- 如果F1和f2都是“强烈异常安全” ,情况并不就此好转。毕竟如果f1圆满结束,程序状态在任何方面都可能有所改变,因此如果f2随后抛出异常,程序状态和someFunc被调用前并不相同,甚至当F2没有改变任何东西时也是如此。
问题出在“连带影响” (side effects) 。如果函数只操作局部性状态(local state,例如somerunc只影响其“调用者对象”的状态),便相对容易地提供强烈保证。但是当函数对“非局部性数据” (non-local data)有连带影响时,提供强烈保证就困难得多。
本文要点
- 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30: 透彻了解inlining的里里外外
- 过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存, inline造成的代码膨胀亦会导致额外的换页行为(paging) ,降低 指令高速缓存装置的击中率(instruction cache hit rate) ,以及伴随这些而来的效率损失。
inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内。
class Person {
public:
int age() const
{
//一个隐喻的inline申请:
//age被定义于class定义式内。
return theAge;
}
...
private:
int theAge;
};
friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline.
template<typename T>
inline const T& std::max(const T& a, const T& b) {
return a < b ? b : a;
}
Template的具现化与inlining无关。如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline (不论显式或隐式) 。
本文要点
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为function templates出现在头文件,就将它们声明为inline.
条款31: 将文件间的编译依存关系降至最低
#include <string>
#include "date.h"
#include "address.h"
这么一来便是在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。
接口与实现分离
这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:
现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策略:
- 如果使用object references或object pointers可以完成任务,就不要使用objects。你可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某类型的objects,就需要用到该类型的定义式。
- 如果能够,尽量以class声明式替换class定义式。
- 为声明式和定义式提供不同的头文件。
另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class (抽象基类) ,称为Interface class。
本文要点
- Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies) 。
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。 基于此构想的两个手段是Handle classes和Interface classes.
- 程序库头文件应该以“完全且仅有声明式” (full and declaration-only forms)的 形式存在。这种做法不论是否涉及templates都适用。
virtual函数意味“接口必须被继承” , non-virtual函数意味“接口和实现都必须被继承”。
条款32: 确定你的public继承塑模出is-a关系
以C+进行面向对象编程,最重要的一个规则是: public inheritance (公开继承)意味"is-a" (是一种)的关系。
class Bird
{
public:
virtual void fly(){cout << "it can fly." << endl;}
};
class Penguin: public Bird
{
// fly()被继承过来了,可以覆写一个企鹅的fly()方法,也可以直接用父类的
};
int main()
{
Penguin p;
p.fly(); // 问题是企鹅并不会飞!
}
方法一,在Penguin的fly()方法里面抛出异常,一旦调用了p.fly(),那么就会在运行时捕捉到这个异常。这个方法不怎么好,因为它要在运行时才发现问题。
方法二,去掉Bird的fly()方法,在中间加上一层FlyingBird类(有fly()方法)与NotFlyingBird类(没有fly()方法),然后让企鹅继承与NotFlyingBird类。这个方法也不好,因为会使注意力分散,继承的层次加深也会使代码难懂和难以维护。
方法三,保留所有Bird一定会有的共性(比如生蛋和孵化),去掉Bird的fly()方法,只在其他可以飞的鸟的子类里面单独写这个方法。这是一种比较好的选择,因为根本没有定义fly()方法,所以Penguin对象调用fly()会在编译期报错。
在艰难选择方法三之后,我们回过头来思考,就是在所有public继承的背后,一定要保证父类的所有特性子类都可以满足(父类能飞,子类一定可以飞),抽象起来说,就是在可以使用父类的地方,都一定可以使用子类去替换。
这正是Liskov替代原则告诉我们的:任何父类可以出现的地方,子类一定可以替代这个父类,只有当替换使软件功能不受影响时,父类才可以真正被复用。通俗地说,是“子类可以扩展父类的功能,但不能改变父类原有的功能”。
本文要点
- “public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于 derived classes身上,因为每一个derived class对象也都是一个base class对象。
条款33: 避免遮掩继承而来的名称
int x;
void someFunc () {
double x;
std::cin >>x;
}
这个读取数据的语句指涉的是local变量x,而不是global变量x,因为内层作用域的名称会遮掩(遮蔽)外围作用域的名称。
class Base {
private:
int x;
public:
virtual void mf1() =0;
virtual void mf2();
void mf3 ();
};
class Derived: public Base {
public:
virtual void mf1();
void mf4();
};
#mf4函数实现部分
void Derived::mf4 () {
...
mf2();
}
当编译器看到这里使用名称mf2,必须估算它指涉(refer to)什么东西。编译器的做法是查找各作用域,看看有没有某个名为mf2的声明式。
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mfl(int);
virtual void mf2();
void mf3();
void mf3 (double);
...
};
#从名称查找观点来看,base::mf1和base::mf3不再被derived继承
Derived d;
int x;
d.mf1 (); //没问题,调用Derived::mf1
d.mf1 (x); //错误!因为Derived: :mf1遮掩了Base::mf1
d.mf2 ();
d.mf3 ();
d.mf3 (x); //错误!因为Derived::mf3遮掩了Base::mf3
解决方法1 using声明
用using声明式来在子类中声明父类的同名函数(重载函数不需要声明多个),此时父类的各重载函数就是子类可见的了。
class Derived:public Base {
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};
解决方法2 转交函数
使用转交函数,即在子类函数的声明时进行定义,调用父类的某个具体的重载函数(此时由于在声明时定义,成为inline函数),此举可以只让需要的部分父类重载函数于子类可见。
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
}
class Derived:private Base {
public:
virtual void mf1() {
Base::mfl();
};
Derived d;
int x;
d.mf1(); //调用Derived::mf1
d.mf1(x); //错误!Base::mf1被遮掩了
本文要点
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
- 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)
条款34: 区分接口继承和实现继承
public继承概念由两部分组成:
- 函数接口(function interfaces)继承
- 函数实现(function implementations)继承
这两种继承的差异很像函数声明与函数定义之间的差异。
- 成员函数的接口总是会被继承。一如条款32所说, public继承意味is-a (是一 种),所以对base class为真的任何事情一定也对其derived classes为真。
- 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。 我们可以为pure virtual函数提供定义。也就是说可以为Shape::draw供应一份实现代码, C++并不会发出怨言,但调用它的唯一途径是“调用时明确指出其class名称”
Shape* ps1 = new Rectangle;
ps1->Shape::draw(); //调用Shape::draw
- 声明简朴的(非纯) impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
“你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本”。
class Shape {
public:
virtual void error(const std::string& msg);
};
class Airplane {
public:
virtual void fly(const Airport& destination) = 0; //pure virtual
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination) {
#缺省行为,将飞机飞至指定的目的地。
}
#inline函数和virtual函数之间的交互关系
class ModelA:public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
};
class ModelB:public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
};
#Airplane 中的pure virtual函数迫使Mode1C必须提供自己的fly版本:
class ModelC:public Airplane {
public:
virtual void fly(const Airport& destination);
};
Airplane继承体系如何给pure virtual函数一份定义
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
};
void Airplane::fly(const Airport& destination) {
//缺省行为,将飞机飞至指定的目的地
}
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destination); //airplane
}
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destination); //airplane
}
}
class Modelc: public Airplane {
public:
virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination) {
//将c型飞机飞至指定的目的地
}
如果成员函数是个non-virtual函数,意味是它并不打算在derived classes中有不同的行为。实际上一个non-virtual成员函数所表现的不变性(invariant)凌驾其 特异性(specialization) ,因为它表示不论derived class变得多么特异化,它的行为都不可以改变。
- 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
class Shape {
public:
int objectID() const;
}
pure virtual函数、simple (impure) virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。
两个常见的错误
- 第一个错误是将所有函数声明为non-virtual。这使得derived classes没有余裕空间进行特化工作。non-virtual析构函数尤其会带来问题(见条款7) 。
- 另一个常见错误是将所有成员函数声明为virtual
本文要点
- 接口继承和实现继承不同。在public继承之下, derived classes总是继承base class的接口。
- pure virtual函数只具体指定接口继承。
- 简朴的(非纯) impure virtual函数具体指定接口继承及缺省实现继承。
- non-virtual函数具体指定接口继承以及强制性实现继承。
条款35: 考虑virtual函数以外的其他选择
Non-Virtual Interface手法实现Template Method模式
class GameCharacter {
public:
int healthvalue() const {
...
int retVal = doHealthValue ();
return retVal;
...
}
...
private:
virtual int doHealthValue() const
{
...
}
};
这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface (NVI)手法。
藉由Function Pointers实现Strategy模式
class GameCharacter;
//以下函数是计算健康指数的缺省算法。
int defaultHealthCalc (const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc) (const GameCharacter&);
explicit GameCharacter (HealthCalcFunc hcf = defaultHealthCalc) : healthFunc (hcf)
{}
int healthValue() const {
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
}
- 同一人物类型之不同实体可以有不同的健康计算函数。
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter (hcf)
{...}
...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthslowly(const GameCharacter&);
#相同类型的人物搭配, 不同的健康计算方式
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthslowly);
- 某已知人物之健康指数计算函数可在运行期变更。
“健康指数计算函数不再是GameCharacter继承体系内的成员函数”这一事实意味,这些计算函数并未特别访问“即将被计算健康指数”的那个对象的 内部成分。
- 藉由tr1:function完成Strategy模式
如果我们不再使用函数指针(如前例的healthFunc) ,而是改用一个类型为tr1l:: function的对象,这些约束就全都挥发不见了。
class GameCharacter; //如前
int defaultHealthcalc (const GameCharacter& gc); //如前
class GameCharacter {
public:
//HealthCalcFunc可以是任何“可调用物” (callable entity) ,
//可被调用并接受任何兼容于GameCharacter之物,返回任何兼容于int的东西。详下。
typedef std::tr1::functio<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter (HealthCalcFunc hcf = defaultHealthCalc) : healthFunc (hcf )
{}
int healthValue () const {
return healthFunc(* this);
}
...
private:
HealthcalcFunc healthFunc;
};
std::tr1::function<int (const GameCharacter&)>
“接受一个reference指向const GameCharacter,并返回int”。
这个trl::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物(callable entity)。
所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacterk,而其返回类型可被隐式转换为into
short calcHealth (const GameCharacter&);
struct HealthCalculator {
int operator () (const GameCharacter&) const
{ ..}
};
class GameLevel {
public:
float health (const GameCharacter&) const;
...
};
class EvilBadGuy: public GameCharacter {
...
};
class EyeCandyCharacter: public GameCharacter {
...
};
EvilBadGuy ebg1(calcHealth);
EyeCandyCharacter eccl(HealthCalculator()); //使用某个函数对象计算健康指数
GameLevel currentLevel;
...
EvilBadGuy ebg2 (
std::tr1::bind(&GaneLevel::health, currentLevel, _1) //使用某个成员函数计算健康指数
);
- 古典的Strategy模式
class GameCharacter;
class HealthCalcFunc {
public:
virtual int calc(const GameCharacter& gc) const {
...
}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter (HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthCalc (phcf)
{}
int healthValue() const
{
return pHealthCalc->calc(*this);
}
private:
HealthcalcFunc* pHealthCalc;
};
本文要点
- 使用non-virtual interface (NVI)手法,那是Template Method设计模式的一种 特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected) 的virtual函数。
- 将virtual函数替换为“函数指针成员变量” ,这是Strategy设计模式的一种分解表现形式。
- 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物 (callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
- 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访 问class的non-public成员。
- trl::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)
条款36: 绝不重新定义继承而来的non-virtual函数
class B {
public:
void mf();
};
class D: public B{
void mf();
...
};
#
D x;
B* pB =&x;
D* pD =&x;
pb->mf(); //Bmf
pd->mf(); //Dmf
由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生之class"的对象, virtual函数是动态绑定, 不论是pb,pd都是导致调用Dmf, 因为pB和pD真正指的都是一个类型为D的对象。
- 适用于B对象的每一件事,也适用于D对象,因为每个D对象都是一个B对象;
- B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数。
本文要点
- 绝对不要重新定义继承而来的non-virtual函数。
条款37: 绝不重新定义继承而来的缺省参数值
virtual函数系动态绑定(dynamically bound) ,而缺省参数值却是静态绑定(statically bound)
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
/所有形状都必须提供一个函数,用来绘出自己
virtual void draw (ShapeColor color = Red) const =0;
};
class Rectangle: public Shape {
public:
/注意,赋予不同的缺省参数值。这真糟糕!
virtual void draw (ShapeColor color = Green) const;
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};
Shape* ps;
Shape* pc = new circle; //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为shape*
ps = pc;
ps = pr;
pc->draw(Shape::Red); //调用circle::draw(Shape::Red)
pr->draw(Shape::Red) ; //调用Rectangle::draw(Shape::Red)
virtual函数是动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值
pr->draw(); //调用Rectangle::draw (Shape::Red)
Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态类型是Shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class!
class Shape
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw (ShapeColor color =Red) const =0;
};
class Rectangle: public Shape {
public:
virtual void draw (ShapeColor color =Red) const;
...
}
代码重复又带着相依性(with dependencies): 如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derivedclasses也必须改变,否则它们最终会导致“重复定义一个继承而来的缺省参数值”。
NVI方法
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw (ShapeColor color =Red) const {
doDraw (color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape {
public:
...
private:
virtual void doDraw (Shapecolor color) const; //注意,不须指定缺省参数值。
...
};
本文要点
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数–你唯一应该覆写的东西-却是动态绑定。
条款38: 通过复合塑模出has-a或"根据某物实现出"
class Address{...};
class PhoneNumber {...};
class Person {
public:
//...
private:
std::string name;
Address address;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
};
条款32曾说, “public继承”带有is-a (是一种)的意义。 复合意味has-a(有一个)或is-implemented-in-terms-of(根据某物实现出) 。 当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于 实现域内则是表现is-implemented-in-terms-of的关系。
本文要点
- 复合(composition)的意义和public继承完全不同。
- 在应用域(application domain) ,复合意味has-a (有一个) 。在实现域 (implementation domain) ,复合意味is-implemented-in-terms-of (根据某物实现出)。
条款39: 明智而审慎地使用private继承
private继承主要用于“当一个意欲成为derived class者想访问 一个意欲成为base class者的protected成分,或为了重新定义一或多个virtual函数” 但这时候两个classes之间的概念关系其实是is-implemented-in-terms-of (根据某物实现出)而非is-a。
本文要点
- Private继承意味is-mplemented-in-terms of (根据某物实现出) 。它通常比复合 (composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
- 和复合(composition)不同, private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
条款40: 明智而审慎地使用多重继承
- 多重继承(multiple inheritance; MI)
- 单一继承(single inheritance; SI)
当MI进入设计景框,程序有可能从一个以上的base classes继承相同名称(如函数、typedef等等)。那会导致较多的歧义(ambiguity)机会。
class Borrowableltem {
public:
void checkout();
};
class ElectronicGadget {
private:
bool checkOut() const;
};
class MP3Player: public Borrowableltem,
public ElectronicGadget
{..};
MP3Player mp;
mp.checkOut(); //调用哪个checkout
为了解决这个歧义,你必须明白指出你要调用哪一个base class内的函数:
mp.Borrowableltem::checkOut();
“钻石继承”
class File {...};
class Inputfile: virtual public File {...};
class Outputfile: virtual public File {...};
class IOFile: public Inputfile, public OutFile
{...};
使用virtual继承的那些classes所产生的对象往往比使 用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。种种细节因编译器不同而异,但基本重点很清楚:你得为virtual继承付出代价。
virtual base class忠告
- 第一,非必 要不使用virtual bases。平常请使用non-virtual继承。
- 第二,如果你必须使用virtualbase classes,尽可能避免在其中放置数据。
本文要点
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
- virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtualbase classes不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及"public继承某个Interface class和"private继承某个协助实现的class"的两相组合。
Re: