第十八章(用于大型程序的工具)
- 子系统,子程序如何协同处理错误
- 如何使用各种库进行协同开发
- 复杂的概念如何建模
/1.异常处理
1).将异常的捕获和处理分开。 2).了解以下的过程
- 异常抛出时发生了什么
- 异常捕获时发生了什么
- 传递错误的对象的意义
//1.抛出异常
1).当执行一个throw 时,跟在throw 后面的语句不再被执行。程序的控制权由,throw 转移到与之匹配的catch 模块中。catch 可能是同一个函数的局部catch ,也可能是直接或者间接调用发生异常函数的另一个函数中。
- 沿着调用链的函数可能会提早推出。
- 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
throw 后面的语句不再被执行,所以它的用法和return 很像。
2).栈展开
- 当一个异常抛出时,程序暂停当前函数的执行过程,并立即开始寻找与异常匹配的
catch 子句。 - 当
throw 出现在一个try 语句块时,检查该try 块关联的catch 字句,如果找到则使用该字句;否则如果该try 语句块嵌套地在其他语句块中,则继续检查与外层的try 语句块中关联的catch 字句。如还是没有找到,则退出当前的函数,在调用当前函数的外层函数中寻找。 - 如果对函数的调用在一个
try 语句块中… - 以上的过程就是一个栈展开。它沿着嵌套函数的调用链不断查找…如果没有找到匹配的
catch ,则退出主函数后查找过程终止。 - 当找到一个
catch 字句之后,程序进入该子句执行代码,执行完毕之后,从该try 语句块的最后一个catch 子句后开始执行代码。 - 当找不到匹配的
catch ,程序将会调用标准库函数terminate ,终止程序。所以一旦发生异常,不能对他置之不理。否则它将导致程序的终止。
3).栈展开过程对象将被自动销毁
- 当栈展开过程中,出现块退出的情况,编译器将会负责确保在这个块中创建的对象被正确销毁。如果是类类型,析构函数会自动调用
- 如若异常发生在构造函数中,**当前对象可能只构造了一部分,有的成员已初始化,有的也许在异常发生前没有初始化。此时我们也要保证已经构造的成员被正确的销毁。**类似的,异常也可能发生在数组或者标准库容器的元素初始化过程中。对于已经构造的一部分元素,我们也需要确保正确地销毁。
4).析构函数与异常
- 析构函数总是会被执行,不论是在程序正常进行时还是发生了异常,为了正确的释放资源,我们使用类来控制资源的分配,从而避免在块中释放资源代码,之前发生了异常导致资源没有正确地释放。
- 由于栈展开过程中很可能会调用析构函数。并且在栈展开的过程中,我们是已经引发了异常但是没有处理它。具体原因未知(连续地抛出异常会怎么样?),析构函数不应该抛出不能被它自身处理的异常。对于可能抛出异常的操作都应该放置在一个
try 语句块中去,并在析构函数内部得到处理。如若析构函数抛出了异常,并且在 函数内部 没有得到处理,则程序将被终止。 - 在实际中,由于析构函数仅仅是释放资源,所以它不太可能抛出异常,所有的标准库类型都确保它们的析构函数不会引发异常。
5).异常对象
- 编译器使用异常抛出表达式对异常对象进行拷贝初始化。**因此
throw 语句中的表达式必须拥有完全类型;**并且如果该表达式是
- 类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或者移动构造函数。
- 如果该表达式是数组类型或者函数类型,则表达式将被转换成与之对应的指针类型。
- 异常对象位于编译器管理的空间中,编译器确保无论最终调用的是哪一个
catch 字句,都能访问到该空间,当异常处理完毕后,异常对象将会被销毁。 - 抛出一个指向局部对象的指针肯定是一个错误的行为;因为在该块的
catch 语句执行之前,它所指的对象肯定已经销毁了。 - 当我们抛出一条表达式时,该表达式的静态编译时的类型,决定了异常对象的类型。因此,如果
throw 表达式解引用一个基类指针,而该指针实际指向的时派生类对象,则抛出时对象将会发生切断。只有基类部分被抛出。
练习,
- 使用
try 语句块 - 使用类来管理内存。
//2.捕获异常
1).catch 字句中的异常声明看起来就像是只包含一个形参的函数形参列表。如果我们不需要使用抛出表达式,我们可以忽略捕获形参的名字。
- 声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,可以是左值引用,但是不能是右值引用。
- 当进入一个
catch 语句之后,通过异常对象初始化声明中的参数。和函数的参数一样,如果catch 的参数是非引用类型,则该参数是异常对象的一个副本,如果是引用,则是异常对象的一个别名。想要说明的区别是修改该形参是否修改本体。 catch 的参数如果是基类类型,则我们可以使用其派生类类型的异常对象对他进行初始化。(和函数的形参是一样的)
- 如果是非引用,派生类的非基类部分将会被切断。
- 如果是基类的引用,将是绑定。动态绑定。
- 异常声明中的静态类型将决定
catch 语句所能执行的操作。如果catch 的参数是基类类型,则catch 无法使用派生类特有的任何成员。 - 通常情况下,如果
catch 的类型和继承体系相关,最好设置catch 的参数为引用类型。
2).查找匹配的处理代码
- 在搜寻
catch 的过程中,我们最终找到的catch 未必是异常的最佳匹配。而是,挑选出来的应该是第一个和异常匹配的catch 语句。因此,越是专门的catch 月应该置于整个catch 列表的前端。 - 原因是:
catch 语句是按照其出现的顺序逐一进行匹配的。当程序使用具有继承关系的多个异常时,必须对catch 语句的顺序进行组织和管理,使得派生类异常的代码出现子在基类异常的处理代码之前。 - 与实参和形参的匹配规则相比,异常和
catch 异常声明之间的匹配规则受到更多的限制。此时,绝大多数的类型转换都不被允许,除了一些极细小的差别之外,要求异常的类型和catch 声明类型是精确匹配的。
- 允许非常量到常量之间的转换。非常量对象的
throw 可以匹配到一个接受常量引用的catch 语句。 - 允许从派生类到基类的转换
- 数组转换成指向数组首元素的指针;函数转换成指向该函数的指针。
- 除此之外,包括算术类型,类类型转换在内的所有转换规则,都不能用在匹配
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;
}
}
4).捕获所有的异常处理代码
- 这是有难度的,因为我们有时候不知道异常的类型是什么。即使知道,也很难为每一个异常提供一个
catch 子句。为了一次性捕获所有的异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码。catch(...); 一条语句可以和任意类型的异常对象匹配。 - 它经常和重新抛出语句一起使用,其中
catch 执行完当前局部能够完成的工作之后,重新抛出异常。
{
void manip () {
try {
}
catch (...) {
throw;
}
}
}
练习
- 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);
}
}
//4.noexcept异常说明
1).预先知道某一个函数不会抛出异常显然有多好处
- 简化调用该函数的代码(用户)
- 编译器可以执行某一个特殊的优化操作;这些优化操作不适用于可能会出错的代码。
2).新标准中,我们可以通过提供noexcept 说明,指定某一个函数不会抛出异常。
{
void recoup(int) noexcept;
void alloc(int);
}
3).违反异常说明
- 编译器并不会在编译时检查
noexcept 说明。如果在一个函数中说明了noexcept 的同时又含有throw 语句,或者调用可能抛出异常的其他函数,编译器将会顺序编译通过,并不会因为这种违反异常说明滚的情况而报错。(又可能会有编译器会提出警告) - 因此可能会出现一个函数既声明了不会抛出异常,而又抛出了异常。此时,一旦抛出异常,程序就会调用
terminate 以确保遵守不在运行时抛出异常的承诺。上述过程对是否执行栈展开没有约定。 - 因此
noexcept 用在两种情况。
- 我们确认函数不会抛出异常
- 我们根本不会处理该异常
- 通常情况下,编译器不能也不必在编译时验证异常说明。
4).向后兼容
- 早期的c++设计了一套详细的异常说明方案。该方案允许我们指定某一个函数可能会抛出的异常类型。函数可以指定一个关键字
throw ,后面跟上括号起来的异常类型列表。位置同noexcept 。 - 上述使用的
throw 在c++11中已经取消了。
{
void recoup(int) noexcept;
void recoup(int) throw();
}
5).异常说明的实参
{
void recoup(int) noexcept(true);
void alloc(int) noexcept(false);
}
6).noexcept 运算符
noexcept 的实参通常和noexcept 运算符混合使用。noexcept 是一个一元运算符,它的返回值是一个bool 类型的右值常量表达式。用于表示给定的表达式是否会抛出异常。- 和
sizeof 一样,noexcept 也不会对该表达式求值。
{
noexcept(recoup(i));
noexcept(e);
void (*pf) noexcept = recoup;
void f() noexcept(noexcept(g()));
}
7).异常说明与指针,虚函数,拷贝控制
noexcept 说明符不是函数类型的一部分,但是函数异常说明会影响函数的使用。- 函数指针及该指针所指的函数必须具有一样的异常说明。
{
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);
int f2() noexcept(true);
void f3() noexcept;
};
}
//5.异常类继承体系
1).标准异常类构成了一个继承体系。见p693。
- 类型
exception 仅仅定义了拷贝构造函数,拷贝赋值运算符,一个虚析构函数,一个名为what 的虚成员。其中what 返回一个const char* ,该指针指向一个以null 结尾的字符数组,并且确保不会抛出任何异常。 - 类
exception ,bad_cast ,bad_alloc 定义了默认构造函数。类runtime_error 和logic_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;
};
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).解决,
- **传统上,**程序员通过将其定义的全局实体名字设得很长来避免命名空间得污染。这样的名字通常包括表示名字所属库的前缀部分。
{
class cplusplus_primer_Query {....};
string cplusplus_primer_make_plural(size_t, string&);
}
- 命名空间,分割了全局命名空间,使得每一个命名空间是一个作用域。通过在某一个命名空间中定义库的名字,库的作者,用户可以避免全局名字固有的限制。
//1.命名空间的定义
1).一个命名空间定义包含两个部分
- 关键字
namespace - 随后是命名空间的名字
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 q = AW::Query("hello);
}
4).命名空间可以是不连续的
- 命名空间可以定义在几个不同的部分,这一点和作用域不太一样。
{
namespace nsp {
}
}
- 命名空间的不连续特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。此时命名空间的组织方式类似我们管理自定义的类及函数的方式
- 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数以及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。(在头文件中使用命名空间)
- 命名空间成员的定义部分则置于另外的源文件中。
- 非内联函数
- 静态数据成员
- 变量等
- 命名空间中定义名字也需要满足这一要求,我们可以通过一样的方式组织命名空间达到目的。这种接口和实现分离的机制使得我们所需的函数和实体只定义一次,而只要是使用到这些实体的地方都可以看到实体的声明。
- 每一个文件应该保存独立的命名空间,或者相互关联的命名空间。p697.
5).定义命名空间
{
#include <string>
namespace cplusplus_primer {
class Sales_data {/.../};
Sales_data operator+(const Sales_data &,
const Sales_data &);
}
#include "Sales_data.h"
namespace cplusplus_primer {
}
#include "Sales_data.h"
int main() {
using cplusplus_primer::Sales_data;
Sales_data trans1, trans2;
return 0;
}
}
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::QueryLib::Query;
}
10).内联命名空间
- c++11新标准引入了一种新的嵌套命名空间,称为内联命名空间。
- 和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。即,我们无须在内联命名空间的名字前面添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问到它。
{
inline namespace FifthEd {
}
namespace FifthEd {
class Query_base {}
}
namespace FourthEd {
class Item_base {};
class Query_base {};
}
namespace cpp_primer {
#include "FifthEd.h"
#include "FourthEd.h"
}
cpp_primer::FourthEd::Query_base;
}
11).未命名的命名空间
- 指的是关键
namespace 之后紧跟花括号起来的一系列声明 - 里面定义的变量拥有静态生命周期,它们在第一次使用前创建,直到程序结束时才销毁。
- 一个未命名的命名空间可以在某一个给定的文件内不连续,**但是不能跨越多个文件。每一个文件定义自己的未命名空间,如果两个文件都含有未命名空间,则这两个空间相互无关。**在这两个空间中可以定义同名变量,并且这些定义表示的是不同的实体。
- 如果一个头文件定义了未命名空间,则该命名空间中定义的名字将在每一个包含该头文件对应不同的实体。
- 定义在未命名的命名空间中的名字可以直接使用,因为我们找不到命名空间的名字来限定。同样的,我们也不能对未命名的命名空间的成员使用作用域运算符。
- 未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外城作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别。
{
int i;
namespace {
int i;
}
i = 10;
namespace local {
namespace {
int i;
}
}
local::i = 42;
}
//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 指示一般被看作是出现在最近的外层作用域中。
{
namespace A {
int i, j;
}
void f() {
using namespace A;
cout << i * j << endl;
}
namespace blip {
int i = 16, j = 15, k = 23;
}
int j = 0;
void manip() {
using namespace blip;
++i;
++j;
++::j;
++blip::j;
int k = 97;
++k;
}
}
练习
- 在函数内
using 声明,并且定义一个与其一样的名字是重复定义的错误 - 在函数内
using 声明,在函数内可以直接使用,而如果要使用全局的,需要加上:: - 在函数外进行
using 声明,函数内可以直接使用。
- 在全局作用域。命名空间的名字可以直接在函数访问。如果全局变量和命名空间里的名字有重名,有二义性的错误。
- 如果在函数里面。同上。
//3.类,命名空间和作用域
1).对命名空间了内部的名字查找遵循常规查找规则。由内向外依次查找每一个外层作用域。外层作用域也可能是一个或者多个嵌套的命名空间。直到最外层的全局命名空间查找过程终止。
- 只有位于开放的块中且在使用功能点之前声明的名字才会被考虑。
{
namespace A {
int i;
namespace B {
int i;
int j;
int f1() {
int j;
return i;
}
}
int f2() {
return j;
}
int j = i;
}
}
- 对于位于命名空间中的类来说,常规的查找规则仍然适用。当类的成员函数是使用了某一个名字
- 首先在该点之前的函数体中查找(包括形参列表)
- 然后再从成员中查找,不关乎函数在哪一个点,包括基类。(友元何时查找?)
- 接着是外层作用域中查找,(此时可能是一个或者多个命名空间。)
{
namespace A {
int i;
int k;
class C1 {
public:
C1() : i(0), j(0) { }
int f1() {return k;}
int f2() {return h;}
int f3();
private:
int i;
int j;
};
int h = i;
}
int A::C1::f3() {return h;}
}
2).类类型形参的函数查找
{
string s;
std::cin >> s;
operator>>(std::cin, s);
using std::operator>>;
std::operator>>(std::cin, s);
}
3).std::move,std::forward 的查找
{
}
4).友元声明与实参相关的查找
{
namespace A {
class C {
friend void f2();
friend void f(const C&);
};
}
int main() {
A::C obj;
f(obj);
f2();
}
}
练习
- 18.18,对于
swap 。是否有using std::swap; 会影响它的匹配过程。
- 如果有
using ,直接匹配到using 的版本。 - 如果没有,则按照上述的规则进行查找。注意,
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;
}
using NS::Quote(int);
using NS::Quote;
namespace lib_R_us {
extern void print(int);
extern void print(double);
}
void print(const string &);
using namespace lib_R_us;
void fooBar(int, ival) {
print("value: ");
print(ival);
}
}
/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;
using Base2::Base2;
};
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,当多个基类有一样的成员时
- 使用
类名:: 访问特定版本的成员 - 对于具有唯一性的继承而来的成员可以直接使用。
//3.多重继承下的类作用域
1).只有一个基类时,由于继承类的嵌套关系,名字的搜索就是从内到外,内层屏蔽外层。
- 在多重继承中,相同的查找过程在所有直接基类(继承链条)中同时进行,如果名字在多个基类(多个继承链条)中被找到,则该名字的使用将具有二义性。
- 对于一个派生类而言,从它的几个基类分别继承名字相同的成员是完全合法的,只不过在使用时需要明确指出它的版本。
- 有时候,即使派生类继承的两个函数形参列表不一样,也可能发生错误。此时,即使同名的在一个类中是
private ,在另一个类中是public ,或者protected 也一样是可能发生错误的。 - 愿意就是,编译器是先进行名字查找,再进行类型检查。如果发现了两个函数,就直接报错。
- 解决方法就是,在该类中,定义一个覆盖的版本。
- 指针和引用是否也是一样的?
{
double Panda::max_weight() const {
return std::max(ZooAnimal::max_weight(), Endangered::max_weight());
}
}
//4.虚继承
1).虽然派生列表中,直接基类的名字不能重复,但是一个派生类可以多次继承同一个类。
- 通过两个直接基类继承同一个类
- 直接继承一个基类,通过另一个直接基类再一次继承该类。
- IO标准库中的
istream 和ostream 分继承了一个共同的名为base_ios 的抽象基类。该抽象基类负责保存流的缓冲内容并管理流的条件条件状态。iostream 是另外一个类,他从istream 和ostream 直接继承而来,可以同时读写流的内容。因此iostream 继承了base_ios 两次。 - 默认情况下,派生类中含有继承链上每一个类对应的子部分。如果一个类在派生过程中,出现了多次,则派生类中将包含该类的多个子对象。
- 这样的默认情况对某些像
iostream 这样的类是行不通的。一个iostream 肯定希望在同一个缓冲区中进行读写操作,也会要求条件状态能同时反应输出和输入操作的情况。如果一个iostream 真的包含了两个base_ios ,那么共享操作无法实现。
2).解决。c++语言引入虚继承。
- 虚继承的目的是里那个某一个类做出声明,承诺愿意共享它的基类。其中共享的基类子对象称为**虚基类。**在这种机制下,不论虚基类在继承体系中出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
3).新的Panda 继承体系。
- 必须在虚派生类的真实需求之前就完成了需派生的操作,两个均需要如此操作。(p718)
- 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
- 位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。
- 实际中,虚继承的类层次是由一个人或者一个组一次性完成…个人设计的类大多不需要这样做…p718。
4).使用虚基类
{
class Raccoon : public virtual ZooAnimal {....};
class Bear : virtual public ZooAnimal {....};
class Panda : public Bear, public Raccoon, PUblic Endangered {....};
}
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(string name, bool onExhibit) :
ZooAnimal(name, onExhibit, "Panda"),
Bear(name, onExhibit),
Endangered(Endangered::critical),
sleeping_flag(false) {}
}
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,虽然虚基类会先进行构造,但是在构造它的时候,必然会调用它的直接基类…
- 在该题中,意外地,有两个
Class 子对象。 Class *pc = new Final(); 是错误的,因为Class 是Final 的一个二义基类。- 基类到派生类的转换,指针和对象都是不允许的。因为,派生类有基类的子对象。
|