IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++_Primer_学习笔记_第十八章(用于大型程序的工具) -> 正文阅读

[C++知识库]C++_Primer_学习笔记_第十八章(用于大型程序的工具)

第十八章(用于大型程序的工具)

  1. 子系统,子程序如何协同处理错误
  2. 如何使用各种库进行协同开发
  3. 复杂的概念如何建模

/1.异常处理

1).将异常的捕获和处理分开。
2).了解以下的过程

  • 异常抛出时发生了什么
  • 异常捕获时发生了什么
  • 传递错误的对象的意义

//1.抛出异常

1).当执行一个throw时,跟在throw后面的语句不再被执行。程序的控制权由,throw转移到与之匹配的catch模块中。catch可能是同一个函数的局部catch,也可能是直接或者间接调用发生异常函数的另一个函数中。

  • 控制权的转移有两个重要的含义。
  1. 沿着调用链的函数可能会提早推出。
  2. 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
  • throw后面的语句不再被执行,所以它的用法和return很像。

2).栈展开

  • 当一个异常抛出时,程序暂停当前函数的执行过程,并立即开始寻找与异常匹配的catch子句。
  • throw出现在一个try语句块时,检查该try块关联的catch字句,如果找到则使用该字句;否则如果该try语句块嵌套地在其他语句块中,则继续检查与外层的try语句块中关联的catch字句。如还是没有找到,则退出当前的函数,在调用当前函数的外层函数中寻找。
  • 如果对函数的调用在一个try语句块中…
  • 以上的过程就是一个栈展开。它沿着嵌套函数的调用链不断查找…如果没有找到匹配的catch,则退出主函数后查找过程终止。
  • 当找到一个catch字句之后,程序进入该子句执行代码,执行完毕之后,从该try语句块的最后一个catch子句后开始执行代码。
  • 当找不到匹配的catch,程序将会调用标准库函数terminate,终止程序。所以一旦发生异常,不能对他置之不理。否则它将导致程序的终止。

3).栈展开过程对象将被自动销毁

  • 当栈展开过程中,出现块退出的情况,编译器将会负责确保在这个块中创建的对象被正确销毁。如果是类类型,析构函数会自动调用
  • 如若异常发生在构造函数中,**当前对象可能只构造了一部分,有的成员已初始化,有的也许在异常发生前没有初始化。此时我们也要保证已经构造的成员被正确的销毁。**类似的,异常也可能发生在数组或者标准库容器的元素初始化过程中。对于已经构造的一部分元素,我们也需要确保正确地销毁。

4).析构函数与异常

  • 析构函数总是会被执行,不论是在程序正常进行时还是发生了异常,为了正确的释放资源,我们使用类来控制资源的分配,从而避免在块中释放资源代码,之前发生了异常导致资源没有正确地释放。
  • 由于栈展开过程中很可能会调用析构函数。并且在栈展开的过程中,我们是已经引发了异常但是没有处理它。具体原因未知(连续地抛出异常会怎么样?),析构函数不应该抛出不能被它自身处理的异常。对于可能抛出异常的操作都应该放置在一个try语句块中去,并在析构函数内部得到处理。如若析构函数抛出了异常,并且在 函数内部 没有得到处理,则程序将被终止。
  • 在实际中,由于析构函数仅仅是释放资源,所以它不太可能抛出异常,所有的标准库类型都确保它们的析构函数不会引发异常。

5).异常对象

  • 编译器使用异常抛出表达式对异常对象进行拷贝初始化。**因此throw语句中的表达式必须拥有完全类型;**并且如果该表达式是
  1. 类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或者移动构造函数。
  2. 如果该表达式是数组类型或者函数类型,则表达式将被转换成与之对应的指针类型。
  • 异常对象位于编译器管理的空间中,编译器确保无论最终调用的是哪一个catch字句,都能访问到该空间,当异常处理完毕后,异常对象将会被销毁。
  • 抛出一个指向局部对象的指针肯定是一个错误的行为;因为在该块的catch语句执行之前,它所指的对象肯定已经销毁了。
  • 当我们抛出一条表达式时,该表达式的静态编译时的类型,决定了异常对象的类型。因此,如果throw表达式解引用一个基类指针,而该指针实际指向的时派生类对象,则抛出时对象将会发生切断。只有基类部分被抛出。

练习,

  • 18.3,解决资源正确释放的问题,
  1. 使用try语句块
  2. 使用类来管理内存。

//2.捕获异常

1).catch字句中的异常声明看起来就像是只包含一个形参的函数形参列表。如果我们不需要使用抛出表达式,我们可以忽略捕获形参的名字。

  • 声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,可以是左值引用,但是不能是右值引用。
  • 当进入一个catch语句之后,通过异常对象初始化声明中的参数。和函数的参数一样,如果catch的参数是非引用类型,则该参数是异常对象的一个副本,如果是引用,则是异常对象的一个别名。想要说明的区别是修改该形参是否修改本体。
  • catch的参数如果是基类类型,则我们可以使用其派生类类型的异常对象对他进行初始化。(和函数的形参是一样的)
  1. 如果是非引用,派生类的非基类部分将会被切断。
  2. 如果是基类的引用,将是绑定。动态绑定。
  • 异常声明中的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。
  • 通常情况下,如果catch的类型和继承体系相关,最好设置catch的参数为引用类型。

2).查找匹配的处理代码

  • 在搜寻catch的过程中,我们最终找到的catch未必是异常的最佳匹配。而是,挑选出来的应该是第一个和异常匹配的catch语句。因此,越是专门的catch月应该置于整个catch列表的前端。
  • 原因是:catch语句是按照其出现的顺序逐一进行匹配的。当程序使用具有继承关系的多个异常时,必须对catch语句的顺序进行组织和管理,使得派生类异常的代码出现子在基类异常的处理代码之前。
  • 与实参和形参的匹配规则相比,异常和catch异常声明之间的匹配规则受到更多的限制。此时,绝大多数的类型转换都不被允许,除了一些极细小的差别之外,要求异常的类型和catch声明类型是精确匹配的。
  1. 允许非常量到常量之间的转换。非常量对象的throw可以匹配到一个接受常量引用的catch语句。
  2. 允许从派生类到基类的转换
  3. 数组转换成指向数组首元素的指针;函数转换成指向该函数的指针。
  • 除此之外,包括算术类型,类类型转换在内的所有转换规则,都不能用在匹配catch中。(除了2,其他的规则和函数模板的推断规则一致。)

3).重新抛出

  • 有时候一个单独的catch语句不能完成地处理某一个异常。在执行了某一些矫正之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。
  • **怎么做到?**一条catch语句通过重新抛出的操作将异常传递给另一个catch语句。
  • 这里的重新抛出仍然是一条throw语句;只不过不包含任何表达式。throw;
  • 空的throw语句只能出现在catch语句或者catch语句直接或者间接调用的函数之内。如果在处理代码之外的区域遇到了空的throw语句,编译器将会调用terminate
  • 一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链,向上传递。
  • 很多时候catch语句会该表其参数的内容。如果在改变了参数内容之后catch语句重新throw,则只有当catch异常声明是引用类型时,我们对参数所作的改变才会被保留并继续传播。
{
    // 使用引用类型的异常声明
    catch (my_error &eObj) {
        eObj.status = errCodes::severeErr;     //修改异常对象
        throw;//重新抛出
    } catch (other_error_eObj) {
        // 非引用类型
        eObj.status = errCodes::badErr;//只是修改了对象的局部副本

        throw;//异常对象的status没有修改
    }
}

4).捕获所有的异常处理代码

  • 这是有难度的,因为我们有时候不知道异常的类型是什么。即使知道,也很难为每一个异常提供一个catch子句。为了一次性捕获所有的异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码。catch(...);一条语句可以和任意类型的异常对象匹配。
  • 它经常和重新抛出语句一起使用,其中catch执行完当前局部能够完成的工作之后,重新抛出异常。
{
    void manip () {
        try {
            // 这里的操作将引发并抛出一个异常
        } 
        catch (...) {
            // 处理异常的某一些操作
            throw;
        }
    }

    // catch(...)既可以单独出现
    // 也可以和其他的catch一起出现
    // 如果和其他的catch一起出现
    // 则它必须放在最后的位置
    // 否则放在捕获所有异常的catch语句之后的catch语句永远不会被执行

}

练习

  • 18.5,调用abort(),用来终止main函数,它是定义在头文件cstdlib中的。

//3.函数try语句块和构造函数

1).异常在任何时刻都可能发生;特别地,异常可能发生在处理构造函数的初始值中。

  • 构造函数在进入函数体之前首先执行初始化列表。因为在初始值列表抛出异常时构造函数体内的try语句块还未生效,所以构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常。
  • 如何解决?**我们必须将构造函数写成函数try语句块(函数测试块)的形式。**函数try语句块使得一组catch语句既可以处理构造函数体(或者析构函数体),也可以处理构造函数的初始化过程(析构函数的的析构过程)。
{
    // 这是处理构造函数初始值错误的唯一方法
    template <typename T> 
    Blob<T>::Blob(initializer_list<T> il) try :
                data(make_shared<vector<T>>(il)) {
                    // 空函数体
    } catch (const bad_alloc &e) {
        handle_out_of_memory(e);
    }
    // 关键字try出现在
    // 表示构造函数初始值列表的冒号
    // 以及表示构造函数体的扩阔好
    // 之前
    // 与之歌try关联的catch既可以处理函数体也可以处理初始化列表
    // 注意,在初始化构造函数的参数时,也可能会发生异常
    // 这样的异常不属于函数try语句块的一部分
    // 而是属于调用表达式的一部分,并将在调用者所在的上下文中处理。
}

//4.noexcept异常说明

1).预先知道某一个函数不会抛出异常显然有多好处

  1. 简化调用该函数的代码(用户)
  2. 编译器可以执行某一个特殊的优化操作;这些优化操作不适用于可能会出错的代码。

2).新标准中,我们可以通过提供noexcept说明,指定某一个函数不会抛出异常。

{
    // 紧跟在函数的参数列表后面
    // 该说明应该在函数的尾置返回类型之前
    // 在成员函数中,noexcept说明符应该出现在const以及引用限定之后
    // 而在final,override,或者虚函数的=0之前。
    void recoup(int) noexcept;//不会抛出异常
    void alloc(int);//可能会抛出异常

    // noexcept要么出现在该函数的所有声明和定义语句中
    // 要么一次也不出现。

    // 我们也可以在函数指针的声明和定义中指定noexcept
    // 在typedef或类型别名中不能出现noexcept
}

3).违反异常说明

  • 编译器并不会在编译时检查noexcept说明。如果在一个函数中说明了noexcept的同时又含有throw语句,或者调用可能抛出异常的其他函数,编译器将会顺序编译通过,并不会因为这种违反异常说明滚的情况而报错。(又可能会有编译器会提出警告)
  • 因此可能会出现一个函数既声明了不会抛出异常,而又抛出了异常。此时,一旦抛出异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。上述过程对是否执行栈展开没有约定。
  • 因此noexcept用在两种情况。
  1. 我们确认函数不会抛出异常
  2. 我们根本不会处理该异常
  • 通常情况下,编译器不能也不必在编译时验证异常说明。

4).向后兼容

  • 早期的c++设计了一套详细的异常说明方案。该方案允许我们指定某一个函数可能会抛出的异常类型。函数可以指定一个关键字throw,后面跟上括号起来的异常类型列表。位置同noexcept
  • 上述使用的throw在c++11中已经取消了。
{
    void recoup(int) noexcept;
    void recoup(int) throw();//等价声明
}

5).异常说明的实参

{
    // noexcept接受一个可选的实参,该实参必须能转换为bool
    // 如果是实参类型为true,函数不会抛出异常
    // 实参时false,函数可能会抛出异常
    void recoup(int) noexcept(true);
    void alloc(int) noexcept(false);
}

6).noexcept运算符

  • noexcept的实参通常和noexcept运算符混合使用。
  • noexcept是一个一元运算符,它的返回值是一个bool类型的右值常量表达式。用于表示给定的表达式是否会抛出异常。
  • sizeof一样,noexcept也不会对该表达式求值。
{
    noexcept(recoup(i));//如果recoup不抛出异常则结果为true
    // 否则结果为false

    noexcept(e);
    // 当e调用的所有函数都做了不跑出说明且e本身不含有throw语句时
    // 上述表达式为true
    // 否则上述表达式为false

    // 这样使用noexcept
    void (*pf) noexcept = recoup;
    // 使用
    void f() noexcept(noexcept(g()));//f和g的异常说明是一样的
    // 当g有异常说符但是实参为false时,也是可能抛出异常的
    // 此时f也是可能抛出异常的
}

7).异常说明与指针,虚函数,拷贝控制

  • noexcept说明符不是函数类型的一部分,但是函数异常说明会影响函数的使用。
  • 函数指针及该指针所指的函数必须具有一样的异常说明。
{
    // 指针做了不抛出的声明,只能指向不跑出的函数
    // 指针显式或者隐式地说明可能抛出异常,则该指针可以指向任何函数
    // 即使是承诺了不抛出异常的函数也是可以的

    // recoup和pf1都承诺不会抛出异常
    // alloc可能会抛出错误
    void (*pf)(int) noexcept = recoup;
    void (*pf2)(int) = recoup;//正确
    pf = alloc;//错误,
    pf2 = alloc;//正确

    // ------------虚函数
    // 如果一个虚函数承诺了它不会抛出异常,则它后续的派生类的虚函数也必须做出同样的承诺。
    // 如果基类的虚函数允许抛出异常时,我们可以设置派生类对应的函数不能抛出异常,当然也可以是允许抛出异常
    class Base {
    public:
        virtual double f1(double) noexcept;
        // 显式和隐式地指出可能会抛出异常
        virtual int f2() noexcept(false);
        virtual void f3();
    };
    class D : public Base {
    public:
        double f1(double);//错误,没有指明是noexcept
        int f2() noexcept(true);//正确,虽然基类的是可能抛出异常的
        void f3() noexcept;//正确,同上
    };


    //-----------拷贝控制成员,没有实例,不好理解。
    // 当编译器合成拷贝控制成员时,同时也生成一个异常说明符。
    // 如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept的。
    // 如果合成成员调用的任意一个函数可能会抛出异常,则合成的成员是noexcept(false)的。
    // 而且如果我们定义了一个析构函数,但是没有为它提供异常说明
    // 编译器将会合成一个,合成的异常说明符将与假设编译器为类合成的析构函数时所得的异常说明一致。
}

//5.异常类继承体系

1).标准异常类构成了一个继承体系。见p693。

  • 类型exception仅仅定义了拷贝构造函数,拷贝赋值运算符,一个虚析构函数,一个名为what的虚成员。其中what返回一个const char*,该指针指向一个以null结尾的字符数组,并且确保不会抛出任何异常。
  • exceptionbad_castbad_alloc定义了默认构造函数。类runtime_errorlogic_error没有默认构造函数,但是有一个接受c风格字符串或者标准库string实参的构造函数,这些实参负责提供关于错误的更多信息。在这些类中,what负责返回用于初始化异常类对象的信息。因为what是一个虚函数,所以当我们捕获基类引用时,对what的调用就是一个动态绑定。

2).书店应用程序的异常类

  • 实际的应用程序通常会自定义exception或者exception的标准库派生类的 派生类以扩展继承体系。这些面向应用的异常类表示了与应用相关的异常条件。
  • 如果我们构建一个真正的书店程序会复杂很多。例如,如何处理异常,我们很可能需要建立一个自己的异常类体系,用它来表示与应用相关的各种问题。
{
    // 异常类
    class out_of_stock : public std::runtime_error {
    pubic:
        explicit out_of_stock(const string &s) :
                        runtime_error(s) { }
    }; 
    class isbn_mismatch : public std::logic_error {
    public:
        explicit isbn_mismatch(const string &s) :
                        logic_error(s) { }
        isbn_mismatch(const string &s,
                        const string &lhs, const string &rhs) :
                        logic_error(s), left(lhs), right(rhs) { }
        const string left, right;
    };  

    // exception表示的含义是某处出错了,具体的信息描述
    // 继承体系的第二层将exception划分为两大类,运行时错误和逻辑错误
    // 运行时错误表示的是只有在程序运行时才能耐检测的错误
    // 逻辑错误一般指的是我们可以在程序代码中发现的错误
    // 我们上述的定义将异常类别进一步细分。

    // ---------------------使用我们自己的异常类型
    // 使用方式和标准库提供的标准异常类的方式完全一致。
    
    Sales_data &
    Sales_data::operator+=(const Sales_data &r) {
        if (isbn() != r.isbn()) 
            throw isbn_mismatch("wrong isbns", isbn(), r.isbn());
        units_sold += r.units_sold;
        revenue += t.revenue;
        return *this;
    }

    Sales_data item1, item2;
    while (cin >> item1 >> item2) {
        try {
            sum = item1 + item2;//计算和
        } catch (const isbn_mismatch &e) {
            cerr << w.what() << ": left isbn(" << e.left << ") right isbn(" << e.right << ")" << endl;
        }
    }
}

练习,

  • 18.11,what确保不会抛出异常。否则,新产生的异常中由于what继续产生异常,将会导致抛出异常的死循环。

/2.命名空间

1).大型程序往往会使用多个独立开发库,这些库有会定义大量的全局名字,例如类,函数,模板等。当应用程序用到多个供应商提供的库时,不可避免地会发生某一些名字的冲突。

  • 多个库将名字放置在全局命名空间中将引发命名空间污染

2).解决,

  1. **传统上,**程序员通过将其定义的全局实体名字设得很长来避免命名空间得污染。这样的名字通常包括表示名字所属库的前缀部分。
{
    class cplusplus_primer_Query {....};
    string cplusplus_primer_make_plural(size_t, string&);

    // 显然这样的方式不太理想
    // 书写和阅读都不方便
}
  1. 命名空间,分割了全局命名空间,使得每一个命名空间是一个作用域。通过在某一个命名空间中定义库的名字,库的作者,用户可以避免全局名字固有的限制。

//1.命名空间的定义

1).一个命名空间定义包含两个部分

  1. 关键字namespace
  2. 随后是命名空间的名字

2).在命名空间名字后面是一系列由花括号括起来的声明和定义。

  • 只要能出现在全局作用域中的声明就能置于命名空间内。主要包括,类,变量(以及初始化操作),函数(以及定义),模板,其他的命名空间。
{
    namespace cplusplus_primer {
        class Sales_data {...};
        Sales_data operator+(const Sales_data&, const Sales_data&);
        class Query {....};
        class Query_base {...};
    }
    // 注意和其他的名字一样,命名空间的名字也必须在定义它的作用域内保持唯一
    // 命名空间既可以定义在全局作用域内
    // 也可以定义在其他命名空间中
    // 但是不能定义在函数或类的内部
}

3).每一个命名空间都是一个作用域

  • 和其他作用域类似,命名空间中的名字都必须是该空间中的唯一实体。因为不同命名空间是不同的作用域,所以在不同命名空间中可以使用相同的名字。
  • 定义在某一个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于命名空间之外的代码则必须明确指出所用的名字属于哪一个空间。
{
    cplusplus_primer::Query q = cplusplus_primer::Query("hello");

    // 如果AW中也定义了一个名为Query的类,可以这样使用
    AW::Query q = AW::Query("hello);
}

4).命名空间可以是不连续的

  • 命名空间可以定义在几个不同的部分,这一点和作用域不太一样。
{
    namespace nsp {
        // 一些声明
    }
    // 可能是定义了一个名为nsp的新命名空间
    // 可能是为已经存在的命名空间添加一些成员
    // 如果之前没有,则是创建一个新的命名空间
    // 否则就是打开已经存在命名空间定义并添加一些成员的声明
}
  • 命名空间的不连续特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。此时命名空间的组织方式类似我们管理自定义的类及函数的方式
  1. 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数以及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。(在头文件中使用命名空间)
  2. 命名空间成员的定义部分则置于另外的源文件中。
  • 在程序中某一些实体只能定义一次,
  1. 非内联函数
  2. 静态数据成员
  3. 变量等
  • 命名空间中定义名字也需要满足这一要求,我们可以通过一样的方式组织命名空间达到目的。这种接口和实现分离的机制使得我们所需的函数和实体只定义一次,而只要是使用到这些实体的地方都可以看到实体的声明。
  • 每一个文件应该保存独立的命名空间,或者相互关联的命名空间。p697.

5).定义命名空间

{
    //---------- Sales_data.h
    // #include要在打开命名空间操作之前
    #include <string>
    namespace cplusplus_primer {
        class Sales_data {/.../};
        Sales_data operator+(const Sales_data &,
                            const Sales_data &);
        // Sales_data的其他接口函数的声明
    }

    // ------------Sales_data.cpp
    #include "Sales_data.h"
    namespace cplusplus_primer {
        // Sales_data成员以及重载运算符的定义。
    }

    // 程序如果想要使用我们的库,必须包含必要的头文件,这些头文件的名字定义在
    // 命名空间cplusplus_primer中。

    //------------ user.cpp
    #include "Sales_data.h"

    int main() {
        using cplusplus_primer::Sales_data;
        Sales_data trans1, trans2;
        //.....
        return 0;
    }


    // 这种程序的组织特性提供了开发者和用户所需要的模块性
    // 每一个类仍组织在自己接口和实现中,一个类的用户不必编译与其他类相关的名字。
    // 我们对用户隐藏了是实现细节
    // 同时允许文件Sales_data.cpp和user.cpp被编译并链接成一个程序而不会产生任何编译错误或链接时错误。
    // 库开发者可以分别实现每一个类,相互之间没有干扰

    // 通常情况下,我们不把#include放在命名空间中。
    // 如果我们这样做,隐含的意思就是把头文件中的名字都定义成该命名空间中的成员。
    // 如果#include<string>放在cplusplus_primer内部
    // 将会导致错误,因为这样做将表示,我们试图将std嵌套在cplusplus_primer中
}

6).定义命名空间成员

{
    // 命名空间中的成员无需前缀
    #include "Sales_data.h"
    namespace cplusplus_primer {
        std::istream &
        operator>>(std::istream &in, Sales_data &s) {/*......*/}
    }

    // 也可以在空间外部定义,需要指出命名空间
    // 但是对名字的声明必须在内部
    cpp_primer::Sales_data
    cpp_primer::operator+(const Sales_data &r, 
                            const Sales_data &l) {
        Sales_data ret(l);
        ...
    }
    // 和类外部定义的类成员一样,一旦看到前缀名字
    // 我们就相当于进入了命名空间内部。

    // 注意,这样的外部定义只能
    // 在所属命名空间的外层空间中。
    //  上例,只能在全局作用域
}

7).模板特例化

{
    // 模板特例化必须声明在原始模板所属的命名空间中。
    // 可以在外部进行特例化
    namespace std {
        template<> struct hash<Sales_data>;
    }

    template<> struct std::hash<Sales_data> {
        size_t operator() (const Sales_data &s) const {
            return hash<string>()(s.bookNo) ^ 
                    hash<unsigned>()(s.units_sold) ^
                    hash<double>()(s.revenue);
            // ...
        }
    };
}

8).全局命名空间(全局作用域)

  • 全局作用域中定义的名字(即在所有类,函数,命名空间之外定义的名字)也就是定义在全局命名空间中。
  • 全局命名空间以隐式的方式声明,并且在所有的程序中都存在。
  • 全局作用域中定义的名字被隐式地添加到全局命名空间中。
  • 作用域运算符同样可以用于全局作用域(全局命名空间)的成员,因为全局明明空间是没有隐式地,它没有名字。
  • ::member_name表示全局命名空间中的一个成员

9).嵌套的命名空间

{
    namespace cpp_primer {
        namespace QueryLib {
            class Query {...};
            Query operator&(const Query&, const Query&);
            //......
        }
        namespace BookStore {
            class Quote {/*...*/};
            class Disc_quote : public Quote {/*...*/};
        }
    }
    // 将cpp_primer空间分割为两个空间
    // 嵌套的命名空间同时是一个嵌套的作用域
    // 它嵌套在外层的命名空间中
    // 嵌套的命名空间中的名字查找遵循的规则和以往一样
    // 内层屏蔽外层
    // 可以访问外层的变量
    // 内层的变量只在内层有效

    // 访问
    cpp_primer::QueryLib::Query;
}

10).内联命名空间

  • c++11新标准引入了一种新的嵌套命名空间,称为内联命名空间。
  • 和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。即,我们无须在内联命名空间的名字前面添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问到它。
{
    // 定义的方式是在关键字namespace之前添加关键字inline
    inline namespace FifthEd {
        // 
    }
    // 隐式内联
    namespace FifthEd {
        class Query_base {/*....*/}
        
    }
    // 关键字inline必须出现在命名空间第一次定义的地方,
    // 后续再打开命名空间的时候可以写inline,也可以不写

    // 当应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到内联命名空间。
    namespace FourthEd {
        class Item_base {/*....*/};
        class Query_base {/*...*/};
    }

    // 命名空间将cpp_primer会同时使用这两个命名空间
    // 假定每一个命名空间都定义在同名字的头文件中,则我们可以把命名空间cpp_primer定义成如下的形式

    namespace cpp_primer {
        #include "FifthEd.h"
        #include "FourthEd.h"
    }
    // 因为FifthEd是内联的,所以cpp_primer::的代码可以直接获得FifthEd的成员
    // 如果我们想要使用更早期的版本的代码
    // 则必须像其他嵌套的命名空间一样加上完整的外层命名空间的名字,
    cpp_primer::FourthEd::Query_base;

}

11).未命名的命名空间

  • 指的是关键namespace之后紧跟花括号起来的一系列声明
  • 里面定义的变量拥有静态生命周期,它们在第一次使用前创建,直到程序结束时才销毁。
  • 一个未命名的命名空间可以在某一个给定的文件内不连续,**但是不能跨越多个文件。每一个文件定义自己的未命名空间,如果两个文件都含有未命名空间,则这两个空间相互无关。**在这两个空间中可以定义同名变量,并且这些定义表示的是不同的实体。
  • 如果一个头文件定义了未命名空间,则该命名空间中定义的名字将在每一个包含该头文件对应不同的实体。
  • 定义在未命名的命名空间中的名字可以直接使用,因为我们找不到命名空间的名字来限定。同样的,我们也不能对未命名的命名空间的成员使用作用域运算符。
  • 未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外城作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别。
{
    int i;//i的全局声明
    namespace {
        int i;
    }
    // 二义性,i既出现在全局作用域中,有出现在未命名的命名空间中
    i = 10;

    // 其他情况下,未命名的命名空间中的成员都属于正确的程序实体

    //----------嵌套的未命名空间
    // 可以用外层的命名空间的名字来访问
    namespace local {
        namespace {
            int i;
        }
    } 
    // 定义在嵌套的未命名的命名空间中,与全局作用域的i不同
    local::i = 42;

    // 是否需要和外层中定义的名字不同?
    // 否则访问时,也会有二义性?

    // 在标准c++引入命名空间之前,程序需要将名字声明为static
    // 使得其对整个文件有效
    // 在文件中进行静态声明是从C中继承而来的
    // 声明为static的全局实体在所在文件外不可见
    // 现在使用未命名的命名空间来代替
}

//2.使用命名空间的成员

1).using声明。
2).命名空间别名。

  • namespace primer = cplusplus_primer;
  • 不能在命名空间还没有定义就声明别名,否则报错。
  • 命名空间的别名也可以指向一个嵌套的命名空间。namespace Qlib = cplusplus_primer::QueryLib;
  • 使用Qlib::Query q;
  • 一个命名空间可以有多个别名,每一个别名都和原来的名字等价。

3).using指示,扼要概述

  • 一条using声明语句一次只引入命名空间的一个成员,它使得我们可以清楚地知道程序中所用到的到底是哪一个名字。
  • using声明引入的名字遵守和过去一样作用域规则,它的有效范围从using声明开始,一直到using声明所在的作用域结束。
  • 在此过程中,外层作用域的同名实体将会被隐藏。
  • 未加限定的名字可以在using声明所在的作用域中以及其内层作用域中使用。有效作用域结束后,我们就必须使用完整的限定名字了。
  • 一条using声明可以使用在全局作用域,局部作用域,命名空间作用域,以及类作用域中。在类的作用域中,这样的using声明语句,只能指向基类成员。?
  • using指示,和using声明一样的是,我们可以使用名字的简写形式。和using不同的是,我们无法控制哪些名字是可见的,因为所有的名字都是可见的。
  • 形式,using namespace /*命名空间的名字*/。如果这里的命名空间不是已经定义好的,那么报错。
  • 可以出现在全局,局部,命名空间作用域中,但是不能出现在类的作用域中。
  • using指示使得某一个特定的命名空间中的所有的名字都可见,这样我们无需为它们添加任何前缀限定。前提是在using的作用域中。
  • 如果我们提供一个对std命名空间的using指示而没有做任何特殊控制,将重新引入由于使用了多个库而造成的名字冲突的问题。

4).using指示和作用域

  • using指示引入的名字的作用域远比using声明引入的名字的作用域复杂。
  • using指示将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。
  • 这是因为命名空间中会含有一些不能出现在局部作用域的定义,因此using指示一般被看作是出现在最近的外层作用域中。
{
    // A和f都定义在全局作用域中,如若f含有一个对A的using指示,则在f看来
    // A中的名字仿佛是出现在全局作用域中f之前的位置一样
    namespace A {
        int i, j;
    }
    void f() {
        using namespace A;      //把A中的名字注入到全局作用域中
        cout << i * j << endl;
    }

    // -------------示例

    namespace blip {
        int i = 16, j = 15, k = 23;
    }
    int j = 0;      //blip::j在命名空间中,正确
    void manip() {
        using namespace blip;
        // using指示,将blip中的名字“添加”到全局作用域中
        // 如若使用了j则将在::j和blip::j
        // 之间发生冲突
        ++i;//正确
        ++j;//二义性错误
        ++::j;//正确,全局的j
        ++blip::j;//正确
        int k = 97;//当前的k屏蔽blip::k
        ++k;//当前局部的k
    }

    // manip的代码可以直接使用它的成员
    // 其他代码呢?
    // 名字的冲突是允许的,但是使用时需要加上限定符号
    // 使用using指示就像是注入到全局作用域中,并且离using指示距离最近的前面。


    // ---------------头文件和using声明与指示

    // 头文件如果在其顶层作用域中含有using指示,或者using声明,则会将名字注入到所有包含该头文件中
    // 通常情况下,头文件最多只能在它的函数或命名空间中使用using指示或者using声明?
    // 因为只是负责定义接口的名字不负责实现部分的名字

    //------------避免using指示
    // 如若对多个命名空间都使用using指示,
    // 这些库的所有名字都可见
    // 则全局命名空间的污染问题重新出现
    // 并且引入库的新版本时,正在工作的可能会编译失败,只要新版本引入了一个与应用程序当前正在使用的名字冲突的名字,就会出现这个问题
    // 而且这种错误,是只有使用了二义性的名字时才会被发现。意味着引入特定库很久之后才会爆发冲突
    // 或者直到程序使用该库的新部分才会发现这样的错误

    // 而using声明,如果出现二义性,声明处就会发现;并且一次只引入一个名字。发生错误容易检测

    // using指示,在命名空间本身的实现文件中就可以使用。 

}

练习

  • 18.16,
  1. 在函数内using声明,并且定义一个与其一样的名字是重复定义的错误
  2. 在函数内using声明,在函数内可以直接使用,而如果要使用全局的,需要加上::
  3. 在函数外进行using声明,函数内可以直接使用。
  • using指示时
  1. 在全局作用域。命名空间的名字可以直接在函数访问。如果全局变量和命名空间里的名字有重名,有二义性的错误。
  2. 如果在函数里面。同上。
  • 书本的下一题解释才是正确的。

//3.类,命名空间和作用域

1).对命名空间了内部的名字查找遵循常规查找规则。由内向外依次查找每一个外层作用域。外层作用域也可能是一个或者多个嵌套的命名空间。直到最外层的全局命名空间查找过程终止。

  • 只有位于开放的块中且在使用功能点之前声明的名字才会被考虑。
{
    namespace A {
        int i;
        namespace B {
            int i;      //在B中隐藏了A::i
            int j;
            int f1() {
                int j;      //j是局部变量隐藏了A::B::j
                return i;   //返回的是B::i
            }
        }//命名空间B结束,此后B中定义的名字不再可见
        int f2() {
            return j;//错误j没有被定义
        }
        int j = i;//使用A::i进行初始化。
    }
}
  • 对于位于命名空间中的类来说,常规的查找规则仍然适用。当类的成员函数是使用了某一个名字
  1. 首先在该点之前的函数体中查找(包括形参列表)
  2. 然后再从成员中查找,不关乎函数在哪一个点,包括基类。(友元何时查找?)
  3. 接着是外层作用域中查找,(此时可能是一个或者多个命名空间。)
{
    namespace A {
        int i;
        int k;
        class C1 {
        public:
            C1() : i(0), j(0) { }//初始化类中成员
            int f1() {return k;}//A::k
            int f2() {return h;}//错误,还未定义
            int f3();
        private:
            int i;//隐藏外层的A::i
            int j;
        };
        int h = i;//使用的是A::i
    }

    int A::C1::f3() {return h;}
    // 上述语句正确,原因是,
    // 函数的限定符指出了查找名字时检查作用域的次序
    // 当定义函数时,h已经有了定义,故可以这样使用。
    // 以上函数的查找f3->C1->A->包含f3定义的作用域。
}

2).类类型形参的函数查找

{
    string s;
    std::cin >> s;
    // 等价于
    operator>>(std::cin, s);
    // operator>>函数定义在标准库string中,string又定义在命名空间std中
    // 但是我们不用std::限定符和using声明就可以使用operator>>

    // 对于命名空间中名字的隐藏规则来说有一个重要例外,他使得我们可以直接访问输出运算符
    // 这个例外就是,当我们给函数传递一个类类型的对象时,除了在常规的作用域中查找外,还会查找实参所属的命名空间
    // 这一例外对于传递类的引用或者指针的调用同样有效

    // 在这个例子中,当编译器发现对operator>>的调用时,首先在当前的作用域中查找合适的函数
    // 接着查找输出语句的外层作用域
    // 随后因为,>>的表达式的形参时类类型的,所以编译器还会查找cin和s的类所属的命名空间
    // 也就是说,对于这个调用,编译器还会查找定义了istream和string的命名空间std。
    // 当在std中查找时,编译器找到了strnig的输出运算符

    // 查找规则这个例外,允许概念上作为类接口的一部分的非成员函数无需单独的using声明就可以被程序使用
    // 假如以上的例外不存在
    // 我们将不得不提供一个using声明
    using std::operator>>;
    // 或者在调用函数时,
    std::operator>>(std::cin, s);

    // 这样既笨拙又增加了负担。
}

3).std::move,std::forward的查找

{
    // 如果在应用程序中定义了标准库中已经有得名字
    // 1.   根据一般的重载规则确定调用应该使用哪一个版本
    // 2.   应用程序根本不会执行函数得标准库版本

    // 对于move和forward函数,它们都是模板函数
    // 在标准库得定义中它们都接受一个右值引用 函数形参
    // 它们可以接受任何类型的参数
    // 如果我们的应用程序也定义了一个接受单一形参的move函数,则不该该形参时什么类型
    // 应用程序的函数都将和标准库的版本冲突,forward函数也是如此

    // 因此这两个函数的名字冲突要比其他标准库函数的冲突频繁的多
    // 同时两个函数也是执行特殊的操作。所以大多数的冲突也是无意中的

    // 这也就解释了我们为什么建议使用它的时候加上std
}

4).友元声明与实参相关的查找

{
    // 当类声明了一个友元时,该友元声明并没有使得友元本身可见
    // 然而,未声明的类或函数如果第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员
    namespace A {
        class C {
            // 两个友元,在友元声明之外没有其他的声明
            // 这些函数隐式地称为命名空间A的成员
            friend void f2();//除非另有声明,否则不会被找到
            friend void f(const C&);//根据实参相关的查找规则可以被找到
        };
    }
    // 注意到f和f2的差别,一个有类类型的参数,一个没有参数
    int main() {
        A::C obj;       
        f(obj);//正确,通过在A::C中的友元声明中找到A::f
        f2();//错误,
    }

    // f接受一个类类型的实参,而且f在C所属的米滚名空间中有隐式的声明,所以f可以被找到
    // 但是f2没有形参,无法被找到。
}

练习

  • 18.18,对于swap。是否有using std::swap;会影响它的匹配过程。
  1. 如果有using,直接匹配到using的版本。
  2. 如果没有,则按照上述的规则进行查找。注意,int是一个内置类型,没有特定版本的swap函数。于是它只能在常规作用域中查找。
  • 18.19,std::swap会直接使用标准库的版本,而不会查找特定版本的swap或者常规作用域中的其他swap。那以上的using

//4.重载和命名空间

1).using声明和指示,能将某一些函数添加到候选函数中去。
2).重载

{
    // -----------实参的查找
    // 我们将在每一个实参类以及实参类的基类,所属的命名空间中寻找候选函数。
    // 在这些命名空间中所有与被调用函数同名的函数都将被添加到候选函数集合中
    // 即使有一些函数在调用函数处不可见
    namespace NS {
        class Quote {/*.....*/}
        void display(const Quote&) {/*..........*/}
    }
    class Bulk_item : public NS::Quote {/*........*/}
    int main() {
        Bulk_item book1;
        display(bookl);
        return 0;
    }

    //我们传递给display的实参是类类型Bulk_item,因此该调用语句的候选函数不仅仅应该在调用语句所在的作用域中查找,而且也应该在Bulk_item以及基类Quote所属的命名空间中查找
    // 命名空间中声明的函数被添加到候选函数集合中。
    // --------------重载和using声明

    // using声明语句声明的是一个名字,而不是一个特定的函数
    using NS::Quote(int);//不能指定形参列表
    using NS::Quote;//正确。将所有的重载都声明了

    // 当我们为函数书写using时,该函数所有的版本都被引入到当前的作用域中
    // 一个using声明包含了重载函数的所有版本
    // 库的作者为某一项任务提供了好几个不同的版本的函数,允许用户选择性地忽略掉重载函数中的一部分但不是全部....p709

    // 一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数
    // using声明将为引入的名字添加额外的重载实例,扩展候选函数集的规模。
    // 如果一个using声明出现在局部作用域中,则引入的名字将会隐藏外层的相关声明
    // 如果using声明所在的作用域中已有一个函数与引入函数同名并且形参列表相同
    // 则该using声明将会引发错误。(派生类中,可以覆盖?)

    // -----------重载和using指示
    // using指示,将命名空间成员提升到外层作用域中,如果命名空间的某一个函数与 该命名空间所属作用域的函数?  同名,则命名空间中函数将被添加到重载函数集合
    namespace lib_R_us {
        extern void print(int);
        extern void print(double);
    }
    void print(const string &);
    using namespace lib_R_us;
    // 此时的print可选函数集合,包括三个
    void fooBar(int, ival) {
        print("value: ");//string
        print(ival);//int
    }
    // 和using声明不一样的是,引入名字和形参都一样的不会产生错误
    // 只要我们指明是哪一个版本即可(不论使用哪一个均加上前缀)

    // --------------跨越多个using指示的重载
    // 各个using指示的函数都是候选函数
}

/3.多重继承和虚继承

1).多重继承指的是,从多个直接基类产生派生类的能力。

  • 多重继承的派生类继承所有父类的属性。
  • 这将会产生,错综复杂的设计问题和实现问题。

//1.多重继承

1).具体要求详见继承。(p711)

  • 同一个直接基类只能出现一次
  • 直接基类个数没有限定
  • 不能是final,已经定义过
  • 访问说明符缺失时

2).多重继承的派生类从每一个基类(直接,间接)中继承状态。(p711)。

  • 派生类包含每一个基类的子对象,以及在本类中的非静态成员。

3).只能初始化直接基类。

  • 没有指出,则隐式地使用直接基类的 默认 构造函数进行初始化。
  • 直接基类的构造顺序与派生类表中的直接基类顺序一致,与初始值列表的直接基类构造函数顺序无关。
  • 构造时,是当一个直接基类完全构造完毕之后,再构造下一个直接接类,这是一个递归的定义。最后再构造自身,执行自身的函数体。

4).继承构造函数

  • 新标准中,允许派生类从它的一个或者几个基类中继承构造函数(自定义的),但是如果从多个基类中继承了相同的构造函数(即形参列表相同),程序将会产生错误。
{
    struct Base1 {
        Base1() = default;
        Base1(const std::string &);
        Base1(std::shared_ptr<int>);
    };
    struct Base2 {
        Base2() = default;
        Base2(const std::string &);
        Base2(int);
    };
    struct D1 : public Base1, public Base2 {
        using Base1::Base1;//从Base1中继承自定义的Base1
        using Base2::Base2;//..
    };
    // 上述的声明是错误的
    // 试图从两个基类中都继承D1::D1(const string&)

    // 如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它的自己版本
    struct D2 : public Base1, public Base2 {
        using Base1::Base1;
        using Base2::Base2;
        D2(const string &s) : Base1(s), Base2(s) { }
        D2() = default;     //一旦定义了自己的版本的构造函数
        // 默认构造函数就必须出现
    };
}

5).析构函数与多重继承

  • 和往常一样,派生类的析构函数只负责处理派生类本身分配的资源。
  • 析构函数和构造函数的调用顺序刚好相反。

6).多重继承的拷贝和移动操作

  • 与只有一个基类一样,如果一个派生类定义了自己的拷贝/移动构造操作,拷贝/移动赋值运算符。必须对所有直接基类调用相应的函数,就像构造函数一样?。
  • 只有当派生类使用的是合成的版本,才会自动对基类部分执行的相应操作。
  • 在合成的拷贝控制成员中,每一个基类分别使用自己的对应成员隐式地完成构造,赋值,销毁操作。
{
    Panda ying_ying("ying_ying");
    Panda ling_ling = ying_ying;    
    // 执行的过程和构造函数的模式完全一致
}

//2.类型转换与多个基类

1).派生类的任何一个间接,直接基类指针,或者引用都可以直接指向该派生类对象。

  • 编译器不会在派生类向基类的几种转换中进行比较和选择,在它看来,转到任意一种基类都一样好。因此,当我们重载函数只是基类的差异时(都是引用,const),将会发生二义性的错误。

2).名称的查找(与只有一个基类一样。)

  • 指针和引用的静态类型,决定我们可以使用哪些操作。例如,我们使用一个ZooAnimal指针,则只有定义在ZooAnimal类中的操作是可以使用的。

练习

  • 18.23,有误。所有的转化均可行。
  • 18.25,多个基类,可以定义一样的虚成员函数,析构函数都是虚函数。
  • 18.27,当多个基类有一样的成员时
  1. 使用类名::访问特定版本的成员
  2. 对于具有唯一性的继承而来的成员可以直接使用。

//3.多重继承下的类作用域

1).只有一个基类时,由于继承类的嵌套关系,名字的搜索就是从内到外,内层屏蔽外层。

  • 在多重继承中,相同的查找过程在所有直接基类(继承链条)中同时进行,如果名字在多个基类(多个继承链条)中被找到,则该名字的使用将具有二义性。
  • 对于一个派生类而言,从它的几个基类分别继承名字相同的成员是完全合法的,只不过在使用时需要明确指出它的版本。
  • 有时候,即使派生类继承的两个函数形参列表不一样,也可能发生错误。此时,即使同名的在一个类中是private,在另一个类中是public,或者protected也一样是可能发生错误的。
  • 愿意就是,编译器是先进行名字查找,再进行类型检查。如果发现了两个函数,就直接报错。
  • 解决方法就是,在该类中,定义一个覆盖的版本。
  • 指针和引用是否也是一样的?
{
    double Panda::max_weight() const {  
        return std::max(ZooAnimal::max_weight(), Endangered::max_weight());
    }
}

//4.虚继承

1).虽然派生列表中,直接基类的名字不能重复,但是一个派生类可以多次继承同一个类。

  1. 通过两个直接基类继承同一个类
  2. 直接继承一个基类,通过另一个直接基类再一次继承该类。
  • IO标准库中的istreamostream分继承了一个共同的名为base_ios的抽象基类。该抽象基类负责保存流的缓冲内容并管理流的条件条件状态。iostream是另外一个类,他从istreamostream直接继承而来,可以同时读写流的内容。因此iostream继承了base_ios两次。
  • 默认情况下,派生类中含有继承链上每一个类对应的子部分。如果一个类在派生过程中,出现了多次,则派生类中将包含该类的多个子对象。
  • 这样的默认情况对某些像iostream这样的类是行不通的。一个iostream肯定希望在同一个缓冲区中进行读写操作,也会要求条件状态能同时反应输出和输入操作的情况。如果一个iostream真的包含了两个base_ios,那么共享操作无法实现。

2).解决。c++语言引入虚继承。

  • 虚继承的目的是里那个某一个类做出声明,承诺愿意共享它的基类。其中共享的基类子对象称为**虚基类。**在这种机制下,不论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

3).新的Panda继承体系。

  • 必须在虚派生类的真实需求之前就完成了需派生的操作,两个均需要如此操作。(p718)
  • 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
  • 位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。
  • 实际中,虚继承的类层次是由一个人或者一个组一次性完成…个人设计的类大多不需要这样做…p718。

4).使用虚基类

{
    // 方式加一个关键字virtual
    class Raccoon : public virtual ZooAnimal {....};
    class Bear : virtual public ZooAnimal {....};

    // 顺序是随意的
    
    // 直接基类是否指定了虚基类,对该类的如何指定派生没有影响
    class Panda : public Bear, public Raccoon, PUblic Endangered {....};

    // Panda中只有一个ZooAnimal基类部分
}

5).支持向基类的常规类型转换

  • 不论是基类是不是虚基类,派生类对象都能绑定到基类的指针或者引用。

6).虚基类成员的可见性

  • 基类的成员可以直接被访问(因为只有唯一的一个基类子对象)。
  • 如若多条派生路径都覆盖了该名字,二义性。解决方法,在派生类中定义一个进行覆盖。
  • 如若只有一个条派生路径覆盖,没有二义性,派生类的比共享基类的优先级更高。

//5.构造函数与虚继承

1).**虚派生中,虚基类是由最底层的派生类初始化的。**例如,创建一个Panda对象时,Panda的构造函数独自控制ZooAnimal的初始化过程。

  • **原因,**按照普通的规则,虚基类将会被多次重复初始化。
  • 任何一个派生类都可能是最底层的派生类。**只要我们可以创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。**如何设计?
{
    Bear::Bear(string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Bear") {}
    Raccoon::Raccoon(string name, bool onExhibit) : ZooAnimal(name, onExhibit, "Raccoon") {}

    // 当创建一个Panda对象时,Panda位于派生类的最底层并由它负责初始化共享的ZooAnimal基类部分
    // 即使,ZooAnimal不是Panda的直接基类部分
    Panda::Panda(string name, bool onExhibit) : 
            ZooAnimal(name, onExhibit, "Panda"),
            Bear(name, onExhibit),
            Endangered(Endangered::critical),
            sleeping_flag(false) {}
}

2).虚继承的对象的构造方式

  • 含有虚基类的对象的构造顺序和一般的顺序稍有区别。
  1. 首先使用提供给最底层派生类构造函数的初始值,初始化该对象的虚基类子部分。
  2. 接下来才按照直接基类在派生列表的顺序进行初始化。
  • 如果没有显式地初始化虚基类,那么虚基类的默认构造函数会被调用,如果虚基类没有默认构造函数,将会报错。
  • 其他基类遇到虚基类就结束继续向上追溯,从该基类开始构造?
  • 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。如何实现?

3).构造函数和析构函数的次序

  • 一个类可以有多个虚基类。这些虚基类子对象按照它们在派生列表中出现的顺序从左往右一次构造。
{
    class Character {...};
    class BookCharacter : public Character {...};
    class ToyAnimal {....};
    class TeddyBear : public BookCharacter, Public Bear, public virtual ToyAnimal {...}
    // 编译器首先按照直接基类的声明顺序检查是否有虚基类,
    //故顺序如下
    ZooAnimal();
    ToyAnimal();
    Character();
    BookCharacter();
    Bear();
    TeddyBear();

    // 合成 的拷贝和移动操作的顺序完全一致
    //  销毁的顺序和构造的顺序刚刚好相反。
    // 栈。
}

练习

  • 18.29,虽然虚基类会先进行构造,但是在构造它的时候,必然会调用它的直接基类…
  1. 在该题中,意外地,有两个Class子对象。
  2. Class *pc = new Final();是错误的,因为ClassFinal的一个二义基类。
  3. 基类到派生类的转换,指针和对象都是不允许的。因为,派生类有基类的子对象。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-30 12:34:13  更:2021-07-30 12:35:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/3 3:12:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码