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

18.1 异常处理

18.2 命名空间

命名空间定义

  • 语法相关:
    • 只要能出现在全局作用域的声明就能置于命名空间中
    • 命名空间不能定义在函数或类内部
    • 每个命名空间是一个作用域。
    • 命名空间可以不连续,即同一命名空间可以定义为几个不同的部分,在多处出现
      • 在头文件中声明命名空间中的成员,在源文件中定义命名空间中的成员
    • 通常不将#include放在命名空间中,否则会将该头文件中的所有名字定义为该命名空间中的成员
  • 几种命名空间
    • 全局命名空间:使用::显式指明
    • 嵌套命名空间
    • 内联命名空间:无需使用该命名空间的前缀,通过外层命名空间就可以直接访问。
      • inline必须出现在命名空间第一次定义的地方,后续打开命名空间时可以不加inline
      • 程序代码更新版本时经常使用内联空间,当前版本放在内联空间中,历史版本放在非内联空间中,
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        
        /* 文件名:FifthEd.h */ 
        inline namespace FifthEd{ //定义第五版命名空间,是内联,使用时不需显式指定该空间的名字 
            class Query_base{/* 类的定义 */}; 
        }
        /* 文件名:FourthEd.h */ 
        namespace FourthEd{ //定义第四版命名空间 
            class Query_base{/* 类的定义 */}; 
        } 
        /* 文件名:cplusplus_primer.h */ 
        namespace cplusplus_primer{ //将上面两个命名空间嵌套进外层空间 
            #include"FifthEd.h" //引入头文件中的所有名字 
            #include"FourthEd.h" 
        } 
        /* 文件名:main.cc */ 
        #include"cplusplus_primer.h" 
        using cplusplus_primer::Query_base; //默认使用第五版中的成员 
        using cplusplus_primer::FourthEd::Query_base; //手动指定第四版中的成员
        
    • 未命名的命名空间
      • 未命名的命名空间可以在一个文件内不连续(是同一个命名空间),但是不可跨越文件(否则是两个无关的命名空间)
      • 未命名的命名空间中定义的变量拥有静态的声明周期:在第一次使用前创建,直到程序结束才销毁。
      • 如果头文件中定义了未命名的命名空间,则不同源文件中包含了该头文件后,该空间中的名字对应不同实体
      • 未命名的命名空间中的名字可以跨越到上一次作用域,因此定义在未命名的命名空间中的名字可以直接使用,不能对未命名的命名空间的成员使用作用域算符
      • 应用:未命名的命名空间取代文件中的静态声明
        • 原来将全局变量声明为static以转变为内部变量(C方式)
        • 现在将全局变量放在未命名的命名空间中(C++方式,原因见上述语法),但是此时全局变量仍然是外部的,

使用命名空间成员

  • 命名空间的别名:namespace new_name = old_name1::old_name2;
    • 一个命名空间可以有多个别名,但不能在未定义命名空间之前就声明别名
  • using声明:using my_namespace::mem;
    • 一次只能引入命名空间的一个成员
    • 声明的名字的作用域与using语句本身的作用域一致
    • 在类作用域中using声明只能声明基类成员
  • using指示:using nemespace my_namespace;
    • 引入命名空间中所有名字
    • using指示将命名空间注入到外层作用域,即将命名空间中所有名字出现在最近的外层作用域中(相当于using声明的外层作用域)
    • 不可出现在类作用域
  • 命名空间污染:
    • 使用了多个命名空间的using指示后,外层作用域中来自不同命名空间的名字可能发生冲突,这种冲突允许存在,但是使用时需要使用::明确指定版本
    • 在头文件中,不要在全局作用域中使用using声明/指示,最多在函数、命名空间中使用
    • using指示引发的二义性错误只有在使用冲突名字的地方才会被发现,难以定位bug
    • 尽量使用using声明而非using指示

类、命名空间与作用域

  • 名字查找的例外:给函数传递类类型对象/引用/指针时,先在常规的作用域中查找函数名,随后还会在实参类(及其基类)所属的命名空间中查找函数名。
    • 这个规则使得概念上作为接口一部分的非成员函数不需单独using声明就可被程序使用
    • 例子:std::cin>>str;表达式中,作用域中没有声明operator>>()函数,但是仍可以使用,这是因为在istream(实参std::cin所属类)和string(实参str所属类)所在的命名空间中进行了查找。否则需要显式声明:using std::operator>>,使用operator>>(std::cin, str);
  • 友元相关:
    • 当类声明友元时,还需要在类外给出友元的正式声明
    • 一个未声明的类/函数若第一次出现在友元声明中,则认为它是最近的外层命名空间的成员。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    namespace A{ 
        class C{ //这2个友元声明时还没有正式声明,认为它是最近的外层空间的成员,即隐式声明为空间A的成员 
            friend void f2(); //没有形参 
            friend void f(const C &); //接受C类型对象作为实参 
        }; 
    } 
    int main(){ 
        A::C cobj; 
        f(cobj); //对,f被隐式声明为A的成员,且实参决定会在A中查找函数f 
        f2(); //错,虽然f2被隐式声明为A的成员,但未显式指明 
    }
    

重载与命名空间

  • using声明
    • using声明语句声明的是一个名字,而非特定的函数,也就是包括该函数的所有版本,都被引入到当前作用域中。
    • using声明引入的函数将重载该声明语句所属作用域中已有的同名函数。
  • using指示
    • 若命名空间中函数名与外层作用域中函数同名,即使函数同名同参也不会报错,只需要使用时指明版本

18.3 多重继承与虚继承

多重继承

  • 可以从多个基类中继承构造函数,但是这些构造函数必须形参列表不同
    • 如果相同,则派生类必须为这种形参列表的构造函数定义自己的版本

类型转换与多个基类

  • 在派生类向基类的转换中,如果有多个基类,编译器不会进行比较,转换到任何基类一样好
  • 对象的指针/引用的静态类型决定了哪些成员可见

多重继承下的类作用域

  • 在派生类中使用了某个名字,则程序并行的在多个基类中查找名字
    • 派生类继承多个基类的同名成员合法,只是使用时需要::指明版本
    • 派生类只是引入潜在的二义性,如果不调用该重名的对象,则不会报错
    • 只有使用该重名对象时,才会产生二义性报错
      • 该名字在多个基类中是形参列表不同的函数
      • 该名字在一个基类中是private,而在另一个基类中是public/protected
      • 该名字在一个基类中直接找到,而在另一个基类的间接基类中找到
    • 避免这种二义性的方法是在派生类中再定义一次这个名字,覆盖基类名字,避免在基类中查找
  • 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性。

虚继承

  • 背景:菱形继承中,间接基类应该只有一个,如果不使用虚继承,则间接基类在派生类对象中有两个部分
  • 虚继承:令某个类做出声明,承诺愿意共享它的基类。被共享的基类子对象称为虚基类,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
    • 经常并不知道一个类是否会被继承多次,因此不知道由它而来的派生是否应该是虚派生
    • 实际编程中,位于中间层次的类将其继承基类的方式声明为虚继承并不会出问题。虚派生只影响从虚基类的派生类中进一步派生出的类,它不影响虚基类的派生类。
  • 语法相关:
    • 在派生列表中添加virtual,表示后续的派生类共享虚基类的同一份实例
    • 菱形继承:类B定义了成员x,D1和D2由B虚继承得到,D继承自D1和D2,则在D的作用域中,x通过两个基类都可见。若通过D的对象使用x,有几种可能:
      • 若D1和D2中都未定义x,则x被解析为B的成员,不存在二义性。因为只在虚基类中有定义
      • 若D1或D2其中之一定义了x,则x被解析为D1或D2的成员,不存在二义性。因为D1和D2是派生类,位于内层作用域,优先级更高
      • 若D1和D2中都定义了x,则直接访问x时是二义性。因为D1和D2的优先级相同
    • 解决二义性最好的方法就是在派生类中为成员自定义新的实例

构造函数与虚继承

  • 在虚派生中,虚基类由最终的派生类在其构造函数初值列表中初始化(越过了继承链),而非由其直接派生类初始化,否则被重复初始化
  • 只要创建了虚基类的派生类对象,该派生类的构造函数就会越过继承链初始化虚基类
  • 含有虚基类的对象的构造顺序:
    • 首先使用提供给最终派生类构造函数的初值来初始化虚基类(否则虚基类默认初始化)
    • 一个类可有多个虚基类,这些虚基类的初始化顺序是它们在派生列表中的顺序
    • 然后按照直接基类在派生列表中的顺序初始化非虚基类
  • 构造派生类时,编译器按照直接基类的声明顺序对其依次检查,若基类中含有虚基类,则先构造虚基类,然后按照声明顺序逐一构造其他非虚基类