Effective C++ 笔记

一、让自己习惯C++

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

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

用编译器替换预处理器

什么是预处理

在C++中,预处理是指在编译过程之前的一个阶段,通过预处理器(preprocessor)对源代码进行处理的过程。预处理器会根据预处理指令(以井号#开头)对源代码进行文本替换、宏展开等操作,生成一个经过预处理的源文件。

以下是常用的预处理指令及其作用:

  1. #define:用于定义宏常量或宏函数。#define指令会将所有出现在源代码中的宏名称都替换为指定的文本。

  2. #include:用于包含头文件内容。#include指令会将指定的头文件内容插入到该指令所在位置。

  3. 等等

a、普通常量

宏 预处理指令定义常量,如:

cpp 复制代码
# define ASPECCT_RATIO 1.653

尽量用以下来代替:

const 常量

cpp 复制代码
const double AspectRaio = 1.653;

另外 定义一个常量的 char*-based字符创,必须写const两次

cpp 复制代码
const char* const authorName = "Acott Meyers"

其实string对象通常比char*-based合宜

cpp 复制代码
const std::string authorName("Scott Meyers")

b、class专属常量

解释:

1.常量的作用域限制在class内,必须让他成为 class 的一个成员

2.确保此常量至多只有一份实体,必须让它成为一个 static 成员:

cpp 复制代码
class Game{
private:
    static const int NumTurns= 5;
    int scores[NumTurns];
};

常量变量默认是不允许在类中直接初始化的。但是静态成员变量可以在类内进行初始化,所以是 static const int

也可以使用enum来实现

cpp 复制代码
class Game{
private:
    enum {NumTurns = 5};
    int scores[NumTurns];
};

c、看起来像函数的宏

cpp 复制代码
//以 a 和 b 的较大值调用f
#define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : (b)

使用template inline 函数在编译阶段处理函数调用点的,减少函数调用的开销,也能获得宏的的效率

cpp 复制代码
template<typename T>
inline T callWithMax(const T& a, const T& b){
    return (a,b) ? a: b;
}

条款03、尽可能使用const

const与指针

如果关键字 const 出现在星号左边,表示被指物是常量,作函数参数时,不能在函数中改变

cpp 复制代码
#include <iostream>


void add(const int* a, const int* b){
    *a+=1; // 错误!
    int c = *a + *b;
    std::cout << c << '\n';

}

int main(){

    int num =1;
    
    
    const int* a = &num;
    const int* b = &num;

    add(a, b);
}

如果关键字 const 出现在星号右边,表示指针自身是常量

cpp 复制代码
#include <iostream>
int main(){

    // 被指物为常量
    char* const  p  = "hello";

//    p = "jason"; // 此时指针p不可以被修改,

    std::cout << *p << std::endl;
}

如果关键字 const 出现在星号两边,表示被指物和指针两者都是常量

cpp 复制代码
#include <iostream>
int main(){

    char words[] = "hello";
   
     const char* const  p  = words;

    std::cout << *p << std::endl;
}

const与迭代器

cpp 复制代码
#include <iostream>
#include <vector>
int main(){
    std::vector<int> vec{1,2,3,4,5};

    std::vector<int>::const_iterator iter =
        vec.begin(); // const 出现在迭代器右边
    *iter += 10; // 不能通过迭代器修改被指内容
//    ++iter;

}

const与函数

const成员函数

cpp 复制代码
#include <iostream>

class TextBlock{
private:
    std::string text;
public:
    TextBlock(const std::string & str):text(str){}
    
    const char& operator[] (std::size_t position) const{
        return text[position];
    }

    char& operator [] (std::size_t position){
        return text[position];
    }
};


int main(){
    TextBlock tb("Hello");
    std::cout << tb[0];  // 调用 non-const TextBlock::operator[]

    const TextBlock ctb("world"); // 调用 const TextBlock::operator[]
    std::cout << ctb[0];
}

这里例子是太过造作。

真实程序中 const 对象大多用于 passes by pointer-to- const 或passes by reference-to-const

应用在函数参数中

cpp 复制代码
void print(const TextBlock& ctb){
    std::cout << ctb[0];
}

应用在成员函数

cpp 复制代码
include <iostream>

class Myclass{
private:

    bool lengthIsValid;
    mutable int num;
public:
    void test() const{
        lengthIsValid = 0; // 错误!在const成员函数内不能赋值给lengthIsValid
        num=10; // 正确,mutable关键字修饰的变量可以被修改
    }
};


int main(){
    Myclass myclass;
}

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

为避免在对象初始化之前过早地使用它们,需要做三件事:

  1. 为内置型对象进行手工初始化,因为C++不保证初始化
  2. 构造函数最好使用成员初值列,而不要在构造函数体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。另外,C++17支持在类中直接对私有变量进行初始化而不通过构造函数
  3. 为免除"跨编译单元之初始化次序问题",用 local static 对象替换 non-local static 对象

前两点很好理解,这里对第三点给出例子解释:

cpp 复制代码
class FileSystem{
public:
    std::size_t numDisks(){}
};

FileSystem& tfs(){
    static FileSystem fs;
    return fs;
}

class Directory{};
Directory::Directory(){
    std::size_t disks = tfs.numDisks();
}

Directory& tempDir(){
    static Directory td;
    return td;
}

二、构造/析构/赋值运算

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

如果你声明的一个空类,如:

cpp 复制代码
class Empty{};

那么编译器会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。如果你没有声明任何构造函数,编译器会你声明一个default构造函数。所有这些函数都是public且inline的。所以上面你声明的空类会被编译器处理为:

cpp 复制代码
class Empty{
public:
    Empty(){}       // default构造函数
    Empty(const Empty& rhs){} // copy构造函数
    ~Empty(){} // 析构函数
    
    Empty& operator=(const Empty& rhs){} // copy assignment操作符
};

调用copy构造函数的例子

cpp 复制代码
#include <iostream>

template<typename T>
class NameObject{
public:
    NameObject(const char* name, const T& value);
    NameObject(const std::string& name, const T& value);

private:
    std::string nameValue;
    T objecctValue;
};

int main(){
    NameObject<int> no1("Smallest Primer Number", 2);
    NameObject<int> no2(no1); // 调用copy构造函数

}

一个错误的例子:

C++不允许"让 reference 改值向不同对象"

更改const成员是不合法的

基于以上两点,如下例子不正确:

cpp 复制代码
#include <iostream>

template<typename T>
class NameObject{
public:

    NameObject(const std::string& name, const T& value);

private:
    std::string& nameValue; // C++不允许"让 reference 改值向不同对象"
    const T objecctValue; //更改const成员是不合法的
};

int main(){
    std::string newDog("Persephone");
    std::string oldDog("Satch");
    NameObject<int> p(newDog, 2);

    NameObject<int> s(oldDog, 36);

    p = s;// 错误;C++不允许 让reference

}

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

如果不希望 copy 构造函数 以及 copy assignment 操作符起作用,则应该声明为 private 并没有定义

cpp 复制代码
#include <iostream>

class HomeForSale{
public:

private:
    HomeForSale(const HomeForSale&);
    HomeForSale& operator =(const HomeForSale&);
};

int main(){
    HomeForSale home;
    home h1;
    home h2(h1);
}

如此一来,就可以阻止 编译器暗自创建他们。而拷贝HomeForSale对象的时候,编译器会阻止:

将连接期报错移动到编译期

cpp 复制代码
class Uncopyable{
protected:
    Uncopyable(){}                   // 允许对象构造和析构
    ~Uncopyable(){}
private:
    Uncopyable(const Uncopyable&); // 但阻止copying
    Uncopyable& operator =(const Uncopyable&);
};

// class HomeForSale 不再声明 copy 构造函数 或 copy assign. 操作符
class HomeForSale : private Uncopyable{};

int main(){
    HomeForSale home1;
    HomeForSale home2(home1);

}

总结:

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

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

C++11不存在 " non-virtual 析构函数问题",此条款略过

条款08、别让异常逃出析构函数

不要让异常逃离析构函数是指在析构函数中抛出异常时,不要让该异常传播到析构函数的调用点以外。这是因为对象的析构过程通常是在对象生命周期的最后阶段发生的,此时其他对象和资源已经被清理或释放。如果析构函数抛出异常并且该异常逃离了析构函数,将会导致以下问题:

  1. 对象的析构不完全:如果析构函数抛出异常,对象的析构可能无法完成,导致对象状态没有得到完全清理或释放。这可能会导致资源泄漏或其他未定义行为。

  2. 内存泄漏:如果在抛出异常之前执行了动态内存分配,并且在抛出异常后没有适当地释放该内存块,将会导致内存泄漏。

  3. 无法回滚操作:如果在对象构造期间进行了一些操作,并且在析构函数中发生异常,无法回滚这些操作。这可能会导致数据不一致或逻辑错误。

为了避免异常从析构函数中逃离,可以采取以下措施:

  1. 在析构函数中捕获异常并处理:可以在析构函数中使用 try-catch 块来捕获异常,并在适当的情况下进行处理。例如,记录异常信息或执行必要的清理操作。

析构函数抛出异常就结束程序。通常通过调用 abort 完成:

cpp 复制代码
#include <iostream>

class DBConnection{
public:
    static DBConnection create(){
        static DBConnection db;
        return db;
    }

    void close(){
        int num=0;
        throw num; // 假装抛出异常
    }
};

//该类用于管理 DBConnection 资源,在其析构函数调用 DBConnection 的close,用以关闭数据库
class DBConn
{
private:
    DBConnection db;
public:
    ~DBConn() {
        try{db.close();}
        // 省略代码:制作运转记录,记下对close的调用失败
        catch (int)
        {
            // 这里最好打印信息,因为QT IDE只会给你报崩溃的信息
            std::abort();
        }
    }

    DBConn(DBConnection d):db(d){}
};

int main()
{
    // 现在就可以这样写代码。
    // dbc 对象销毁时候,会调用其析构函数,....自动关闭 DBConnection 的数据库
    DBConn dbc(DBConnection::create());
}

本例的假设情况:

如果程序遭遇一个"于析构函数期间发生的错误"后无法继续执行,"强迫结束程序"是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用 abort 可以抢险置"不明确行为"于死地。

吞下因调用而发生的异常:

修改上例代码中的DBConn类的析构函数如下:

cpp 复制代码
    ~DBConn() {
        try{db.close();}
        // 省略代码:制作运转记录,记下对close的调用失败
        catch (int){}
    }

这里析构函数将异常吞掉了,一般而言,这是和坏主意,因为压制了"某些动作失败"的重要信息!然而有时候吞下异常也比负担"草率结束程序"或"不明确行为带来的风险"好。为了让这成为一个可行方案,程序必须能够继续可靠地执行,及时在遭遇并忽略一个错误之后。

上面两种方法没有无法对"导致 close 抛出异常"的情况做出反应,另外一个思路是重新设计 DBonn接口,使其客户有机会对可能和出现的问题作出反应。例如 DBConn 自己可以提供一个close函数,给客户一个机会得以处理"因该操作而发生的异常":

cpp 复制代码
#include <iostream>

class DBConnection{
public:
    static DBConnection create(){
        static DBConnection db;
        return db;
    }

    void close(){
        int num=0;
        throw num; // 假装抛出异常
    }
};

//该类用于管理 DBConnection 资源,在其析构函数调用 DBConnection 的close,用以关闭数据库
class DBConn
{
private:
    DBConnection db;
    bool closed=false;
public:

    void close(){      // 供客户使用的新函数
        db.close();
        closed = true;
    }

    ~DBConn() {

        if(!closed){              // 关闭连接(如果客户不那么做的话)
            
            try{db.close();}
            catch(int) {
                // 如果关闭动作失败,
                // 记录下来并结束程序或吞掉异常
            };
        }
    }

    DBConn(DBConnection d):db(d){}
};

int main()
{
    // 现在就可以这样写代码。
    // dbc 对象销毁时候,会调用其析构函数,....自动关闭 DBConnection 的数据库
    DBConn dbc(DBConnection::create());
    
    
}

总结:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行操作

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

如标题

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

cpp 复制代码
#include <iostream>

class Widget{
public:
    Widget& operator =(const Widget& rhs){

        // 将右侧对象的值赋给左侧对象的成员变量
        if (this != &rhs){
            value = rhs.value;
        }

        // 返回左侧对象
        return* this;
    }

    Widget(int num):value(num){}
    Widget(){};

    void getValue(){
        std::cout <<  "value: " << value << std::endl;
    }

private:
    int value;
};

int main(int argc, char *argv[])
{
    Widget m1(10);

    Widget m2;
    Widget m3;
    
    // 连锁赋值
    m3=m2=m1;

    m3.getValue();
    m2.getValue();
    m1.getValue();

    return 0;
}

本例子以operator=操作符为例, +=, -=, *=也适用

条款11、在 operator= 中处理 "自我赋值"

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

三、资源管理

条款13、以对象管理资源

中心:资源取得时机便是初始化时机

一般情况下:

cpp 复制代码
#include <iostream>

class Investment{};

Investment * creatInvestment(){
    Investment* i = new Investment;
    return i;
}

void f()
{
    Investment* pInv = creatInvestment();

    // 中间代码省略

    delete pInv; // 必须释放pInv所指向对象,否则导致内存泄露
}

int main(){
    f();
}

但是有时候由于中间代码出现问题,delete pInv 语句会无法得到执行,这样便导致内存泄露!

下面这个例子示范"以对象管理资源"的两个关键想法:

获得资源后立即放进管理对象内

creatInvestment() 返回的资源被当作其管理 unique_ptr的初值

管理对象运行析构函数确保资源被释放

cpp 复制代码
#include <iostream>
#include <memory>

class Investment{};

Investment * creatInvestment(){
    Investment* i = new Investment;
    return i;
}

void f()
{
    // 获得资源后立即放进管理对象内
    std::unique_ptr<Investment> pInv(creatInvestment());

    // 中间代码省略

//    delete pInv; // 管理对象运行析构函数确保资源被释放,不需要手动释放内存
}

int main(){
    f();
}

不过,unique_ptr 是 C++11 引入的智能指针模板,用于管理动态分配的内存资源。它提供了一个独占所有权的智能指针,确保只有一个指针可以访问动态分配的内存块,从而避免内存泄漏和悬挂指针(dangling pointer)的风险。这样做就不行:

cpp 复制代码
#include <iostream>
#include <memory>

class Investment{};

Investment * creatInvestment(){
    Investment* i = new Investment;
    return i;
}

void f()
{
    // 获得资源后立即放进管理对象内
    Investment* ptr = creatInvestment();
    
    //不能把同一块内存由两个unique_ptr管理者同时管理
    std::unique_ptr<Investment> pInv1(ptr);
    std::unique_ptr<Investment> pInv2(pInv1); // 不允许

    // 中间代码省略

//    delete pInv; // 管理对象运行析构函数确保资源被释放,不需要手动释放内存
}

int main(){
    f();
}

而STL容器要求其元素发挥 "正常的" 复制行为,因此容不得unique_ptr。

但是shared_ptr就不一样了:

cpp 复制代码
#include <iostream>
#include <memory>

class Investment{};

Investment * creatInvestment(){
    Investment* i = new Investment;
    return i;
}

void f()
{
    // 获得资源后立即放进管理对象内
    Investment* ptr = creatInvestment();

    //同一块内存可以由两个shared_ptr管理者同时管理
    std::shared_ptr<Investment> pInv1(ptr);
    std::shared_ptr<Investment> pInv2(pInv1);

    pInv2 = pInv1; // 同上,无任何变化

    // 中间代码省略

//    delete pInv; // 管理对象运行析构函数确保资源被释放,不需要手动释放内存
}

int main(){
    f();
}

条款14、在资源管理类中小心 copying 行为

对于 heap-based 资源,可以用unique_ptr以及shared_ptr进行管理。但是别的资源就需要建立自己的资源管理类

下述例子实现了对 mutex* 的管理,并禁止复制相当于unique_str:

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

void lock(mutex* pm){};
void unlock(mutex* pm){};

class Lock{
private:
    mutex* mutexPtr;

public:
    explicit Lock(mutex* pm):mutexPtr(pm)
        {
        lock(mutexPtr); // 构造函数获取资源
        }

    ~Lock()
    {
        unlock(mutexPtr); // 析构函数释放资源
    }

private:
    // 不希望 copy 构造函数 以及 copy assignment 操作符起作用
    Lock(const Lock&);
    Lock& operator =(const Lock&);
};



int main(){
    mutex m;
    Lock m1(&m);
    Lock m2(m1); // 禁止复制,条款6
}

使用shared_ptr

cpp 复制代码
#include <iostream>
#include <mutex>
#include <memory>

using namespace std;

void lock(mutex* pm){};
void unlock(mutex* pm){};

class Lock{
private:
    std::shared_ptr<mutex> mutexPtr;

public:
    // 以某个 mutex 初始化 shared_ptr,
    // 第二参数是unlock
    // 当shared_ptr 被销毁时会调用unlock
    explicit Lock(mutex* pm):mutexPtr(pm, unlock)
        {
        lock(mutexPtr.get()); // 构造函数获取资源
        }

    // 不再声明析构函数
    // 条款5-》class析构函数会自动调用其non-static成员变量的析构函数

};



int main(){
    mutex m;
    Lock m1(&m);
    Lock m2(m1);
}

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

来看一个例子,就理解标题的意思:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

class Investment{};

Investment * creatInvestment(){
    Investment* i = new Investment;
    return i;
}


int daysHeld(const Investment* pi){
    return 10;
}

int main(int argc, char *argv[])
{
    std::shared_ptr<Investment> pInv(creatInvestment());

    // 错误,函数需要的是Investment*指针!
    // 传过来的却是类型为 std::shared_ptr<Investment>对象
    int days = daysHeld(pInv);

    return 0;
}

那么如何获得原始指针:

方法一:使用shared_ptr的get成员函数,用来执行显式转换,也就是他会返回智能指针内部的原始指针(的复件):

cpp 复制代码
int main(int argc, char *argv[])
{
    std::shared_ptr<Investment> pInv(creatInvestment());

    //使用shared_ptr的get成员函数,获取智能指针内部的原始指针
    int days = daysHeld(pInv.get());

    return 0;
}

方法二:就像(几乎)所有智能指针一样,shared_ptr也重载了指针取值(pointer deference)操作符(operator->和operator*),他们允许隐式转换至底部原始指针:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

class Investment{
    //
public:
    bool isTaxFree() const{
        return true;
    }
};

Investment * creatInvestment(){
    Investment* i = new Investment;
    return i;
}



int main(int argc, char *argv[])
{
    // shared_ptr管理一笔资源
    std::shared_ptr<Investment> pi1(creatInvestment());

    // 经由 operator->f访问资源
    bool taxable1 = !(pi1->isTaxFree());


    // unique_ptr 管理一批资源
    std::unique_ptr<Investment> pi2(creatInvestment());

    // 经由 operator*访问资源
    bool taxable2 = !((*pi2).isTaxFree());

    return 0;
}

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

必须一一对应:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

int main(int argc, char *argv[])
{
    std::string* stringPtr1 = new std::string;
    std::string* stringPtr2 = new std::string[100];

    delete stringPtr1; // 删除一个对象
    delete [ ] stringPtr2; // 删除一个由对象组成的数组

    return 0;
}

如果你喜欢用typedef:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

int main(int argc, char *argv[])
{
    typedef std::string AddressLines[4]; // 每个人地址有4行
                                         // 每行是一个string

    // 注意,"new AddressLines" 返回一个string*,就像
    // "new string[4]"一样
    std::string* pa1 = new AddressLines;

    // 那必须匹配"数组形式"的delete
//    delete pa1;//行为未有定义
    delete [] pa1;

    return 0;
}

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

从int prioryty(){}阅读下述例子,即可理解标题:

cpp 复制代码
#include <iostream>
#include <memory>

class Widget{
public:
    Widget& operator =(const Widget& rhs){

        // 将右侧对象的值赋给左侧对象的成员变量
        if (this != &rhs){
            value = rhs.value;
        }

        // 返回左侧对象
        return* this;
    }

    Widget(int num):value(num){}
    Widget(){};

    void getValue(){
        std::cout <<  "value: " << value << std::endl;
    }

private:
    int value;
};

int priority(){
    return 10;
}

void processWidget(std::shared_ptr<Widget> pw, int priority){}

int main(int argc, char *argv[])
{
    // shared_ptr构造函数需要一个原始指针(raw pointer)
    // 但该构造函数是个explicit构造函数,无法进行隐式转换,
    // 将自"new Widget"的原始指针转换为processWidget所要求的
    // shared_ptr,所以下面这样写不能通过编译
//    processWidget(new Widget, priority());

    // 这样写才能通过编译,干了三件事:如果是这个顺序
    // 执行 "new Widget"、调用priority、调用new Widget构造函数
    // 调用priority失败会"new Widget"返回的指针遗失,引发资源泄露
    processWidget(std::shared_ptr<Widget> (new Widget), priority());

    // 所以最好是使用分离语句
    std::shared_ptr<Widget> pw(new Widget); // 在单独语句内以智能指针
                                            //存储new Widget所得对象
    processWidget(pw,priority()); // 这个调用动作绝不至于造成泄露

    return 0;
}

分离语句即可说明:

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

四、设计与声明

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

很可能传一个错误的参数:

cpp 复制代码
#include <iostream>

class Date{
public:
    Date(int month, int day, int year){};
};

int main(int argc, char *argv[])
{
    Date d(30, 3, 1995); // 月份不可能大于12!!
    return 0;
}
  • std::shared_ptr 在构造函数中提供了一个可选的删除器(deleter)参数,用于自定义资源的释放方式。删除器是一个函数或者可调用对象,用于在shared_ptr的引用计数器变为0时释放资源

    删除器可以是普通函数、函数指针、lambda 表达式 等,只要符合指定的删除器的函数签名即可。删除器接受一个指向资源的指针(被shared_ptr管理的指针)作为参数,不返回任何值。

    下面是一个示例,展示了如何给 shared_ptr 提供自定义的删除器:

    cpp 复制代码
    #include <iostream>
    #include <memory>
    
    struct MyStruct {
        void operator()(int* p) {
            std::cout << "Deleting pointer using custom deleter...\n";
            delete p;
        }
    };
    
    int main() {
        std::shared_ptr<int> ptr(new int(42), MyStruct());
    
        // 使用默认删除器释放资源
        std::shared_ptr<int> ptr2(new int(100));
    
        return 0;
    }

    在上面的示例中,MyStruct 是一个删除器类,它重载了operator(),在函数体中使用 delete 运算符释放资源。在创建 std::shared_ptr 时,通过提供 MyStruct 的实例作为构造函数的第二个参数,我们指定了自定义删除器。

    需要注意的是,使用删除器时,它必须与被管理指针的类型兼容,否则会导致未定义的行为。此外,当存在多个 shared_ptr 共享同一个对象时,它们必须使用相同类型的删除器。

    自定义删除器为我们提供了更大的灵活性,能够管理不同类型的资源释放方式,例如,在资源释放时执行一些额外的操作。

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

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

首先来看下 pass-by-reference-to-const 的传递成本:

cpp 复制代码
#include <iostream>

class Person{
public:
    Person(){}
    virtual ~Person(){}

private:
    std::string name;
    std::string address;

};

class Student: public Person{
private:
    std::string schoolName;
    std::string schoolAddress;

public:
    Student(){}
    ~Student(){}
};

bool validateStudent(Student s){
    // 省略对Student对象的检查

    return 1;
}

int main(int argc, char *argv[])
{
    Student plato;
    bool platoIsOK = validateStudent(plato);
    return 0;
}

这里例子中,无疑地Student的 copy 函数会被调用,以 plato 为蓝本将 s 初始化。同样明显地,当 validayeStudent 返回后, s 会被销毁。因此,对此函数而言,参数的传递成本是 "一次 Student copy 构造函数调用,加上一次 Student 析构函数调用"。

而使用 pass by reference-to-const 可以回避上述那些构造和析构动作:

cpp 复制代码
bool validateStudent(const Student& s)

const的作用:validateStudent 不能修改传入的Student

除了效率之外,两种传递方式还有别的区别,来看看这里例子:

cpp 复制代码
#include <iostream>

class Window{
public:
    std::string name() const{
        return "window\n";
    }
    virtual void display() const{
        std::cout << "display from Window!\n";
    }
};

class WindowWithScrollBars: public Window{
public:
    // 重写了基类中的display方法
    virtual void display() const{
        std::cout << "display from WindowWithScrollBars!\n";
    }
};

void printNameAndDisplay(Window w){
    std::cout << w.name();
    w.display();
}


int main(int argc, char *argv[])
{
    WindowWithScrollBars wwsb;
    printNameAndDisplay(wwsb);
    return 0;
}

printNameAndDisplay函数的本意是,调用传入对象的display成员函数。也就是说,如果传入的是Window对象,就调用其display成员函数;如果传入的是WindowWithScrollBars对象,就其display成员函数。

但是参数传递方式选择为 pass-by-value时候,即使传入的是WindowWithScrollBars对象,也是调用基类Window的display成员函数。下图是上例子的执行结果

而将printNameAndDisplay函数参数传递方式修改为pass-by-reference-to-const时,就能实现该函数设计的本意:

cpp 复制代码
void printNameAndDisplay(const Window& w){
    std::cout << w.name();
    w.display();
}

但是对于内置类型,以及 STL 的迭代器和函数对象而言,比如 int,尽量选择 pass-by-value

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

返回对象时,返回其 reference,可行?

cpp 复制代码
#include <iostream>

class Rational{
public:
    Rational(int numerator = 0, int denominator = 1)
        :n(numerator),d(denominator){}
private:
    int n, d;
    friend const Rational&
    operator*(const Rational& lhs, const Rational& rhs);
};

const Rational& operator*(const Rational& lhs,
                          const Rational& rhs){
    // local 变量,是在 stack 空间创建的
    // 是不能作为返回值的
    Rational result(lhs.n * rhs.n, lhs.d *rhs.d);
    
    
    return result;

}

int main(int argc, char *argv[])
{
    Rational a(1,2);
    Rational b(3,5);
    Rational c = a * b;
    return 0;
}

考虑在 heap 内构造一个对象,看能返回 referene,还是行不通:

cpp 复制代码
const Rational& operator*(const Rational& lhs,
                          const Rational& rhs){
    
    // 考虑在 heap 内构造一个对象
    Rational* result= new Rational(lhs.n * rhs.n, lhs.d *rhs.d);
    return result;

}

考虑使用 static 关键字:

cpp 复制代码
const Rational& operator*(const Rational& lhs,
                          const Rational& rhs){

    // 定义一个在函数内部的 static Rational 对象
    static Rational result = Rational(lhs.n * rhs.n, lhs.d *rhs.d);
    return result;

}

这次代码可用通过编译

但是在书中这种写法也会在延伸点上出现问题。所以一个"必须返回新对象"的函数的正确写法是:就让那个函数返回一个新对象:

cpp 复制代码
const Rational operator*(const Rational& lhs,
                          const Rational& rhs){

    // local 对象可以直接被返回,而不是返回其reference
    Rational result = Rational(lhs.n * rhs.n, lhs.d *rhs.d);
    return result;

}

总结:

绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated对象,或返回 pointer 或 reference 指向一个local static 对象而有多个可能同时需要多和这样的对象。条款4已经为 "在单线程环境中合理返回 reference 指向一个 local static 对象"提供一份设计实例。

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

一旦将一个成员变量声明为 public 或 protected 而客户开始使用它们,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。从封装的角度观之,其实只有两种访问权限:priavate(提供封装)和其他(不提供封装)。

请记住:

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

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

cpp 复制代码
#include <iostream>

class WebBrowser{
public:
    void clearCache();
    void clearHistory();
    void removeCookies();
    
    // 如果想一整个执行所有这些动作
    // 方式一:
    // 在类中提供这样一个函数,调用上面三个函数
    void clearEveryThing();
};

// 方式二: 
// 也可以用一个non-member函数实现
void clearBrowser(WebBrowser& wb){
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

1、member 函数 clearEveryThing 带来的封装性比 non-member 函数 clearBrowser 低:

  • 对象内的数据,愈多函数可访问,数据的封装性就愈低

2、降低编译依赖性

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

先来看看何为隐式转换:

cpp 复制代码
#include <iostream>

class Rational {
public:
    // 构造函数刻意不为 explicit,
    // 允许 int-to-Rational 隐式转换
    Rational (int numerator = 0,
             int denominator = 1)
        :n(numerator),d(denominator){}

    // 分子(numerator)和 分母(denominator)的的访问函数
    int numerator() const;
    int denominator() const;

private:
    int n,d;
};

int main(int argc, char *argv[])
{
    // 这便是隐式转换:
    // 【 将会隐式地将整数5转换为Rational类型,并通过调用构造函数
    // Rational(int numerator = 0, int denominator = 1)来
    // 创建Rational对象r。在这种情况下,numerator将被初始化为5,
    // denominator将被初始化为1】
    Rational r =5;

    return 0;
}

再来看看operator* 的使用会涉及到什么问题:

cpp 复制代码
#include <iostream>

class Rational {
public:
    // 构造函数刻意不为 explicit,
    // 允许 int-to-Rational 隐式转换
    Rational (int numerator = 0,
             int denominator = 1)
        :n(numerator),d(denominator){}

    // 分子(numerator)和 分母(denominator)的的访问函数
    int numerator() const;
    int denominator() const;

    const Rational operator* (const Rational& rhs) const{}

private:
    int n,d;
};




int main(int argc, char *argv[])
{
    Rational oneEighth(1,8);
    Rational oneHalf(1,2);
    
    Rational result = oneHalf * oneEighth;
    result = result * oneEighth;
    
    
    // 能运行
    result = oneHalf * 2;
    
    // 错误!
    result = 2 * oneHalf;

    return 0;
}

为啥 result = oneHalf * 2 可以运行, 而 result = 2 * oneHalf 却会报错:

1、result = oneHalf * 2 发生了所谓隐式转换:

oneHalf 是一个内含 operator* 函数的 class 的对象,所以编译器调用该函数。然后编译器知道你正在传递一个 int,而函数需要的是 Rational; 但它也知道只要调用 Rational 构造函数并赋予你所提供的 int,就可以变出一个适当的 Rational 来。于是它就这样做了。换句话说此一调用动作在编译器眼中有点像这样:

cpp 复制代码
const Ration temp(2); // 根据2建立一个暂时的 Rational 对象
result = oneHalf * temp; // 等同于 oneHalf.operator*(temp)

当然,只因为涉及到 non-explicit 构造函数,编译器才会这样做。如果 Rational 构造函数是 explicit ,以下语句没有一个能通过编译:

cpp 复制代码
    result = oneHalf * 2;
    

    result = 2 * oneHalf;

2、result = 2 * oneHalf,整数2并没有相应的class,也就没有 operator* 成员函数。编译器也会尝试寻找可被以下这般调用的 non-member operator* (也就是在命名空间或在 global 作用域内):

cpp 复制代码
result = operator*(2, oneHalf); // 错误

但本例并不存在这样一个接受 int 和 Rational 作为参数的 non-member operator*,因此查找失败

那怎么可以支持混合式算术运算。可行之道终于拨云见日:让 operator* 成为一个 non-member 函数, 便允许编辑器在每个实参身上执行隐式类型转换:

cpp 复制代码
#include <iostream>

class Rational {
public:
    // 构造函数刻意不为 explicit,
    // 允许 int-to-Rational 隐式转换
    Rational (int numerator = 0,
             int denominator = 1)
        :n(numerator),d(denominator){}

    // 分子(numerator)和 分母(denominator)的的访问函数
    int numerator() const;
    int denominator() const;

private:
    int n,d;
};

// 注意:参数是两个Rational对象的引用
const Rational operator* (const Rational& lhs,
                         const Rational& rhs) {}


int main(int argc, char *argv[])
{
    Rational oneEighth(1,8);
    Rational oneHalf(1,2);

    Rational result = oneHalf * oneEighth;
    result = result * oneEighth;


    // 能运行
    result = oneHalf * 2;

    // 万岁,通过编译了!
    result = 2 * oneHalf;

    return 0;
}

最后一个问题:operator* 是否应该成为 Rational class 的一个 friend 函数呢?

就本例而言答案是否定的,因为 operator* 可以完全由 Rational 的 public 接口完成任务。这导出一个重要的观察:member 函数的反面是 non-member 函数,不是 friend 函数。

无论任何时候可以避免 friend 函数就该避免。

请记住:

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

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

先来看看std::swap的典型实现:置换 a 和 b 的值

cpp 复制代码
namespace  std {
    template<typename T>
    void swap(T& a, T& b)
    {
        T temp(a); // a 复制到temp
        a = b; // b 复制到 a
        b = temp; // temp 复制到 b
    }
}

只要类型 T 支持 copying (通过 copy 构造函数和 copy assignment 操作符完成),缺省的 swap 实现代码就会帮你置换类型为 T 的对象。

尝试将 std::swap 针对 Widget 特化:

cpp 复制代码
#include <iostream>
#include <vector>

class WidgetImp1{
public:
private:
    int a,b,c;
    std::vector<double> v;
};

class Widget{
private:
    WidgetImp1* pImp1; // 一旦要置换
public:
    Widget(){}
    Widget (const Widget& rhs){}
    Widget& operator =(const Widget& rhs){
        *pImp1 = *(rhs.pImp1);
    }
};


// 一旦要置换两个 Widget 对象值,需要做的就是置换其pImp1指针。
// 但缺省的 swap 算法 效率很低:
// 不止复制三个 Widget, 还复制三个 WidgetWidgetImp1对象
//

// 所以要告诉 std::swap:
// 当 Widgets 被置换时真正该做的是置换其内部的pImp1指针
// 所以,将 std::swap 针对 Widget 特化
namespace  std {
    template<>
    void swap(Widget& a, Widget& b)
    {
    swap(a.pImp1, b.pImp1);
    }
}

 using namespace std;

int main(int argc, char *argv[])
{
    Widget a;
    Widget b;

    std::swap(a,b);
    return 0;
}

但是发现pImp1指针是私有变量,在类外访问不了

所以要在类中定义一个swap成员函数,并在swap特化函数中 使用该函数:

cpp 复制代码
#include <iostream>
#include <vector>

class WidgetImp1{
public:
private:
    int a,b,c;
    std::vector<double> v;
};

class Widget{
private:
    WidgetImp1* pImp1; // 一旦要置换
public:
    Widget(){}
    Widget (const Widget& rhs){}
    Widget& operator =(const Widget& rhs){
        *pImp1 = *(rhs.pImp1);
    }

    // 给swap 特化 所调用
    void swap(Widget& other){
        using std::swap;
        swap(pImp1, other.pImp1);// 置换pImp1指针
    }
};


// 一旦要置换两个 Widget 对象值,需要做的就是置换其pImp1指针。
// 但缺省的 swap 算法 效率很低:
// 不止复制三个 Widget, 还复制三个 WidgetWidgetImp1对象
//

// 所以要告诉 std::swap:
// 当 Widgets 被置换时真正该做的是置换其内部的pImp1指针
// 所以,将 std::swap 针对 Widget 特化
namespace  std {
    template<>
    void swap(Widget& a, Widget& b)
    {
        a.swap(b);
    }
}

 using namespace std;

int main(int argc, char *argv[])
{
    Widget a;
    Widget b;

    std::swap(a,b);
    return 0;
}

上面都是 class ,而非 class template。关于 class template 的 swap 函数暂时不学习。

请记住:

  • 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个 member swap,也该提供一个non-member swap 用来调用前者。对于classes (而非 templates),也清特化 std::swap.
  • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何 "命名空间资格修饰符"
  • 为"用户定义类型"进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西

五、实现

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

过早定义变量的例子:

cpp 复制代码
#include <iostream>
const int MininmumPasswordLength = 10;

std::string  encryptPassword(const std::string& password){
    using namespace std;

    // 过早定义变量encrypted
    // 如果下面抛出异常,该变量没用使用到
    // 但还是要承受该变量的构造和析构成本
    string encrypted;

    if (password.length() < MininmumPasswordLength){
        throw logic_error("Password is too short");
    }

    // 必要动作,便能将一个加密后的密码
    // 置入变量 encrypted 内

    return encrypted;
}

最好延后 encrypted 的出现,直到真正需要它。

但循环怎么办?

如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代时赋值给它比较好,还是该把它定义于循环内?

cpp 复制代码
class Widget{};
const int n;


//A、 定义于循环外
Widget w;
for (int i=0; i<n; ++i){
    w = 取决于i的某个值;
}

//B、 定义于循环内
for (int i=0; i<n; ++i){
    Widget w = 取决于i的某个值;
}

两种写法的成本:

做法A: 1个构造函数 + 1个析构函数 + n个赋值操作

做法B:n个构造函数 + n个析构函数

看情况而定!一般选B!

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

旧式转型

cpp 复制代码
// C风格
(T)expression // 将expression 转型为T

// 函数风格
T(expression) //将expression转型为T

新式转型

cpp 复制代码
// 用来将对象的常量性转除
const_cast<T>(expression)
    
// 安全向下转型
dynamic_cast<T>(expression)
    
// 低级转型,很少用
reinterpret_cast<T>(expression)
    
// 用来强迫转换
static_cast<T>(expression)

旧式转型合法,但一般用新式转型:

1、容易在代码中被辨识

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

唯一使用旧式转型的的时机是,当我要调用一个 explicit 函数将一个对象传递给一个函数时。例如:

cpp 复制代码
#include <iostream>

class Widget{
public:
    explicit Widget(int size){}
};

void doSomeWork(const Widget& w){}

int main(int argc, char *argv[])
{
    // 以一个int加上"函数风格"的转型动作创建一个Widget
    doSomeWork(Widget(15));
    
    // 以一个int加上"C++风格"的转型动作创建一个Widget
    // 蓄意的"对象生成"动作感觉不怎么像"转型",所以一般
    // 用前者
    doSomeWork(static_cast<Widget>(15));
    return 0;
}

这里避免用转型

cpp 复制代码
#include <iostream>

class Window{
public:
    virtual void onResize(){}
};

class SpecialWindow: public Window{
public:
    virtual void onResize(){
        // 避免这种写法
        static_cast<Window>(*this).onResize();

        // 使用这种写法
        Window::onResize(); // 调用Window::onResize 作用于*this身上

        // 这里写SpecialWindow专属行为
        //。。。。。。。。。。。。。。
    }

};

int main(int argc, char *argv[])
{
    SpecialWindow sw;
    return 0;
}

dynamic_cast 比较耗时

之所以需要dynamic_cast,通常时候因为你想在一个你认定为 derived class 对象身上执行 derived class 操作汉航速,但你手上却只有一个"指向base"的Pointer 或 referene, 你只能靠它们处理对象。

省略。。。。。。。。。。

请记住:

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

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

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

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

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>

class Point{
public:
    Point(int a, int b):x(a),y(b){}
    void setX(int newVal){}
    void setY(int newVal){}

    int getX(){return x;}
private:
    int x,y;
};

struct RectData
{
    Point leftTop;
    Point rightDown;
};

class Rectangle{
private:
    std::shared_ptr<RectData> pData;

public:
    // point 式自定义类型,根据条款20
    // by-reference 更加高效
    // 此时返回的式内部数据的引用,有被修改的风险
    // 所以要前面要加上 const
    const Point& upperLeft() const {return pData->leftTop;}
    const Point& lowerRight() const {return pData->rightDown;}

    Rectangle(Point p1, Point p2){
        pData->leftTop = p1;
        pData->rightDown = p2;
    }
};

本例中是成员函数返回 references,但如果它们返回的式指针或者迭代器也应该如此

空悬号码牌的问题:

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>

class Point{
public:
    Point(int a, int b):x(a),y(b){}
    void setX(int newVal){}
    void setY(int newVal){}

    int getX(){return x;}
private:
    int x,y;
};

struct RectData
{
    Point leftTop;
    Point rightDown;
};

class Rectangle{
private:
    std::shared_ptr<RectData> pData;

public:
    // point 式自定义类型,根据条款20
    // by-reference 更加高效
    // 此时返回的式内部数据的引用,有被修改的风险
    // 所以要前面要加上 const
    const Point& upperLeft() const {return pData->leftTop;}
    const Point& lowerRight() const {return pData->rightDown;}

    Rectangle(Point p1, Point p2){
        pData->leftTop = p1;
        pData->rightDown = p2;
    }
};


class GUIObject{};
const Rectangle boundingBox(const GUIObject& obj){}


int main(){
    GUIObject* pgo;

    const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

}

本例子最后一句:

cpp 复制代码
boundingBox(*pgo)

对 boundingBox 的调用获得一个新的、暂时的Rectangle 对象。这个对象没有名称,所以暂时称它为 temp。

cpp 复制代码
&boundingBox(*pgo).upperLeft()

随后 upperLeft 作用于 temp 身上,返回一个 reference 指向 temp 的内部成分,更具体地说指向一个用以标识 temp 的 Points。于是pUpperLeft指向那个对象。

但是在在那个语句结束后,boundingBox 的返回值,也就是我们所说的 temp,将被销毁,而那间接导致 temp 内的 Points 析构。最终导致 pUpperLeft 指向一个不再存在的对象;

也就是说一旦产生 pUpperLeft 的那个语句结束,pUpperLeft 也就变成空悬、虚吊(dangling)

但是我有个疑问:

这里的shared_ptr怎么不需要分配内存?

请记住:

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

条款29、为 " 异常安全 "而努力是值得的

仔细分析下例ChangeBackground函数

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

struct Image
{
    char image[480*640];
};

class PrettyMenu{
public:
    void ChangeBackground(Image& imgSrc);

private:
    mutex mutex1;      // 互斥器
    Image* bgImage;   // 目前的背景图像
    int imageChanges; // 背景图像被改变的次数
};


void PrettyMenu::ChangeBackground(Image& imgSrc)
{
    mutex1.lock();               // 取得互斥器
    delete bgImage;              // 摆脱旧的背景图像
    ++imageChanges;              // 修改图像变更次数
    bgImage = new Image(imgSrc); // 安装新的背景图像
    mutex1.unlock();             // 释放互斥器
}

int main(int argc, char *argv[])
{
    PrettyMenu pt;
    return 0;
}

该函数可能会发生的问题:

泄露资源:new Image(imgSrc) 导致异常,对 unlock 的调用就绝不会执行,于是互斥器就永远把持住了

数据败坏:new Image(imgSrc) 抛出异常,bgImage就是指向一个已被删除的对象,imageChanges 也被累加,而其实并没有新的图像被成功安装起来

泄露资源的问题如何解决:以对象管理资源

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

struct Image
{
    char image[480*640];
};

class PrettyMenu{
public:
    void ChangeBackground(Image& imgSrc);

private:
    mutex mutex1;      // 互斥器
    Image* bgImage;   // 目前的背景图像
    int imageChanges; // 背景图像被改变的次数
};

void lock(mutex* pm){}
void unlock(mutex* pm){}

class Lock{
private:
    mutex* mutexPtr;
public:
    explicit Lock(mutex* pm):mutexPtr(pm)
    {
        lock(mutexPtr);
    }

    ~Lock()
    {
        unlock(mutexPtr);
    }
};


void PrettyMenu::ChangeBackground(Image& imgSrc)
{
    Lock m1(&mutex1);            // 来自条款14:获得互斥器并确保它稍后被释放
    delete bgImage;              // 摆脱旧的背景图像
    ++imageChanges;              // 修改图像变更次数
    bgImage = new Image(imgSrc); // 安装新的背景图像

}

int main(int argc, char *argv[])
{
    PrettyMenu pt;
    return 0;
}

如此,利用对象管理资源!

再来专注解决数据败坏,在解决该问题之前,先面对异常安全函数的三个保证:

基本承诺

如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。举个例子,ChangeBackground 函数抛出异常后,PrettyMenu 对象仍然可以继续拥有原背景图像 ,或是令它拥有某个缺省背景图像。

强烈保证:

如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到 "调用之前的状态"。

不抛掷保证:

承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。

异常安全码(Exception-safe code)必须提供上述三种保证之一,对于大部分函数往往在基本承诺和强烈保证中择一;

怎么让 ChangeBackground 函数提供强烈保证:

  1. 用智能指针管理Image*
  2. 调整 ++imageChanges 到真的发生新背景图像安装之后
cpp 复制代码
class PrettyMenu{
        ...
        std::shared_ptr<Image> bgImage;
        ...
}

void PrettyMenu::ChangeBackground(Image& imgSrc)
{
    Lock m1(&mutex1);            

    bgImage.reset(new Image(imgSrc));// 以 "new Image" 的执行结果设定bgImage 内部指针

    ++imageChanges;              

}

上述两个改变几乎足够让 ChangeBackground 提供强烈的异常安全保证。

但是我们还想进一步。有一个一般化的设计策略很典型地会导致强烈保证,很值得熟悉。这个策略被称为--copy and swap:

1、为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。

2、若有任何修改动作抛出异常,原对象仍保持未改变状态。

3、待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。

实现上通常是将所有 "隶属对象的数据" 从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被称为 pimpl idiom,条款31 详细描述了它。典型的写法如下:

cpp 复制代码
// 将所有 "隶属对象的数据" 从原对象放进另一个对象内
// 让 PMImpl 成为一个 struct 而不是 一个 class:
// 1、PrettyMenu 的数据封装性已经由于 "pImpl是private" 而获得了保证
// 2、如果令 PMImpl 为一个class,不是很方便 (条款25)
struct PMImpl{
    std::shared_ptr<Image> bgImage;
    int imageChanges;
};


class PrettyMenu{
    ...
private:
    mutex mutex1
    // 赋予原对象一个指针
    std::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::ChangeBackground(Image& imgSrc)
{
    using std::swap;   // 见条款25
    Lock m1(&mutex1);

    // 创建副本
    // 感觉应该写成下面这个:
    //std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));

    std::shared_ptr<PMImpl> pNew(new PMImpl);
    
    // 修改副本
    pNew->bgImage.reset(new Image(imgSrc)); 
    ++pNew->imageChanges;
    
    // 置换(swap)数据
    swap(pImpl, pNew); 
}

本例中,借以 "copy-and-swap" 策略 对对象状态实现 "全有或全无"。

清记住:

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

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

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

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

inline 关键字的作用:

inline 关键字用于修饰函数,它是一种对编译器的建议,用于告诉编译器在编译时将函数内联展开。

通常情况下,函数的调用会导致程序跳转到函数的定义处执行,然后再返回到调用点。当函数较小且频繁调用时,这种跳转和返回的开销可能会成为性能瓶颈。

使用 inline 关键字可以建议编译器将函数的定义插入调用点,而不是跳转到函数的定义处执行。这样可以减少函数调用的开销,提高程序的执行效率。

要声明一个内联函数,只需在函数定义的前面加上 inline 关键字即可。例如:

cpp 复制代码
inline int add(int x, int y) {
    return x + y;
}

需要注意的是,inline 关键字只是对编译器的建议,编译器可以选择是否将函数内联展开。 通常情况下,对于较短的函数,编译器会选择内联展开。但是对于较长的函数,编译器可能会忽略 inline 关键字的建议。

另外,将函数定义放在头文件中时,为了避免出现多重定义错误,通常需要将函数声明为 inline。因为头文件会被多个源文件包含,如果函数定义不是 inline 的,则每个源文件中都会有一份函数定义,从而导致多重定义错误。

直接在 class 定义式内呈现成员函数的本体,会让该成员函数暗自成了inline 。

请记住:

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

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

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

在了解文件间的编译依存关系之前,先来看看这个例子:

cpp 复制代码
class Person{
public:
    Person(const std::string& name, const Date& birthday,
           const Address& addr){}

    std::string name() const;
    std::string birthday() const;
    std::string address() const;

private:
    std::string theName;      //
    Date theBirthData;
    Address theAddress;
};

int main(int argc, char *argv[])
{
    std::string name;
    Date birthday;
    Address addr;

    Person p(name, birthday, addr);
    return 0;
}

没错,这个例子根本就无法通过编译,因为编译器没有取得其实现代码所用到的 classes string, Data 和 Address的定义式

这样的定义式通常由 #include 指示符提供,所以 Person 定义文件的最上方很可能存在这样的的东西:

cpp 复制代码
#include <string>
#include "data.h"
#include "address.h"

但是这样一来,Person 定义文件和其含入文件之间形成了一种编译依存关系。

现在需要 "将对象实现细目隐藏于一个指针背后"。正对 Person 可以这样做: 把 Person 分割成两个 classes,一个只提供接口,另一个负责实现该接口。如果负责实现的那个所谓 implementation class 取名为 PersonImpl,Person 将定义如下:

cpp 复制代码
#include <string>
#include <memory>


// Person 实现类的前置声明
class PersonImpl{};

//Person 接口用到的 classes 的前置声明
class Date{};
class Address{};


class Person{
public:
    Person(const std::string& name, const Date& birthday,
           const Address& addr){}

    std::string name() const;
    std::string birthday() const;
    std::string address() const;

private:
    // 指针,指向实现物
    // 以对象管理资源,条款13
    std::shared_ptr<PersonImpl> pImpl;
};

int main(int argc, char *argv[])
{
    std::string name;
    Date birthday;
    Address addr;

    Person p(name, birthday, addr);
    return 0;
}

在这里,main class(Person) 只内含一个指针成员(这里使用std::shared_ptr,见条款13),指向其实现类(PersonImpl)。这般设计常被称为 pimpl idiom(pimpl 是 "pointer to implementation"的缩写)。这种 classes 内的指针名称往往就是pImpl,就像上面代码那样。

这样的设计之下,Person的客户就完全与 Dates,Addresses 以及 Persons 的实现细目分离。那些classes 的任何实现修改都不需要 Person 客户端重新编译。这样就是"接口与实现分离"

这个分离的关键在于以"声明的依存性"替换"定义的依存性",那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策略:

1、如果使用 object reference 或 objecct pointers 可以完成任务,就不要使用 objects。

2、如果能够,尽量以 class 声明式替换 class 定义式。

3、为声明式和定义式提供不同的头文件,一个用于声明式,一个用于定义式。

像person这样使用 pimpl idiom 的 classes,往往被成为 Handle classes。

另一个制作 Handle class 的办法式,令 Person 成为一种特殊的 abstract base class(抽象基类),称为 Interface class。

这个例子有问题:

cpp 复制代码
#include <string>
#include <memory>
#include <iostream>

// 一个针对Person而写的 Interface class (抽象基类)
// 目的:
// 详细描述 derived classes 的接口(见条款34)
// 所以通常不带成员变量,也没有构造函数,
// 只有一个virtual 析构函数以及一组 pure virtual 函数,
// 用来叙述整个接口

class Date{};
class Address{};
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::shared_ptr<Person> create(const std::string& name,
                                          const Date& birthDate,
                                          const Address& addr);

};

class RealPerson: public Person{
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
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{}
};

std::shared_ptr<Person> Person::create(const std::string &name,
                                       const Date &birthDate,
                                       const Address &addr)
{
    return
        std::shared_ptr<Person>(new RealPerson(name,birthDate,addr));
}

int main(){

    std::string name;
    Date dateOfBirth;
    Address address;

    std::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));

    std::cout << pp->name() << " was born on "
              << pp->birthDate() << " and now lives at "
              << pp->address();
}

请记住:

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

2、程序库头文件应该式以 "完全且仅有声明式"的形式存在。这种做法不论是否涉及 templates 都适用。

六、继承与面向对象设计

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

请记住:

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

条款33、避免遮挡继承而来的名称

看看这个例子:

cpp 复制代码
#include <iostream>

int x= 1; // global 变量

void someFunc()
{
    double x=2.3; // local 变量
    std::cout << x << '\n';
}

int main(int argc, char *argv[])
{
    someFunc();
    return 0;
}

看见没,打印的是2.3。是 local 变量 x, 而不是 global 变量 x,因为内层作用域的名称会遮掩(遮蔽)外围作用域的名称。没错即使类型不一样,也会被遮蔽,只看变量名称!

在类继承中呢?

cpp 复制代码
#include <iostream>

class Base{
private:
    int x;
public:
    virtual void mf1()=0;
    virtual void mf1(int){}
    virtual void mf2(){}
    void mf3(){}
    void mf3(double){}
};

class Derived: public Base{
public:
    virtual void mf1(){}
    void mf3(){}
    void mf4(){}
};

int main(){
    Derived d;
    int x;

    d.mf1();             // 调用 Derived::mf1
    d.mf1(x); // 错误!因为 Derived::mf1 遮掩了 Base::mf1

    d.mf2();             // 调用 Base::mf2

    d.mf3();             // 调用 Derived::mf3
    d.mf3(x); // 错误! 因为 Derived::mf3 遮掩了 Base::mf3           
}

如本例所见,即使 base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数是 virtual 或 non-virtual 一体适用。只要函数名字相同,继承类就会遮掩掉基类的函数

当然,也有办法在扩展类中调用基类被遮掩的函数:

方法一:使用using 声明式

cpp 复制代码
#include <iostream>

class Base{
private:
    int x;
public:
    virtual void mf1()=0;
    virtual void mf1(int){}
    virtual void mf2(){}
    void mf3(){}
    void mf3(double){}
};

class Derived: public Base{
public:
    // 让Base class 内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
    using Base::mf1;
    using Base::mf3;

    virtual void mf1(){}
    void mf3(){}
    void mf4(){}
};

int main(){
    Derived d;
    int x=1;

    d.mf1();             // 调用 Derived::mf1
    d.mf1(x); // 现在可以了

    d.mf2();             // 调用 Base::mf2

    d.mf3();             // 调用 Derived::mf3
    d.mf3(x); // 现在可以了
}

方法二:使用转交函数(forwarding functions)

请记住:

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

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

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

cpp 复制代码
#include <iostream>

class Shape{
public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objectID() const;
};

class Rectangle : public Shape{
    void draw() const{}
};
class Ellipse : public Shape{
    void draw() const{}
};

int main(){
    // 错误! Shape 是抽象的,不能实例化
    Shape* ps = new Shape;

    Shape* ps1 = new Rectangle;
    ps1->draw();// 调用 Rectangle::draw

    Shape* ps2 = new Ellipse;
    ps2->draw(); // 调用 Ellipse::draw

    // 调用Shape::draw
    // 但是没有什么意义,因为纯虚函数在基类中根本没有定义
    ps1->Shape::draw();
    ps2->Shape::draw();
}

声明简朴的(非纯)impure virtual 函数的目的,是让 derived classes 继承该函数的接口和缺省实现:

比如

cpp 复制代码
class Shape{
public:
virtual void error(const std::string& msg);
}

你必须支持一个 error 函数,但如果你不想自己写一个,可以使用 Shape class 提供的缺省版本

再看一个例子:

cpp 复制代码
class Airport{};
class Airplane{
public:
    virtual void fly(const Airport& destination);
};

void Airplane::fly(const Airport& destination){
    // 缺省代码,将飞机飞到指定的目的地
}

class ModelA : public Airplane{};
class ModelB : public Airplane{};

为了避免在ModelA 和 ModelB 中撰写相同代码,缺省飞行行为由 Airplane::fly 提供,它同时被 ModelA 和 ModelB 继承。

这是个典型的面向对象设计。两个 class 共享一份相同性质(也就是它们实现 fly的方式),所以共同性质被搬到 base class 中。

如果有一个新的类 ModelC也继承 Airplane,但是飞行动作和缺省的不一样,如果忘记重载飞机动作的话,在调用飞行动作的时候就会不可避免的使用基类的飞行动作,而这会导致错误。

cpp 复制代码
class Airport{};

class Airplane{
public:
    virtual void fly(const Airport& destination);
};

void Airplane::fly(const Airport& destination){
    // 缺省代码,将飞机飞到指定的目的地
}

class ModelA : public Airplane{};
class ModelB : public Airplane{};

class ModelC: public Airplane{

    // 未声明fly函数
};

int main(int argc, char *argv[])
{
    Airport PDX;

    Airplane* pa = new ModelC;
    
    // 
    pa->fly(PDX);

    delete pa;
    return 0;
}

那有什么方法,避免上述问题?

方法一:切断 "virtual 函数接口"和其 "缺省实现之间的连接"

cpp 复制代码
#include <iostream>

class Airport{};

class Airplane{
public:
    virtual void fly(const Airport& destination) = 0;


protected:
    // 这个不是接口,是用于接口的缺省代码
    // 所以是protected,
    // 继承类也可以访问该函数
    void defaultFly(const Airport& destination);
};

void Airplane::defaultFly(const Airport& destination){
    // 缺省代码,将飞机飞到指定的目的地
}

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);}
};

class ModelC: public Airplane{
public:
    // 现在不可能意外继承不正确的fly实现代码了
    // 因为 Airplane中的pure virtual 函数迫使ModelC 必须提供自己的fly版本
    virtual void fly(const Airport &destination){}


};

int main(int argc, char *argv[])
{
    Airport PDX;

    Airplane* pa = new ModelA;

    //
    pa->fly(PDX);

    delete pa;
    return 0;
}

如果你不喜欢 以不同的函数分别提供接口和缺省实现,像上述的 fly 和 defaultFly 那样。

方法二:

pure virtual 函数必须在 derived classes 中重新声明,但它们也可以拥有自己的实现

cpp 复制代码
#include <iostream>

class Airport{};

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);}
};

class ModelB : public Airplane{
public:
    virtual void fly(const Airport &destination)
    {Airplane::fly(destination);}
};

class ModelC: public Airplane{
public:

    virtual void fly(const Airport &destination);
};

void  ModelC::fly(const Airport &destination){
    // 将C型飞机飞至指定的目的地
}

int main(int argc, char *argv[])
{
    Airport PDX;

    Airplane* pa = new ModelA;

    //
    pa->fly(PDX);

    delete pa;
    return 0;
}

纯虚函数也可以有定义!

声明 non-virtual 函数的目的是为了令 dedrived classes 继承函数的接口及一份强制性实现:

每个扩展类都必须有这个non-virtual 函数

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

请记住:

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

2、pure virtual 函数只具体指定接口继承

3、简朴的(非纯)impure virtual 函数具体指定接口继承以及缺省实现继承

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

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

healthValue 并为被声明 pure virtual,这暗示我们将会有个计算健康指数的缺省算法(见条款34)

cpp 复制代码
class GameCharacter{
public:
    virtual int healthValue() const;
}

还有其他方法:

借由 Non-Virtual Interface 手法实现 Template Method 模式:

cpp 复制代码
class GameCharacter{
private:
    virtual int doHealthValue() const{
        // 缺省算法,计算健康指数
    }

public:
    // derived classes 不重新定义它,见条款36
    // 在class 定义式内呈现成员函数本体,会让其暗自成为inline,条款30
    int healthValue() const{
        // 做一些事前工作,

        // 做真正的工作
        int retValue =  doHealthValue();

        // 做一些事后的工作

        return retValue;
    }
}

借由 Function Pointers 实现 Strategy 模式:

书上的代码表述不清楚

省略。。。。

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

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

virtual 函数系动态绑定(dynamically bound),而缺省参数值却是静态绑定(statically bound)。

这句话是不是看起来不好理解,先来看看动态绑定和静态绑定式什么意思:

cpp 复制代码
// 一个用以描述几何形状的 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 = Green) const{
        std::cout << "color: " << color << '\n';
    }
};

class Circle: public Shape{
public:
    // 以对象调用此函数,一定要指定参数值
    // 因为静态绑定下这个函数并不从其base继承缺省参数值

    // 但若以指针(或reference)调用此函数,可以并不指定参数值
    // 因为动态绑定下这个函数会从其base继承缺省参数值
    virtual void draw(ShapeColor color) const{
        std::cout << "color: " << color << '\n';
    }
};

int main(){
    // ps、pc、pr 被声明为 pointer-to-Shape类型
    // 所以不论它们真正指向什么,它们的静态类型都是 Shape*
    Shape* ps;
    Shape* pc = new Circle;
    Shape* pr = new Rectangle;
    
    // 对象的所谓动态类型则是指:
    // "目前所指对象的类型"
    // 所以:
    // pc 的动态类型是 Circle*
    // pr 的动态类型是 Rectangle*
    // ps 没有动态类型,因为它尚未指向任何对象
    

    Circle c;
    c.draw(); // 错误!没有指定参数值
    
    ps = pc; // ps的动态类型如今是 Circle*
    ps = pr;
}

Virtual 函数系动态绑定,意思是调用一个Virtual 函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型

cpp 复制代码
    pc->draw(Shape::Red); // 调用Circle::draw(Shape::Red)
    pr->draw(Shape::Red); // 调用 Rectangle::draw(Shape::Red)

缺省参数值是静态绑定。意思是你可能会在"调用一个定义于dereived class 内的函数"的同时,却使用 base class 为它指定的缺省参数值

cpp 复制代码
    // 调用Rectangel::draw(Shape::Red)
    // 是的,你没看错参数是Shape::Red
    // 而不是在Rectangle里重新定义的Shape::Green
    pr->draw();  

即使把指针换成references问题仍然存在。问题在于draw 是个 virtual 函数,而它有个缺省参数值 在 derived class 中被重新定义了。

当你想令 virtual 函数表现出你所想要的行为但却遭遇麻烦,聪明的做法是考虑替代设计。条款35中,NVI(non-virtual interface)手法:令 base class 内的一个 public non-virtual 函数调用 private virtual 函数,后者可被 derived classes 重新定义。这里我们可以让 non-virtual 函数指定缺省参数,而 private virtual 函数负责真正的工作:

cpp 复制代码
#include <iostream>


// 一个用以描述几何形状的 class
class Shape{
public:
    enum ShapeColor{Red, Green, Blue};
    void draw(ShapeColor color = Red) const{  // 如今它是non-virtual
         doDraw(color);                       // 调用一个virtual
    }

private:
    // 真正的工作在此完成
    virtual void doDraw(ShapeColor color) const=0;
};


class Rectangle : public Shape{
public:

private:
    // 注意,不须指定缺省参数值

    // 就算你这里重新定义了缺省参数,也没有用
    // 因为non-vitual 函数应该绝对不被 derived classes 覆写(条款36)
    // 这个设计很清楚地使得 draw 函数的color 缺省参数总是为Red
    virtual void doDraw(ShapeColor color) const{
         std::cout << "color:" << color << '\n';
    }
};

int main(){
    Shape* r = new Rectangle;
    r->draw();
    delete r;

    // 即使以对象调用此函数,也不需要指定参数值
    Rectangle r1;
    r1.draw();
}

请记住:

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

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

cpp 复制代码
#include <iostream>
#include <list>


template<class T>
class Set{
public:
    bool member(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    std::size_t size() const;

private:
    std::list<T> rep;
}

template<typename T>
bool Set<T>::member(const T &item) const
{
    return std::find(rep.begin(), rep.end(), item) != rep.end()
}


template<typename T>
bool Set<T>::insert(const T &item)
{
    if (!member(item)) rep.push_back(item);
}

template<typename T>
void Set<T>::remove(const T &item)
{
    typename std::list<T>::iterator it =
        std::find(rep.begin(), rep.end(), item);
    if (it != rep.end()) rep.erase(it);
}

template<typename T>
std::size_t Set<T>::size() const
{
    return rep.size();
}

请记住:

复合的意思和 public 继承完全不同

在应用域,复合意味着 has-a, 在实现域,复合意味着更具某物实现出

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

看完这个例子后,你是否理解是否要谨慎使用 private 继承:

cpp 复制代码
#include <iostream>

class Person{};
class Student: private Person{};
void eat(const Person& p);
void study(const Student& s);


int main(){
    Person p;
    Student s;

    eat(p);

    // 错误!
    // classes之间的继承关系是private
    // 编译器不会自动将一个derived class 对象转换为一个 base  class 对象
    // 这确实与 public 继承不一样

    // 第二条规则:
    // 由 private base 继承而来的所有成员,在 derived class 中都会变成 private 属性,
    // 纵使它们在 base class 中原来是 protected 或 public 属性

    eat(s);
}

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

一个简单的多重继承的例子:

cpp 复制代码
#include <iostream>

class BorrowableItem{ // 图书光允许你借某些东西
public:
    void checkOut(){} // 离开时进行检查
};

class ElectronicGadget{
private:
    bool checkOut() const{} // 执行自我检测,返回是否检测成功
};

class MP3Player:
    public BorrowableItem,
    public ElectronicGadget{};


int main(){
    MP3Player mp;

    // 有歧义,不知道是用那个基础类的
    mp.checkOut();

    // 应该这样
    mp.BorrowableItem::checkOut();
}

再复杂一点,一个钻石型多重继承:

cpp 复制代码
#include <iostream>
#include <string>

class File{
public:
    std::string fileName = "jason";
};

class InputFile : public File{};

class OutFile : public File{};

class IOFile : public InputFile,
               public OutFile{};

int main(){
    IOFile io;

    std::cout << io.fileName << '\n';
}

File class 有个成员变量 fileName,但是 IOFile 从其每一个 base class 继承一份,所以其应该有两份 fileName 成员变量。所以导致有歧义!

解决方法是:直接继承 File 的 classes 采用 "virtual 继承"

cpp 复制代码
class File{
public:
    std::string fileName = "jason";
};

class InputFile : virtual public File{};

class OutFile : virtual public File{};

class IOFile : public InputFile,
               public OutFile{};

其他省略

七、模板与泛型编程

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

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

以下 template 声明式,class 和 typename 意义完全相同:

cpp 复制代码
template<class T> class Widget;
template<typename T> class Widget;

其实下面个例子运行不起来。

cpp 复制代码
#include <iostream>
#include <vector>
// 下述 template function 接受一个STL兼容容器作为参数
template<typename C>
void prinr2nd(const C& container){ // 打印容器内的第二个元素
    if (container.size() >= 2){
        C::const_iterator iter(container.begin()); // 取得第一元素的迭代器
        ++iter;  // 将 iter 移往第二元素
        int value = *iter; // 将该元素复制到某个int
        std::cout << value; // 打印那个int
    }
}


int main(int argc, char *argv[])
{
    std::vector p{1,2,3,4};
    prinr2nd(p);
    return 0;
}

这是解决办法:

cpp 复制代码
#include <iostream>
#include <vector>
// 下述 template function 接受一个STL兼容容器作为参数
template<typename C>
void prinr2nd(const C& container){ /
    if (container.size() >= 2){
        
        //iter 的类型是 C::const_iterator,实际是什么必须取决于 template 参数C
        // template 内出现的名称如果相依某个 template 参数,称之为从属名称(dependent names)
        // 如果从属名称在class内呈嵌套状,我们称为嵌套从属名称(nested dependent name)
        
        // C::const_iterator 就是这样嵌套从属类型名称,所以必须以template 为前导
        
        typename C::const_iterator iter(container.begin()); 
        ++iter;  
        int value = *iter; 
        std::cout << value; 
    }
}


int main(int argc, char *argv[])
{
    std::vector p{1,2,3,4};
    prinr2nd(p);
    return 0;
}

当然也有例外,但暂不学习

在真实程序的代表性例子:

cpp 复制代码
template<typename IterT>
void workWithIterator(IterT iter){
    typename std::iterator_traits<IterT>::value_type temp(*iter);
}


// 如果你认为 std::iterator_traits<IterT>::value_type 读起来不畅快
// 可以考虑建立一个 typedef
template<typename IterT>
void workWithIterator(IterT iter){
    typedef typename std::iterator_traits<IterT>::value_type value_type;
    value_type    temp(*iter);
}

请记住:

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

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

条款43、学习处理模板化基类内的名称

看这个不能通过编译的例子:

cpp 复制代码
#include <iostream>
#include <vector>

class CompanyA{
public:
    void sendCleartext(const std::string& msg);
    void sendEncrypted(const std::string& msg);
};
class CompanyB{
public:
    void sendCleartext(const std::string& msg);
    void sendEncrypted(const std::string& msg);
};

class MsgInfo{};

template<typename Company>
class MsgSender{
public:
    void sendClear(const MsgInfo& info)
    {
        std::string msg;
        // 在这里,根据 info 产生信息

        Company c;
        c.sendCleartext(msg);
    }

    void sendSecret(const MsgInfo& info)
    {}
};


template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
    void sendClearMsg(const MsgInfo& info)
    {
        // 将"传送前"的信息写至log:


        // 调用 base class 函数; 这段无法通过编译
        // 问题在于:
        // 当编译器遭遇 class template LoggingMsgSender 定义式时,并不知道它继承什么样的class
        // 当然它继承的是 MsgSender<Company>,但其中的Company是个是个template参数,
        // 不到后来(当 LoggingMsgSender 被具体化)无法确切知道它是什么。
        // 而如果不知道 Company 是什么,就无法知道 class MsgSender<Company>看起来像什么
        // 更确切地说是没办法知道它是否有个 sendClear 函数
        sendClear(info);


        // 将"传送后"的信息写至log
    }
};


int main(){
    MsgSender m;
}

解决之道:针对 某个Company 产生一个MsgSender 特化版:

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

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

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

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

条款48、认识template 元编程

八、定制 new 和 delete

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

通过本例了解 new-handler:

cpp 复制代码
#include <iostream>


// 以下是 operator new 无法分配足够内存时,该被调用的函数
void outOfMem(){
    std::cerr << "Unable to satisfy request for memory\n";
    std::abort();
}

int main(){
    std::set_new_handler(outOfMem);
    int* pBigDataArray = new int[10000000000000000000000];

    delete pBigDataArray;
}

看见没,在崩溃前有报错信息了!!

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

1、让更多内存可被使用:

当程序一开始执行就分配一大块内存,而后当new-handler 第一次被调用,将它们释还给程序使用

2、安装另一个new-handler

3、卸除 new-handler

4、抛出 bad_alloc 的异常

5、不返回,通常调用 abort 或 exit。

C++ 并不支持 class 专属之 new handlers,但可以自己实现。只需令每个一个class 提供自己的 set_new_handler 和 operator new 即可。

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

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

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

九、杂项讨论

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

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

条款55、让自己熟悉 Boost

相关推荐
chushiyunen5 分钟前
dom操作笔记、xml和document等
xml·java·笔记
chushiyunen8 分钟前
tomcat使用笔记、启动失败但是未打印日志
java·笔记·tomcat
汇能感知13 分钟前
光谱相机的光谱数据采集原理
经验分享·笔记·科技
人人题36 分钟前
汽车加气站操作工考试答题模板
笔记·职场和发展·微信小程序·汽车·创业创新·学习方法·业界资讯
小脑斧爱吃鱼鱼1 小时前
鸿蒙项目笔记(1)
笔记·学习·harmonyos
车载小杜1 小时前
基于指针的线程池
开发语言·c++
沐知全栈开发1 小时前
Servlet 点击计数器
开发语言
m0Java门徒1 小时前
Java 递归全解析:从原理到优化的实战指南
java·开发语言
桃子酱紫君2 小时前
华为配置篇-BGP实验
开发语言·华为·php
QTX187302 小时前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式