effective c++读书笔记

effective c++读书笔记

    • Charpter 1. 让自己习惯C++
      • 条款01: 视C++为一个语言联邦
      • 条款02: 尽量以const,enum,inline替换#define
      • 条款03: 尽可能使用const
      • 条款04: 确定对象被使用前已先被初始化
    • Charpter 2. 构造/析构/赋值运算
      • 条款05: 了解C++默默编写并调用哪些函数
      • 条款06: 若不想使用编译器自动生成的函数,就该明确拒绝
      • 条款07: 为多态基类声明virtual析构函数
      • 条款08: 别让异常逃离析构函数
      • 条款09: 绝不在构造和析构过程中调用virtual函数
      • 条款10: 令operator=返回一个reference to *this
      • 条款11: 在operator=中处理“自我赋值”
      • 条款12: 复制对象时勿忘其每一个成分
    • Chapter 3.资源管理
      • 条款13: 以对象管理资源
      • 条款14: 在资源管理类中小心coping行为
      • 条款15: 在资源管理类中提供对原始资源的访问
      • 条款16: 成对使用new和delete时要采取相同形式
      • 条款17: 以独立语句将newed对象置入智能指针
    • Charpter 4. 设计与声明
      • 条款18: 让接口容易被正确使用,不易被误用
      • 条款19: 设计class犹如设计type
      • 条款20: 宁以pass-by-reference-to-const替换pass-by-value
      • 条款21: 必须返回对象时,别妄想返回其reference
      • 条款22: 将成员变量声明为private
      • 条款23: 宁以non-member、non-friend替换member函数
      • 条款24: 若所有参数皆需类型转换,请为此采用non-member函数
      • 条款25: 考虑写出一个不抛异常的swap函数
    • Charpter 5. 实现
      • 条款26: 尽可能延后变量定义式的出现时间
      • 条款27: 尽量少做转型动作
      • 条款28: 避免返回handles指向对象内部成分
      • 条款29: 为“异常安全”而努力是值得的
      • 条款30: 透彻了解inlining的里里外外
      • 条款31: 将文件间的编译依存关系降至最低
      • handle class
      • Interface class
    • Chapter 6.继承与面向对象设计
      • 条款32: 确定你的public继承塑模出is-a关系
      • 条款33: 避免遮掩继承而来的名称
      • 条款34: 区分接口继承和实现继承
      • 条款35: 考虑virtual函数以外的其他选择
      • 条款36: 绝不重新定义继承而来的non-virtual函数
      • 条款37: 绝不重新定义继承而来的缺省参数值
      • 条款38: 通过复合塑模出has-a或“根据某物实现出”
      • 条款39: 明确而审慎地使用private继承
      • 条款40: 明智而审慎地使用多重继承
    • Chapter 7.模版与泛型编程
      • 条款41: 了解隐式接口和编译期多态
      • 条款42: 了解typename的双重意义
      • 条款43: 学习处理模版化基类内的名称
      • 条款44: 将与参数无关的代码抽离templates
      • 条款45: 运用成员函数模版接受所有兼容类型
      • 条款46: 需要类型转换时请为模版定义非成员函数
      • 条款47: 请使用traits classes表现类型信息
      • 条款48: 认识template元编程
    • Chapter 8.定制new和delete
      • 条款49: 了解new-handler的行为
      • 条款50: 了解new和delete的合理替换时机
      • 条款51: 编写new和delete时需固守常规
      • 条款52: 写了placement new也要写placement delete
    • Chapter 9.杂项讨论
      • 条款53: 不要轻忽编译器的警告
      • 条款54: 让自己熟悉包括TRI在内的标准程序库
      • 条款55: 让自己熟悉Boost

Charpter 1. 让自己习惯C++

条款01: 视C++为一个语言联邦

条款02: 尽量以const,enum,inline替换#define

  1. 对于单纯常量,最好以const对象或enums替换#defines
  2. 对于形似函数的宏(macros),最好改用inline函数替换#defines
    • 可提供封装性,因为defines就是全局替换而const可以有class作用
    • define编译报错的时候由于#define不会被编译器看到(已被替换,即不会进入记号表),会显示一堆乱码。而const不会。
    • inline提供函数检查

条款03: 尽可能使用const

const char * p //常量指针,不能通过p改变变量的值,但可以通过别的方式改变,p可以变。
char const * p //跟第一个一样
char * const p //指针常量,p不能指向别处了
const int* const p=&n2; //指向的变量值不能变,也不能指向别处
const_iterator //相当于常量指针
  • 将东西声明为const可以帮助编译器检查出错误用法。const可以施加于任何作用域的对象,函数参数,函数返回类型,成员函数本体

  • 区别bitwise constness和conceptual constness。前者是编译器强制实现,后者是概念上,成员函数声明了const就不应该以任何方式修改成员变量。

    class CTextBlock{
    public:
        ...
        char* operator[](std::size_t position)const
        	{return pText[position];}
    private:
        char* pText;
    };
    //上述类可以通过编译,但在这样调用下,仍会更改成员变量的值
    
    const CTextBlock cwd("hello");
    char* pc = &cwd[0];
    *pc = 'j';
    //这样就产生了"jello"
    
  • 当const和non-const成员函数有实质等价的实现的时候,让non-const版本调用const版本避免代码重复。

条款04: 确定对象被使用前已先被初始化

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

  • 构造函数最好使用初值列,而不要再构造函数北部体内使用赋值操作。(那样的话是先用默认构造函数,然后赋新值)初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

  • 为免除“跨编译单元的初始化顺序”问题,用local static对象替换non-local static对象

    ​ 一般来说,在函数内部的static对象都叫做”local-static”,以外的叫做”non-local-static”。现假设有两个cpp源文件,各自的类里有一个non-local-static对象,其中一个的初始化动作会使用到另一个static,但是被使用的对象可能还未初始化,此时会发生错误。

    将两个类各自的non-local-static搬到属于自己的函数里,将它们变成local-static,函数会返回对象的引用。此操作保证了调用函数时,此函数包含的static对象一定会被初始化。很棒的是,只要你还未调用此函数,就不会有构造与析构成本。因为c++保证,函数内的local static对象会在该函数被调用器件,首次遇上该对象之定义式时被初始化。(对标单例模式的懒汉模式)

Charpter 2. 构造/析构/赋值运算

条款05: 了解C++默默编写并调用哪些函数

class Empty
 {
 	public:
 	Empty(); //缺省默认构造函数
 	Empty(const Empty&);//拷贝构造函数
 	~Empty(); //析构函数
    Empty& perator=(const Empty&); //赋值运算符
  	Empty* operator&(); //取址运算符
  	const Empty* operator&() const;  // 取址运算符
 };

条款06: 若不想使用编译器自动生成的函数,就该明确拒绝

​ 为了不使用编译器自动提供的函数,可以将其声明为private并且不予实现。

条款07: 为多态基类声明virtual析构函数

  • 一个基类如果含有任何virtual函数,就应该声明一个虚析构函数。
  • 一个类的设计目的如果不是作为基类,就不要设置虚析构函数,虚函数带来的vptr等会有空间的开销

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

  • c++并不禁止析构函数产生异常,但是不鼓励。(否则内存泄漏)如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉异常然后处理,或者结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数而非在析构函数中执行操作。

条款09: 绝不在构造和析构过程中调用virtual函数

在构造和析构过程中,因为derived的成员变量处于未定义状态,虚函数并不会像期望的那样调用derived class的版本。故而出现混乱。

条款10: 令operator=返回一个reference to *this

reference to *this就是指向自己的对象。就是把自己本身返回回来。这样做的目的是使这样的连锁赋值合理

x=y=z=15;

条款11: 在operator=中处理“自我赋值”

例如a[i]=a[j];就存在潜在的自我赋值的可能。

//假设有这样一个类
class Bitmap {...};
class Widget{
    ...
private:
    Bitmap* pb;
}

Widget& Widget::operator=(const Widget& rhs){
    if(this==&rhs) return *this;//证同测试,若在自我赋值,直接返回就完了
    delete pb;  //若没有上一行,此处就把要赋值的东西给删掉了。
    pb= new Bitmap(*rhs.pb);  //若没有上一行,这里的pb是一个野指针
    return *this;
}

解决办法一个是证同测试,或者先copy再swap

条款12: 复制对象时勿忘其每一个成分

当编写一个copy函数的时候,确保

  1. 复制所有的local变量
  2. 调用所有的base class内的copy函数。在初值列里
class PriorityCustomer : public Customer {
public:
	PriorityCustomer(const PriorityCustomer& rhs)
		: Customer(rhs), // 调用base class的copy构造函数
		  priority(rhs.priority)
	{
		logCall("PriorityCustormer copy constructor");
	}
 	....
};

Chapter 3.资源管理

条款13: 以对象管理资源

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

Resource-Acquisition Is Initialization;资源取得时机就是初始化时机。RAII的存在就是为了确保资源释放一定会发生。比如智能指针(shared_ptr, auto_ptr),懒汉单例模式。我们在获得资源的同一个语句内以它初始化某个管理对象或者赋值给某个管理对象,总之资源在获得的同时立刻被放进管理对象中。一旦对象被销毁,其析构函数会自动调用,于是资源被释放。

条款14: 在资源管理类中小心coping行为

RAII的一个问题就是,被复制会发生什么事。一般有两种处理方式,禁止复制或者在底层加上引用计数法。

条款15: 在资源管理类中提供对原始资源的访问

  • API往往要求访问原始资源(shared_ptr指向的东西),所以每一个RAII class应该提供一个“取得其管理的资源”的方法

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

    //有这样的api函数
    FontHandle getFont();
    void releaseFont(FontHandle fh);
    void changeFontSize(FontHandle f,int newsize);
    
    class Font{
    public:
        explicit Font(FontHandle fh):f(fh){}
        ~Font() {releaseFont (f);}
        FontHandle get() const { return f;}//显式转换函数
        operator FontHandle() const { return f;}//隐式转换函数
    private:
        FontHandle f;
    }
    
    //客户可以这样调用
    Font f(getFont());
    int newFontSize;
    changeFontSize(f.get(),newFontSize);//调用显式转换函数
    changeFontSize(f,newFontSize);//调用隐式转换函数
    

    对原始资源的访问与class的封装是相悖的,但是RAII的类用意是在资源管理而不是封装。

条款16: 成对使用new和delete时要采取相同形式

new[]对应delete[]

new对应delete

条款17: 以独立语句将newed对象置入智能指针

processWidget(std::shared_ptr<Widget>(new Widget),priority());

考虑上述语句,编译器可能是这样的执行顺序

  1. new Widget
  2. priority()
  3. 调用shared_ptr构造函数

那么当priority()调用导致异常,那么new Widget返回的指针丢失了,就发生资源泄露。应改为

std::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());

Charpter 4. 设计与声明

条款18: 让接口容易被正确使用,不易被误用

  1. 好的接口很容易被正确使用,不容易被误用。努力达成这些性质(例如 explicit关键字)
  2. “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
  3. “防治误用”b包括建立新类型,限制类型上的操作,束缚对象值,以及消除用户的资源管理责任
  4. shared_ptr支持定制deleter,需要灵活使用

条款19: 设计class犹如设计type

条款20: 宁以pass-by-reference-to-const替换pass-by-value

  1. 尽量以pass-by-reference-to-const替换pass-by-value,比较高效(pass-by-value方式的参数传递成本是一次构造函数和一次析构函数的调用),并且可以避免切割问题

  2. 以上规则并不使用内置类型,以及STL迭代器,和函数对象。它们采用pass-by-value更合适(其实采用pass-by-reference-to-const也可以)

    pass-by-reference往往以指针的形式实现

    void diaplay(baseclass a){
        ...
    }
    //这样一个函数,传进去的哪怕是一个derivedclass,里面的虚函数调用,仍然是调用的基类版本,因为已经复制过了。
    
    void diaplay(const baseclass& a){
        ...
    }
    //这样以引用调用,就会调用子类的函数版本。
    //const用于修饰参数不会在函数内被修改
    

条款21: 必须返回对象时,别妄想返回其reference

  1. 不要返回pointer或者reference指向一个on stack对象(被析构)

  2. 不要返回pointer或者reference指向一个on heap对象

    书上举的例子是operator * 无法处理 w=x*y*z这样的情况。

  3. 不要返回pointer或者reference指向local static对象,却需要多个这样的对象(static只能有一份,后面的使用会覆盖前面的)

条款22: 将成员变量声明为private

  1. 切记将成员变量申明为private
  2. protected并不比public更有封装性(用户可能继承你的base class)

修改了一个public成员变量,所有使用它的用户代码都需要改变。

修改了一个protected成员变量,所有derived classes都需要重构。

以上二者都有不可预知的大量代码被改变。

而修改private成员变量,只需要修改使用它的成员函数。客户最多只需要重新编译。

条款23: 宁以non-member、non-friend替换member函数

可增加封装性和包裹弹性以及机能扩充性。

  • 封装性 :多一个成员函数,就多一分破坏封装性
  • 扩充性:可以写出大量的non-member便利函数,然后客户使用的时候,引入需要的便利函数在的头文件就可以了,而class不能被这样拆分。

条款24: 若所有参数皆需类型转换,请为此采用non-member函数

class Rational{
public:
    Rational(int numerator = 0,int denominator = 1);//故意不放explicit,允许从int隐式转换
    int numerator() const;
    int denominator() const;
    const Rational operator*(const Rational& rhs) const;//以成员函数形式定义
private:
    ...
}

//客户调用如下
Rational oneEight(1,8);
Rational oneHalf(1,2);
Rational result;
result = oneHalf*2;//可以调用
result = 2*oneHalf;//不可以

//本条款给出的解决办法
const Rational operator*(const Rational& lhs,const Rational& rhs);//non-member函数
result = oneHalf*2;//可以调用
result = 2*oneHalf;//也可以

条款25: 考虑写出一个不抛异常的swap函数

  1. 当std::swap效率不高(std::swap调用拷贝构造函数和赋值操作符,如果是深拷贝,效率不会高),提供一个swap成员函数,并确定不会抛出异常。

    class Obj{
        Obj(const Obj&){//深拷贝}
        Obj& operator= (const Obj&){深拷贝
    private:
        OtherClass *p;//深拷贝时会拷贝OtherClass里的内容,实则没有必要,此时就需要自己写swap
    };
    
  2. 如果提供一个member swap,也该提供一个non-member swap用来调用前者.对于class而不是class template,还需要特化std::swap

    class WidgetImpl{
    public:
        ...
    private:
        int a,b,c;
        std::vector<double> v;//此数据在swap时应避免交换
    };
    
    class Widget{
    public:
        Widget (const Widget& rhs);
        Widget& operator=(const Widget& rhs){
            ...
            *pImpl = *(rhs.pImpl);
            ...
        }
        void swap(Widget& other){
            using std::swap;//这样可以让编译器自己决定调用哪个swap,万一用户没有实现针对Obj的swap,还能调用std::swap
            swap(pImpl,other.pImpl);
        }
        ...
    private:
        WidgetImpl* pImpl;
    };
    //特化版本
    template<>
    void swap<Widget>(Widget& a,Widget& b){
        a.swap(b);//置换Widget,调用membet swap就可以了
    }
    
  3. 调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何"命名空间修饰”

  4. 不要往std命名空间里面加东西

Charpter 5. 实现

条款26: 尽可能延后变量定义式的出现时间

条款27: 尽量少做转型动作

  • const_cast通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C+±style转型操作符。
  • dynamic_cast主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作
  • reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类转型在低级代码以外很少见。本书只使用一次,那是在讨论如何针对原始内存(rawmemory)写出一个调试用的分配器(debugging allocator)时,见条款50。
  • static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象(就像条款3所为),或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将 void*指针转为 typed 指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const——这个只有const_cast才办得到。

请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

条款28: 避免返回handles指向对象内部成分

第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

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

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

异常安全函数(Exception-safe functions)提供以下三个保证之一:

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
  • 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
  • 不抛掷(nothrow)保证,承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。

copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • “强烈保证”往往能够以 copy-and-swap 实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30: 透彻了解inlining的里里外外

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为function templates出现在头文件,就将它们声明为inline。

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

考虑这样一个类

#include 
#inlcude "date.h"
#include "address.h"
class Person{
public:
    Person(const std::string & name,const Date & birthday,const Address &addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    ...
private:
    std::string theName;
    Date theBirthdate;
    Address theAddress;
}

这样写在person定义文件和其含入文件之间形成了一种编译依存关系,如果这些头文件中的任何一个改变,或者头文件所依赖的其他头文件有任何改变,每一个含入Person class的文件也必须重新编译。

handle class

可以将其分隔为两个class,一个提供接口,一个负责实现

#include 
#include 
class PersonImpl;
class Date;
class Addressl;
class Person{
public:
    Person(const std::string & name,const Date & birthday,const Address &addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    ...
private:
    std::tr1::shared_ptr<PersonImpl> pImpl;//指针,指向实现的类
}

实现程序这样写

#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string & name,const Date & birthday,const Address &addr):
	pImpl(new PersonImpl(name,birthday,addr))
    {}
std::string Person::name() const{
    return pImpl->name();
}

这样Dates,Address以及Persons的实现分离,任何修改都不需要person的客户端重新编译。

Interface class

class Person{
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    ...
    static std::tr1::shared_ptr<Person>
        create(const std::string&name,
              const Date& birthday,
              const Address &addr);
    //create函数就是工厂函数,返回指针指向动态分配的对象,该对象他又支持person(Interface class)的接口
    ...
};

客户可以这样使用

std::string name;
Date dateOfBirth;
Address address;
...
std::tr1::shared_ptr<Person> pp(Person::create(name,dateOfBirth,address));
...
std::cout<<pp->name()<<"was born on"<<pp->birthDate()<<"and now lives at"<<pp->address();
...
//当pp离开作用域,会自动消除 

Interface class会有一个或多个derived class.如RealPerson

class RealPerson: public Person{
public:
    RealPerson(const std::string & name,const Date & birthday,const Address &addr)
        :thename(name),theBirthDate(birthday),theAddress(addr)
        {}
    virtual ~RealPerson() {}
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    ...
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
}

//有了RealPerson之后,写create就简单了
std::tr1::shared_ptr<Person> create(const std::string&name,
              const Date& birthday,
              const Address &addr)
{
    return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));
    //真正的实现不会这么简单,应该有不同的derived class对象,在不同的情况下创建,均在该函数内实现。
}
  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。以上详述。
  • 程序库头文件应该以“完全且仅有声明式”(full and declaration-onlyforms)的形式存在。这种做法不论是否涉及templates都适用。如果将“提供class定义式(通过#include完成)的义务从“”函数声明所在”的头文件转移到“内含函数调用”的客户文件,便可将“非真正必要之类型定义”与客户端之间的编译依存性去掉。

Chapter 6.继承与面向对象设计

条款32: 确定你的public继承塑模出is-a关系

public inheritance(公开继承)意味 “is-a”(是一种)的关系。

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

条款33: 避免遮掩继承而来的名称

  • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用 using 声明式或转交函数(forwardingfunctions)。
class Base{
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2(int);
    ...
};
class Derived:public Base{
public:
    //使用using 声明式让Base class内名为mf1,mf2的所有东西在Derived作用域内都可见。
    using Base::mf1;
    using Base::mf2;
    void mf1();
}

//客户使用
Derived d;
int x;
d.mf1();//调用Derived::mf1
d.mf1(x);//调用Base::mf1;若没有using声明则会报错,因为被mf1被覆盖了,而参数列表不同。
/*------------------------------------------------------------------------------------------------------------------------------------*/
//转交函数的设计方法
class Derived:private Base{
public:
    virtual void mf1(){
        Base::mf1();
    }
    void mf1();
}
//这样可以只继承无参数版本
//客户使用
Derived d;
int x;
d.mf1();//调用Derived::mf1
d.mf1(x);//错误!

条款34: 区分接口继承和实现继承

函数接口(function interfaces)继承和函数实现(functionimplementations)继承。

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

pure virtual函数、simple (impure) virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。

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

注意,纯虚函数要求其在derived classes中重新声明,但是也可以有自己的实现。

  • pure virtual函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

条款35: 考虑virtual函数以外的其他选择

这是一种设计手法,令客户通过调用public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法.是模板方法设计模式的一个表现形式。

我们来看下面这段短小的C++程序

#include 
using namespace std; 
class Base { 
public: 
	void f() { 
        ...
        //可以进行事前工作,在virtual函数真正工作之前设定场景,锁定互斥锁,设定日志,验证约束条件等
		g(); 
        ...
        //可以进行事后工作,在virtual函数真正工作之后收尾
	}

private: 
	virtual void g() { 
		cout << "Hi, MorningStar! I am g() of Base!." << endl; 
	} 
};

class Derived : public Base { 
private: 
	virtual void g() { 
     //在继承类里重新定义virtual函数的实现
		cout << "Hi, MorningStar! I am g() of Derived." << endl; 
	} 
};

int main() { 
	Base *pB = new Derived(); 
	pB->f(); 
	delete pB; 
	return 0; 
}

程序很简单,我们在基类“Base”中,通过公有方法f调用了虚拟的私有方法g,而继承自Base的Derived类中覆写(override)了私有的方法g。

先不考虑其他,语法上有问题吗?没有;编译能通过吗?能。那运行结果是什么呢?既然编译能通过,那么结果也就能猜到了,应该是:

运行结果:Hi, MorningStar! I am g() of Derived.

从结果来看,对私有方法g的调用在运行时绑定到了对象实际类型(Derived)的方法上,这符合虚拟函数的语义。f()更像是一个包覆器,确保在virtual函数进行真正工作之前和之后环境的设定。

注意Startegy设计模式也可以参考。例子如下:

class GameCharacter;//前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    //定义参数为(const GameCharacter&),返回值为int的函数指针名字叫HealthCalFunc
    typedef int (*HealthCalFunc) (const GameCharacter&);
    explicit GameCharacter(HealthCalFunc hcf = defaultHealthCalc): healthFunc (hcf){}
    int healthValue() const{
        return healthFunc(*this);
    }
    ...
private:
    HealthCalFunc healthFunc;
}

上例中,同一类的不同对象,也可以有不一样的HealthCalFunc实现,可以增加如set_HealthCalFunc()函数,在运行期间修改对象的HealthCalFunc函数,完成策略模式。

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

条款36: 绝不重新定义继承而来的non-virtual函数

引发奇异性。虽编译通过,但语义上不对。若需要覆盖,为什么不直接在base上加上virtual呢

条款37: 绝不重新定义继承而来的缺省参数值

#include 
using namespace std;
class base{
public:
    
    virtual void show(int a = 0){
        cout<<"pure virtual shows a="<<a<<endl;
    };
};

class derived1:public base{
public:
    void show(int a = 1){
        cout<<"derived1 shows a="<<a<<endl;
    }
};

int main(int argc, char const *argv[])
{
    base *p1=new derived1();
    p1->show();
    return 0;
}
//derived1 shows a=0
//虚函数和继承函数各出一半力。因为虚函数是动态绑定而缺省参数是静态绑定。

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

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

复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。

当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

  • 复合(composition)的意义和public继承完全不同。
  • 在应用域(application domain),复合意味 has-a (有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)

复合的典型表现是STL里面的stack里面有vector。map里面有RB-tree

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

Private 继承意味 implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术(这就是为什么继承自一个privatebase class的每样东西在你的class内都是private:因为它们都只是实现枝节而已)。借用条款34 提出的术语,private 继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。Private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。继承的编译依赖性也较高。

  • Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected baseclass的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
  • 没有以上两条需求的时候,尽量使用复合。

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

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtualbase classes不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interfaceclass”和“private继承某个协助实现的class”的两相组合。

virtual继承

class File{};
class Inputfile:public file{};
class Outputfile:public file{};
class IOFike: public InputFile,
			public OutFile{};

考虑上述继承关系,当file类里面有一个filename()函数的话,IOfile里应该有几份呢。为解决这个问题,采用virtual public继承,则只会含有一份,但是需要付出virual的代价,就是访问成员变量的速度变慢,大小变大初始化变复杂等成本。

class File{};
class Inputfile:virtual public file{};
class Outputfile:virtual public file{};
class IOFike: public InputFile,
			public OutFile{};

Chapter 7.模版与泛型编程

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

面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtimepolymorphism)解决问题。Templates 及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)移到前头了。

  • classes和templates都支持接口(interfaces)和多态(polymorphism)。
  • 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
  • 对 template 参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

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

template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。

C++有个规则可以解析(resolve)此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。

一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字 typename。

“typename 必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename 不可以出现在 base classes list 内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。

  • 声明template参数时,前缀关键字class和typename可互换。

  • 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。

    stl里面经典的用法:

    typedef typename std::iterator_traits<IterT>::value_type value_type;
    

    typename用于向编译器说明,std::iterator_traits::value_type 是一个typename而不是iterator_traits里面的static成员变量。

条款43: 学习处理模版化基类内的名称

可在derived class templates内通过 “this->” 指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。

//方法1
this->sendClear(info);
//方法2
using MsgSender<Company>::sendClear;//告诉编译器,假设sendClear在base class里面
//方法3
MsgSender<Company>::sendClear(Info);//调用的时候,直接指定,但这个相当于拒绝了virtual发生作用

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

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
  • 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
  • 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiationtypes)共享实现码。

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

template<typename T>
class SmartPtr{
public:
    //以下就是成员函数模板
    template<typename U>
    SmartPtr(const SmartPtr<U>& other);
    ...
};
//对于任何类型T和任何类型U,可以根据SmartPtr构造出SmartPtr
//这个做法又叫泛化构造函数
  • 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果你声明 member templates 用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

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

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

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

如何使用一个traits class了:

  • 建立一组重载函数(身份像劳工)或函数模板(例如 doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。■
  • 建立一个控制函数(身份像工头)或函数模板(例如 advance),它调用上述那些“劳工函数”并传递traits class所提供的信息。

请记住

  • Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现。
  • 整合重载技术(overloading)后,traits classes 有可能在编译期对类型执行if…else测试。

条款48: 认识template元编程

Template metaprogramming(TMP,模板元编程)是编写template-basedC++程序并执行于编译期的过程。所谓templatemetaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译。

可以生成客户定制之设计模式(custom designpattern)实现品。设计模式如Strategy(见条款35),Observer,Visitor等等都可以多种方式实现出来。运用所谓policy-based design之TMP-based技术,有可能产生一些templates用来表述独立的设计选项(所谓 “policies”),然后可以任意结合它们,导致模式实现品带着客户定制的行为。

  • Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

考虑以下这个递归

#include 
template <int n>
struct Factorial{
    enum {value = n*Factorial<n-1>::value};
};

template <>
struct Factorial<0>{
    enum {value = 1};
};

int main(int argc, char const *argv[])
{
    std::cout<<Factorial<10>::value;//3628800
    return 0;
}

Chapter 8.定制new和delete

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

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

    可以用类似于RAII的方式,为一些类设置类专属的new-handling。用这样的方式是为了避免忘记把new-handler改回来。(set_new_handler的修改是全局性质的)这个书上写了好几页,但感觉太细节了,不在本笔记里体现。

    new-handler一般做一下几个事情

    • 让更多内存可以使用
    • 在自己无法获取更过内存的情况下,设置下一个new-handler
    • 卸除new-handler,即把null传给set_new_handler,operator new分配不成功就抛出异常
    • 抛出bad_alloc
    • 不返回,结束程序。abort()或者exit()
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常

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

  • 用来检测运用上的错误。如果我们自行定义一个 operator news,便可超额分配内存,以额外空间(位于客户所得区块之前或后)放置特定的 byte patterns(即签名,signatures)。operator deletes便得以检查上述签名是否原封不动,若否就表示在分配区的某个生命时间点发生了overrun或underrun,这时候operator delete可以志记(log)那个事实以及那个惹是生非的指针。
  • 为了强化效能。对某些(虽然不是所有)应用程序而言,将旧有的(编译器自带的) new和delete替换为定制版本,是获得重大效能提升的办法之一。
  • 为了收集使用上的统计数据。

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

  • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
  • operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。

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

new完成三个工作

  1. 调用operator new分配空间
  2. 若1成功,则调用构造函数初始化空间。
  3. 返回对应类型的指针

注意到,若2出问题,那么new就应该负责把第一部里面开辟的空间归还。否则就会有内存泄漏。

如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new。众多 placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”。如果一个带额外参数的 operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何 operator delete 会被调用。

这意味如果要对所有与 placement new相关的内存泄漏宣战,我们必须同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常被抛出)。后者的额外参数必须和operatornew一样。

  • 当你写一个 placement operator new,请确定也写出了对应的 placementoperator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
  • 当你声明placement new和placementdelete,请确定不要无意识(非故意)地遮掩了它们的正常版本。

Chapter 9.杂项讨论

条款53: 不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
  • 不要过度倚赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本倚赖的警告信息有可能消失。

条款54: 让自己熟悉包括TRI在内的标准程序库

时间原因,TR1已经在现在的c++11里面实现,包括但不限于shared_ptr等等

条款55: 让自己熟悉Boost

Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。

你可能感兴趣的:(c++)