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();
    ...
}
    1. 假设f1只提供基本保证,那么为了让someFunc提供强烈保证,我们必须写出代码获得调用f1之前的整个程序状态、捕捉f1的所有可能异常、然后恢复原状态。
    1. 如果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:

https://blog.csdn.net/qq_36915078/article/details/104718945

https://harttle.land/tags.html#Effective-C++