影视网站建设源码,最新网页游戏开服时间表,最近一周热点回顾,网站营销不同阶段的网站分析目标文章目录用于大型程序的工具18.1异常处理18.1.1抛出异常栈展开栈展开过程中对象被自动销毁析构函数与异常异常对象18.1.2捕获异常查找匹配的处理代码重新抛出捕获所有异常的处理代码18.1.3函数try语句块与构造函数18.1.4noexcept异常说明违反异常说明异常说明的实参noexcept运算…
文章目录用于大型程序的工具18.1异常处理18.1.1抛出异常栈展开栈展开过程中对象被自动销毁析构函数与异常异常对象18.1.2捕获异常查找匹配的处理代码重新抛出捕获所有异常的处理代码18.1.3函数try语句块与构造函数18.1.4noexcept异常说明违反异常说明异常说明的实参noexcept运算符异常说明与指针、虚函数和拷贝控制18.1.5异常类层次18.2命名空间18.2.1命名空间定义每个命名空间都是一个作用域命名空间可以是不连续的定义命名空间成员模板特例化内联命名空间未命名的命名空间18.2.2使用命名空间成员命名空间的别名using声明扼要概述using指示using指示与作用域头文件与using声明或指示18.2.3类、命名空间与作用域实参相关的查找与类类型形参查找与std;:move和std::forward友元声明与实参相关的查找18.2.4重载与命名空间与实参相关的查找与重载重载与using声明重载与using指示跨越多个using指示的重载18.3多重继承与虚继承18.3.1多重继承多重继承的派生类从每个基类中继承状态派生类构造函数初始化所有基类继承的构造函数与多重继承多重继承的派生类的拷贝与移动操作18.3.2类型转换与多个基类基于指针类型或引用类型的查找18.3.3多重继承下的类作用域18.3.4虚继承使用虚基类支持向基类的常规类型转换虚基类成员的可见性18.3.5构造函数与虚继承虚继承的对象的构造方式构造函数与析构函数的次序用于大型程序的工具
18.1异常处理 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。 18.1.1抛出异常 当执行一个throw时跟在throw后面的语句将不再被执行。相反程序的控制权从throw转移到与之匹配的catch模块。该catch可能是同一个函数中的局部catch也可能位于直接或间接调用了发生异常的函数的另一个函数中。此时 沿着调用链的函数可能会提早退出。一旦程序开始执行异常处理代码则沿着调用链创建的对象将被销毁。 throw的用法有点类似于return语句它通常作为条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。 栈展开 栈展开过程沿着嵌套函数的调用链不断查找直到找到了与异常匹配的catch子句为止或者也可能一直没找到匹配的catch则退出主函数后查找过程终止(terminate)。 栈展开过程中对象被自动销毁 如果在栈展开过程中退出了某个块编译器将负责确保在这个块中创建的对象能被正确地销毁。如果某个局部对象的类型是类类型则该对象的析构函数将被自动调用。编译器在销毁内置类型的对象时不需要做任何事情。 如果异常发生在构造函数中即使某个对象只构造了一部分也要确保已构造的成员能被正确地销毁。 类似的异常也可能发生在数组或标准库容器的元素初始化过程中此时应该确保已经构造的这部分元素被正确地销毁。 析构函数与异常 在栈展开的过程中运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常并且析构函数自身没能捕获到该异常则程序将被终止(terminate)。因此如果析构函数需要执行某个可能抛出异常的操作则该操作应该被放置在一个try语句块中并且在析构函数内部得到处理。 在实际的编程过程中因为析构函数仅仅是释放资源所以它不太可能抛出异常。所有标准库类型都能确保它们的析构函数不会引发异常。 异常对象 编译器使用异常抛出表达式来对异常对象进行拷贝初始化。如果该表达式是类类型的话则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。如果该表达式是数组类型或函数类型则表达式将被转换成与之对应的指针类型。 当抛出一条表达式时该表达式的静态编译时类型决定了异常对象的类型。如果一条throw表达式解引用一个基类指针而该指针实际指向的是派生类对象则抛出的对象将被切掉一部分只有基类部分被抛出。 18.1.2捕获异常 如果catch无须访问抛出的表达式的话则可以忽略捕获形参的名字。 声明的类型必须是完全类型它可以是左值引用但不能是右值引用。如果catch的参数类型是非引用类型则该参数是异常对象的一个副本相反如果参数是引用类型则该参数是异常对象的一个别名。 如果catch的参数是基类类型则可以使用其派生类类型的异常对象对其进行初始化。此时如果catch的参数是非引用类型则异常对象将被切掉一部分。另一方面如果catch的参数是基类的引用则该参数将以常规方式绑定到异常对象上。 需要注意的是异常声明的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型则catch无法使用派生类特有的任何成员。 通常情况下如果catch接受的异常与某个继承体系有关则最好将该catch的参数定义成引用类型。 查找匹配的处理代码 越是专门的catch越应该置于整个catch列表的前端所以当程序使用具有继承关系的多个异常时派生类异常的处理代码应该出现在基类异常的处理代码之前。 与实参和形参的匹配规则相比异常和catch异常声明的匹配规则受到更多限制 允许从非常量向常量的类型转换一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句。允许从派生类向基类的类型转换。数组被转换成指向数组(元素)类型的指针函数被转换成指向该函数类型的指针。 重新抛出 有时一个单独的catch不能完整地处理某个异常。在执行了某些校正操作之后当前的catch可能会决定由调用链更上一层的函数接着处理异常 // 只能出现在catch语句或catch语句直接或间接调用的函数之内如果在处理代码
// 之外的区域遇到了空throw语句编译器将调用terminate。
throw;很多时候catch语句会改变其参数的内容。如果在改变了参数的内容后catch语句重新抛出异常则只有当catch异常声明是引用类型时对参数所做的改变才会被保留并继续传播 catch (my_error eObj) { // 引用类型eObj.status errCodes::severeErr; // 修改了异常对象throw; // 异常对象的status成员是severeErr
} catch (other_error eObj) { // 非引用类型eObj.status errCodes::badErr; // 只修改了异常对象的局部副本throw; // 异常对象的status成员没有改变
}捕获所有异常的处理代码 有时希望不论抛出的异常是什么类型程序都能统一捕获它们。 void manip() {try {// 这里的操作将引发并抛出一个异常} catch(...) {// 处理异常的某些特殊操作throw;}
}catch(...)通常与重新抛出语句一起使用其中catch执行当前局部能完成的工作随后重新抛出异常。catch(...)既能单独出现也能与其他几个catch语句一起出现此时catch(...)必须在最后的位置。 18.1.3函数try语句块与构造函数 构造函数在进入其函数体之前首先执行初始值列表因此此时构造函数体内的catch语句无法处理相关的异常。所以必须将构造函数写成函数try语句块的形式 templatetypename T
BlobT::Blob(std::initializer_listT il) try : data(std::make_sharedstd::vectorT(il)) {/* 空函数体 */
} catch(const std::bad_alloc e) {handle_out_of_memory(e);
}值得注意的是在初始化构造函数的参数时也可能发生异常这样的异常不属于函数try语句块的一部分。该异常属于调用表达式的一部分并将在调用者所在的上下文中处理。 18.1.4noexcept异常说明 在c11新标准中可以通过提供noexcept说明指定某个函数不会抛出异常 void recoup(int) noexcept; // 不会抛出异常
void alloc(int); // 可能抛出异常noexcept说明要么出现在函数的所有声明语句和定义语句中要么一次也不出现。该说明应该在函数的尾置返回类型之前。可以在函数指针的声明和定义中指定noexcept。在typedef或类型别名中则不能出现noexcept。在成员函数中noexcept说明符需要跟在const及引用限定符之后而在final、override或虚函数的0之前。 违反异常说明 编译器并不会在编译时检查noexcept说明。实际上如果一个函数在说明了noexcept的同时又含有throw语句或者调用了可能抛出异常的其他函数编译器将顺利通过并不会因为这种违反异常说明的情况而报错 void f() noexcept {throw exception(); // 违反了异常说明
}此时程序会调用terminate以确保遵守不在运行时抛出异常的承诺。因此noexcept可以用在两种情况下一是确认函数不会抛出异常二是根本不知道该如何处理异常。 指明某个函数不会抛出异常可以令该函数的调用者不必再考虑如何处理异常。无论是函数确实不抛出异常还是程序被终止调用者都无须为此负责。 异常说明的实参 noexcept说明符接受一个可选的实参该实参必须能转换为bool类型如果实参是true则函数不会抛出异常如果实参是false则函数可能抛出异常 void recoup(int) noexcept(true); // recoup不会抛出异常
void alloc(int) noexcept(false); // alloc可能抛出异常noexcept运算符 noexcept说明符的实参常常与noexcept运算符混合使用。 noexcept运算符是一个一元运算符返回值是一个bool类型的右值常量表达式用于表示给定的表达式是否会抛出异常不会求其运算对象的值 noexcept(recoup(i)) // 如果recoup不抛出异常则结果为true否则结果为false。// 更普通的形式是
noexcept(e)
// 当e调用的所有函数都做了不抛出说明且e本身不含有throw语句时上述表达式为true
// 否则noexcept(e)返回false。// 可以使用noexcept运算符得到如下的异常说明
void f() noexcept(noexcept(g())); // f和g的异常说明一致异常说明与指针、虚函数和拷贝控制 函数指针及该指针所指的函数必须具有一致的异常说明。即如果为某个指针做了不抛出异常的声明则该指针只能指向不抛出异常的函数。相反如果显式或隐式地说明了指针可能抛出异常则该指针可以指向任何函数即使是承诺了不抛出异常的函数也可以 // recoup和pf1都承诺不会抛出异常
void (*pf1)(int) noexcept recoup;
// 正确recoup不会抛出异常pf2可能抛出异常二者之间互不干扰。
void (*pf2)(int) recoup;pf1 alloc; // 错误alloc可能抛出异常但是pf1已经说明了它不会抛出异常。
pf2 alloc; // 正确pf2和alloc都可能抛出异常。如果一个虚函数承诺了它不会抛出异常则后续派生出来的虚函数也必须做出同样的承诺与之相反如果基类的虚函数允许抛出异常则派生类的对应函数既可以允许抛出异常也可以不允许抛出异常 class Base {
public:virtual double f1(double) noexcept; // 不会抛出异常virtual int f2() noexcept(false); // 可能抛出异常virtual void f3(); // 可能抛出异常
};class Derived : public Base {
public:double f1(double); // 错误Base::f1承诺不会抛出异常。int f2() noexcept(false); // 正确与Base::f2的异常说明一致。void f3() noexcept; // 正确Derived的f3做了更严格的限定这是允许的。
};当编译器合成拷贝控制成员时同时也生成一个异常说明 如果对所有成员和基类的所有操作都承诺了不会抛出异常则合成的成员是noexcept的。如果合成成员调用的任意一个函数可能抛出异常则合成的成员是noexcept(false)。如果定义了一个析构函数但是没有为它提供异常说明则编译器将合成一个。合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致。 18.1.5异常类层次 在这些类中what负责返回用于初始化异常对象的信息。因为what是虚函数所以当捕获基类的引用时对what函数的调用将执行与异常对象动态类型对应的版本。 // 为某个书店应用程序设定的异常类
class out_of_stock : public std::runtime_error {
public:explicit out_of_stock(const std::string s) : std::runtime_error(s) {}
};class isbn_mismatch : public std::logic_error {
public:explicit isbn_mismatch(const std::string s) : std::logic_error(s) {}isbn_mismatch(const std::string s, const std::string lhs, const std::string rhs) : std::logic_error(s), left(lhs), right(rhs) {}const std::string left, right;
};// 如果参与加法的两个对象并非同一本书籍则抛出一个异常。
Sales_data Sales_data::operator(const Sales_data rhs) {if (isbn() ! rhs.isbn()) {throw isbn_mismatch(wrong isbns, isbn(), rhs.isbn());}units_sold rhs.units_sold;revenue rhs.revenue;return *this;
}// 使用之前设定的书店程序异常类
Sales_data item1, item2, sum;
while (cin item1 item2) { // 读取两条交易信息try {sum item1 item2; // 计算它们的和// 此处使用sum} catch(const isbn_mismatch e) {cerr e.what() : left isbn( e.left ) right isbn( e.right ) endl;}
}18.2命名空间
18.2.1命名空间定义
namespace cplusplus_primer {// ...
} // 无须分号与块类似。命名空间的名字必须在定义它的作用域内保持唯一。 每个命名空间都是一个作用域 定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须明确指出所用的名字属于哪个命名空间 cplusplus_primer::Query q cplusplus_primer::Query(hello);命名空间可以是不连续的
// 可能是定义了一个名为nsp的新命名空间也可能是
// 为已经存在的命名空间添加一些新成员。
namespace nsp {// ...
}这种特性使得可以将几个独立的接口和实现文件组成一个命名空间。此时命名空间的组织方式类似于管理自定义类及函数的方法 命名空间的一部分成员的作用是定义类以及声明作为类接口的函数及对象则这些成员应该置于头文件中这些头文件将被包含在使用了这些成员的文件中。命名空间成员的定义部分则置于另外的源文件中。 在程序中某些实体只能定义一次如非内联函数、静态数据成员、变量等命名空间中定义的名字也需要满足这一要求可以通过上面的方式组织命名空间并达到目的。这种接口和实现分离的机制确保所需的函数和其他名字只定义一次而只要是用到这些实体的地方都能看到对于实体名字的声明。 // 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出现在打开命名空间的操作之前
#include Sales_data.h
namespace cplusplus_primer {// Sales_data成员及重载运算符的定义可以直接使用名字此时无须前缀。
}// user.cpp
#include Sales_data.h
int main() {using cplusplus_primer::Sales_data;Sales_data trans1, trans2;// ...return 0;
}这种程序的组织方式提供了开发者和库用户所需的模块性。每个类仍组织在自己的接口和实现文件中一个类的用户不必编译与其他类相关的名字。库的开发者可以分别实现每一个类相互之间没有干扰。 定义命名空间成员 可以在命名空间定义的外部定义该命名空间的成员但是这样的定义必须出现在所属命名空间的外层空间中而不能在一个不相关的作用域中。 // 命名空间之外定义的成员必须使用含有前缀的名字一旦看到含有完整前缀的名字
// 就可以确定该名字位于命名空间的作用域内因此可以直接使用该命名空间的其他成员。
cplusplus_primer::Sales_data
cplusplus_primer::operator(const Sales_data lhs, const Sales_data rhs) {// ...
}模板特例化 模板特例化必须定义在原始模板所属的命名空间中 // 必须将模板特例化声明成std的成员
namespace std {template struct hashSales_data;
}// 在std中添加了模板特例化的声明后就可以在命名空间std的外部定义它了。
template struct std::hashSales_data {size_t operator()(const Sales_data ) const {return hashstring()(s.bookNo) ^hashunsigned()(s.units_sold) ^hashdouble()(s.revenue);}// 其他成员保持一致
}内联命名空间 C11新标准引入内联命名空间中的名字可以被外层命名空间直接使用。当应用程序的代码在一次发布和另一次发布之间发生了改变时常常会用到内联命名空间。 // 必须出现在第一次定义的地方后续可写可不写。
inline namespace FifthEd {// 该命名空间表示第5版的代码
}namespace FifthEd { // 隐式内联class Query_base { /* ... */ };// 其他与Query有关的声明
}namespace FourthEd {class Item_base { /* ... */ };class Query_base { /* ... */ };// 第4版用到的其他代码
}// 因为FifthEd是内联的所以形如cplusplus_primer::的代码
// 可以直接获得FifthEd的成员。如果想使用早起版本的代码则
// 必须像其他嵌套的命名空间一样加上完整的外层命名空间名字。
namespace cplusplus_primer {#include FifthEd.h#include FourthEd.h
}未命名的命名空间 未命名的命名空间是指关键字namespace后紧跟花括号括起来的一系列声明语句。其中定义的变量拥有静态生命周期它们在第一次使用前创建并且直到程序结束才销毁。 一个未命名的命名空间可以在某个给定的文件内不连续但是不能跨越多个文件。每个文件定义自己的未命名的命名空间如果两个文件都含有未命名的命名空间则这两个空间互相无关。因此如果一个头文件定义了未命名的命名空间则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。 定义在未命名的命名空间中的名字可以直接使用毕竟找不到什么名字来限定它们同样的也不能对未命名的命名空间的成员使用作用域运算符。 未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外层作用域中则该命名空间中的名字一定要与全局作用域中的名字有所区别 int i; // i的全局声明
namespace {int i;
}
// 二义性i的定义既出现在全局作用域中又出现在未嵌套的未命名的命名空间中。
i 10;namespace local {namespace {int i;}
}
// 正确定义在嵌套的未命名的命名空间中的i与全局作用域中的i不同。
local::i 42;未命名的命名空间取代文件中的静态声明 在标准c引入命名空间的概念之前程序需要将名字声明成static的以使得其对于整个文件有效。这样的做法是从c语言继承而来的。在c语言中声明为static的全局实体在其所在的文件外不可见。 在文件中进行静态声明的做法已经被c标准取消了现在的做法是使用未命名的命名空间。 18.2.2使用命名空间成员
命名空间的别名
// 不能在命名空间还没有定义前就声明别名。
// 别名也可以指向一个嵌套的命名空间。
namespace primer cplusplus_primer;using声明扼要概述 一条using声明语句一次只引入命名空间的一个成员可以清楚地知道程序中所用的到底是哪个名字。 using声明的有效范围从声明的地方开始一直到其所在的作用域结束为止。在此过程中外层作用域的同名实体将被隐藏。 using指示 using指示无法控制哪些名字是可见的因为所有名字都是可见的。简写的名字从using指示开始一直到其所在的作用域结束都能使用。 using指示与作用域 using声明的名字的作用域与语句本身的作用域一致从效果上看就好像为命名空间的成员在当前作用域内创建了一个别名一样。 而using指示具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。通常情况下命名空间中会含有一些不能出现在局部作用域中的定义因此using指示一般被看作是出现在最近的外层作用域中。 // 命名空间A和函数f定义在全局作用域中
namespace A [int i, j;
}void f() {using namespace A; // 把A中的名字注入到全局作用域中cout i * j endl; // 使用命名空间A中的i和j// ...
}namespace blip {int i 16, j 15, k 23;// 其他声明
}int j 0; // 正确blip的j隐藏在命名空间中。void manip() {// using指示blip中的名字被添加到全局作用域中。using namespace blip; // 如果使用了j则将在::j和blip::j之间产生冲突。i; // 将blip::i设定为17j; // 二义性错误是全局的j还是blip::j::j; // 正确将全局的j设定为1。blip::j; // 正确将blip::j设定为16。int k 97; // 当前局部的k隐藏了blip::kk; // 将当前局部的k设定为98
}头文件与using声明或指示 头文件如果在其顶层作用域中含有using指示或声明则会将名字注入到所有包含了该头文件的文件中。 通常情况下头文件应该只负责定义接口部分的名字而不定义实现部分的名字。因此头文件最多只能在它的函数或命名空间内使用using指示或using声明。 避免using指示 using指示引发的二义性错误只有在使用了冲突名字的地方才能被发现而**using声明引起的二义性问题在声明处就能发现**。 using指示也并非一无是处例如在命名空间本身的实现文件中就可以使用using指示。 18.2.3类、命名空间与作用域 对命名空间内部名字的查找遵循常规的查找规则即由内向外依次查找每个外层作用域。只有位于开放的块中且在使用点之前声明的名字才被考虑 namespace A {int i;namespace B {int i; // 在B中隐藏了A::iint j;int f1() {int j; // j是f1的局部变量隐藏了A::B::jreturn i; // 返回B::i}} // 命名空间B结束此后B中定义的名字不再可见。int f2() {return j; // 错误j没有被定义。}int j i; // 用A::i进行初始化
}对于命名空间中的类来说常规的查找规则仍然适用当成员函数使用某个名字时首先在该成员中查找然后在类中查找(包括基类)接着在外层作用域中查找。可以从函数的限定名推断出查找名字时检查作用域的次序限定名以相反次序指出被查找的作用域。 namespace A {int i;int k;class C1 {public:C1() : i(0), j(0) {} // 正确初始化C1::i和C1::j。int f1() { return k; } // 返回A::kint f2() { return h; } // 错误h未定义。int f3();private:int i; // 在C1中隐藏了A::iint j;};int h i; // 用A::i进行初始化
}
// 成员f3定义在C1和命名空间A的外部
int A::C1::f3() { return h; } // 正确返回A::h。实参相关的查找与类类型形参
std::string s;
// 不用std::限定符和using声明就可以调用operator
std::cin s;
// 等价于
operator(std::cin, s);
// 首先在当前作用域中寻找合适的函数接着查找输出语句的外层作用域。
// 随后因为表达式的形参是类类型的所以编译器还会查找cin和s的类
// 所属的命名空间即std。当给函数传递一个类类型的对象时除了在常规的作用域查找外还会查找实参类所属的命名空间对于传递类的引用或指针的调用同样有效。 因此允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。 查找与std;:move和std::forward 通常情况下如果在应用程序中定义了一个标准库中已有的名字则要么根据一般的重载规则确定某次调用应该执行函数的哪个版本要么应用程序根本就不会执行函数的标准库版本。 由于标准库中move和forward都是模板函数且都接受一个右值引用的函数形参(可以匹配任何类型)因此如果应用程序也定义了一个接受单一形参的move/forward函数则不管该形参是什么类型都将与标准库的版本冲突。 因此move/forward的名字冲突要比其他标准库函数的冲突频繁得多所以建议最好使用带限定语的完整版本。 友元声明与实参相关的查找 当类声明了一个友元时该友元声明并没有使得友元本身可见。然而一个另外的未声明的类或函数如果第一次出现在友元声明中则认为它是最近的外层命名空间的成员 namespace A {class C {// 两个友元在友元声明之外没有其他的声明// 这些函数隐式地成为命名空间A的成员。friend void f2(); // 除非另有声明否则不会被找到。friend void f(const C ); // 根据实参相关的查找规则可以被找到};
}int main() {A::C cobj;f(cobj); // 正确通过在A::C中的友元声明找到A::f。f2(); // 错误A::f2没有被声明。
}18.2.4重载与命名空间
与实参相关的查找与重载 对于接受类类型实参的函数来说将在每个实参类(以及实参类的基类)所属的命名空间中搜寻候选函数。在这些命名空间中所有与被调函数同名的函数都将被添加到候选集当中即使其中某些函数在调用语句处不可见也是如此 namespace NS {class Quote { /* ... */ };void display(const Quote ) { /* ... */ }
}
// Bulk_item的基类声明在命名空间NS中
class Bulk_item : public NS::Quote { /* ... */ };
int main() {Bulk_item book1;display(book1);return 0;
}重载与using声明 using声明语句声明的是一个名字而非一个特定的函数 using NS::print(int); // 错误不能指定形参列表。
using NS::print; // 正确using声明只声明一个名字。当为函数书写using声明时该函数的所有版本都被引入到当前作用域中。 一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。如果using声明出现在局部作用域中则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域中已经有一个函数与新引入的函数同名且形参列表相同则该using声明将引发错误。除此之外using声明将为引入的名字添加额外的重载实例并最终扩充候选函数集的规模。 重载与using指示 using指示将命名空间的成员提升到外层作用域中如果命名空间的某个函数与该命名空间所属作用域的函数同名则命名空间的函数将被添加到重载集合中 namespace libs_R_us {extern void print(int);extern void print(double);
}
// 普通的声明
void print(const std::string );
// 这个using指示把名字添加到print调用的候选函数集
using namespace libs_R_us;
// print调用此时的候选函数集包括
// libs_R_us的print(int)
// libs_R_us的print(double)
// 显式声明的print(const std::string )
void fooBar(int ival) {print(Value: ); // 调用全局函数print(const std::string )print(ival); // 调用libs_R_us::print(int)
}对于using指示来说引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时只要指明调用的是命名空间中的函数版本还是当前作用域的版本即可。 跨越多个using指示的重载 如果存在多个using指示则来自每个命名空间的名字都会成为候选函数集的一部分 namespace AW {int print(int);
}
namespace Primer {double print(double);
}
// using指示从不同的命名空间中创建了一个重载函数集合
using namespace AW;
using namespace Primer;long double print(long double);int main() {print(1); // 调用AW::print(int)print(3.1); // 调用Primer::print(double)return 0;
}18.3多重继承与虚继承 多重继承是指从多个直接基类产生派生类的能力。多重继承的派生类继承了所有父类的属性。 18.3.1多重继承
class Bear : public ZooAnimal { /* ... */ };
class Panda : public Bear, public Endangered { /* ... */ };需要注意的是在某个给定的派生列表中同一个基类只能出现一次。 多重继承的派生类从每个基类中继承状态 在多重继承关系中派生类的对象包含有每个基类的子对象。 派生类构造函数初始化所有基类 构造一个派生类的对象将同时构造并初始化它的所有基类子对象。多重继承的派生类的构造函数初始值也只能初始化它的直接基类 // 显式地初始化所有基类
// 首先初始化ZooAnimal其次是Bear然后是Endangered最后是Panda。
Panda::Panda(std::string name, bool onExhibit): Bear(name, onExhibit, Panda), Endangered(Endangered::critical) {}
// 隐式地使用Bear的默认构造函数初始化Bear子对象
Panda::Panda(): Endangered(Endangered::critical) {}基类的构造顺序与派生列表中基类的出现顺序保持一致而与派生类构造函数初始值列表中基类的顺序无关。析构函数的调用顺序则与构造函数相反。 继承的构造函数与多重继承 在c11新标准中允许派生类从它的一个或几个基类中继承构造函数。但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同)则程序将产生错误。此时必须定义自己的版本 struct Base1 {Base1() default;Base1(const std::string );Base1(std::shared_ptrint);
};
struct Base2 {Base2() default;Base2(const std::string );Base2(int);
};
// D1试图从两个基类中都继承D1::D1(const string )
// 如果不定义自己的版本将引发错误。
struct D1 : public Base1, public Base2 {using Base1::Base1; // 从Base1继承构造函数using Base2::Base2; // 从Base2继承构造函数// D2必须自定义一个接受string的构造函数D2(const string s) : Base1(s), Base2(s) {}D2 default; // 一旦D2定义了它自己的构造函数则必须出现。
};多重继承的派生类的拷贝与移动操作 与单继承一样多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符则必须在完整的对象上执行拷贝、移动或赋值操作。 只有当派生类使用的是合成版本的拷贝、移动或赋值成员时才会自动对其基类部分执行这些操作。在合成的拷贝控制成员中每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。 Panda ying_yang(ying_yang);
// 将调用Bear的拷贝构造函数后者又在执行自己的拷贝任务之前先调用
// ZooAnimal的拷贝构造函数。一旦ling_ling的Bear部分构造完成接着
// 就会调用Endangered的拷贝构造函数来创建对象相应的部分。最后执行
// Panda的拷贝构造函数。合成的移动构造函数/拷贝赋值运算符类似。
Panda ling_ling ying_yang; // 使用拷贝构造函数18.3.2类型转换与多个基类 可以令某个可访问基类的指针或引用直接指向一个派生类对象 // 接受Panda的基类引用的一系列操作
void print(const Bear );
void highlight(const Endangered );
ostream operator(ostream , const ZooAnimal );
Panda ying_yang(ying_yang);
print(ying_yang); // 把一个Panda对象传递给一个Bear的引用
highlight(ying_yang); // 把一个Panda对象传递给一个Endangered的引用
cout ying_yang endl; // 把一个Panda对象传递给一个ZooAnimal的引用编译器不会在派生类向基类的几种转换中进行比较和选择因为在它看来转换到任意一种基类都一样好。 基于指针类型或引用类型的查找 对象、指针和引用的静态类型决定了能够使用哪些成员。 Bear *pb new Panda(ying_yang);
pb-print(); // 正确Panda::print()。
pb-cuddle(); // 错误不属于Bear的接口。
pb-highlight(); // 错误不属于Bear的接口。
delete pb; // 正确Panda::~Panda()。Endangered *pe new Panda(ying_yang);
pe-print(); // 正确Panda::print()。
pe-toes(); // 错误不属于Endangered的接口。
pe-cuddle(); // 错误不属于Endangered的接口。
pe-highlight(); // 正确Panda::highlight()。
delete pe; // 正确Panda::~Panda()。18.3.3多重继承下的类作用域 在多重继承的情况下查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到则对该名字的使用将具有二义性。 对于一个派生类来说从它的几个基类中分别继承名字相同的成员是完全合法的只不过在使用这个名字时必须明确指出它的版本。 // 如果ZooAnimal和Endangered都定义了名为max_weight的成员并且
// Panda没有定义该成员则下面的调用是错误的。此时需要指出所调用
// 的版本ZooAnimal::max_weight或者Endangered::max_weight。
double d ying_yang.max_weight();一种更复杂的情况是有时即使派生类继承的两个函数形参列表不同也可能引发错误。此时即使函数在一个类中是私有的而在另一个类中是公有的或受保护的同样也可能发生错误。 和往常一样先查找名字后进行类型检查。当编译器在两个作用域中同时发现了相同的成员时将直接报告一个调用二义性的错误。 要想避免潜在的二义性最好的办法是在派生类中为该函数定义一个新版本。 double Panda::max_weight() const {return std::max(ZooAnimal::max_weight(), Endangered::max_weight());
}18.3.4虚继承 尽管在派生列表中同一个基类只能出现一次但实际上派生类可以多次继承同一个类。 在默认情况下派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次则派生类中将包含该类的多个子对象。 因此对于形如iostream这样的类显然是行不通的。一个iostream对象肯定希望在同一个缓冲区中进行读写操作也会要求条件状态能同时反映输入和输出操作的情况。假如在iostream对象中真的包含了base_ios的两份拷贝则共享行为就无法实现了。 在c中通过虚继承的机制解决这样的问题。虚继承的目的是令某个类做出声明承诺愿意共享它的基类。其中共享的基类子对象称为虚基类。在这种机制下不论虚基类在继承体系中出现了多少次在派生类中都只包含唯一一个共享的虚基类子对象。 虚继承的一个不太直观的特征必须在虚派生的真实需求出现前就已经完成虚派生的操作。 在实际的编程过程中位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下使用虚继承的类层次是由一个人或一个项目组一次性设计完成的。对于一个独立开发的类来说很少需要基类中的某一个是虚基类况且新基类的开发者也无法改变已存在的类体系。 使用虚基类 在派生列表中添加关键字virtual后续的派生类当中共享虚基类的同一份实例 // 关键字public和virtual的顺序随意
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };
// 如果某个类指定了虚基类则该类的派生仍按常规方式进行
class Panda : public Bear,public Raccoon, public Endangered { /* ... */ };支持向基类的常规类型转换
// 不论基类是不是虚基类派生类对象都能被可访问基类的指针或引用操作。
void dance(const Bear );
void rummage(const Raccoon );
ostream operator(ostream , const ZooAnimal );
Panda ying_yang;
dangce(ying_yang); // 正确把一个Panda对象当成Bear传递。
rummage(ying_yang); // 正确把一个Panda对象当成Raccoon传递。
cout ying_yang; // 正确把一个Panda对象当成ZooAnimal传递。虚基类成员的可见性 因为在每个共享的虚基类中只有唯一一个共享的子对象所以该基类的成员可以被直接访问并且不会产生二义性。此外如果虚基类的成员只被一条派生路径覆盖则仍然可以直接访问这个被覆盖的成员。但是如果成员被多余一个基类覆盖则一般情况下派生类必须为该成员自定义一个新的版本。 18.3.5构造函数与虚继承 在虚派生中虚基类是由最低层的派生类初始化的。之所以这样设计不妨假设当以普通规则处理初始化任务时会发生什么情况。在此例中虚基类将会在多条继承路径上被重复初始化。 当然继承体系中的每个类都可能在某个时刻成为最低层的派生类。只要能创建虚基类的派生类对象该派生类的构造函数就必须初始化它的虚基类。 // 当创建一个Bear(或Raccoon)的对象时它已经位于派生的最低层
// 因此Bear(或Raccoon)的构造函数将直接初始化其ZooAnimal基类部分
Bear:Bear(std::string name, bool onExhibit) : ZooAnimal(name, onExhibit, Bear) {}
Raccoon::Raccoon(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, Raccoon) {}
// 当创建一个Panda对象时其位于派生的最低层并由它负责初始化共享的ZooAnimal基类部分。
// 即使ZooAnimal不是Panda的直接基类Panda的构造函数也可以初始化ZooAnimal。
Panda::Panda(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, Panda),Bear(name, onExhibit),Raccoon(name, onExhibit),Endangered(Endangered::critical),sleeping_flag(false) {}虚继承的对象的构造方式 首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。 虚基类总是先于非虚基类构造与它们在继承体系中的次序和位置无关。 构造函数与析构函数的次序 一个类可以有多个虚基类。此时这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造 class Character { /* ... */ };
class BookCharacter : public Character { /* ... */ };
class ToyAnimal { /* ... */ };
class TeddyBear : public BookCharacter,public Bear, public virtual ToyAnimal { /* ... */ };编译器按照直接基类的声明顺序对其依次进行检查以确定其中是否含有虚基类。如果有则先构造虚基类然后按照声明的顺序逐一构造其他非虚基类。 // 创建一个TeddyBear对象需要按照如下次序调用这些构造函数
ZooAnimal(); // Bear的虚基类
ToyAnimal(); // 直接虚基类
Character(); // 第一个非虚基类的间接基类
BookCharacter(); // 第二个直接非虚基类
Bear(); // 第二个直接非虚基类
TeddyBear(); // 最低层的派生类合成的拷贝和移动构造函数按照完全相同的顺序执行合成的赋值运算符中的成员也按照该顺序赋值。和往常一样对象的销毁顺序与构造顺序正好相反。