C++ 小白 学习记录18

第十八章 异常处理,命名空间, 多重继承与虚继承

18.1 异常处理

18.1.1 抛出异常

throw 以后 将会被符合要求的最近的catch捕获. 从而将当前的控制权转移到catch处理. throw之后的代码将不会被执行, 之前创建的对象将被销毁.

栈展开: 寻找匹配catch的过程. 最近的try catch中, 外层的try catch, 调用了该函数的函数中寻找, 依次向外. 如果 一直找不到 则程序调用标准库函数terminate, 终止该程序的继续执行.

栈展开的过程中 对象被自动销毁.

析构函数与异常: 

析构函数会在栈展开过程中被调用, 因此, 析构函数不能抛出异常, 异常必须能够在析构函数内部被处理. 一旦 在栈展开的过程中析构函数抛出了未能在内部处理的异常, 则程序将被终止. 有些资源有可能未被回收.

异常对象:

  • throw语句中的表达式必须是完全类型
  • 如果是类类型的话, 则类必须包含一个可访问的析构函数和一个可访问的拷贝/移动构造函数
  • 如果是数组或函数类型, 则表达式被转换成与之对应的指针类型

当抛出一条表达式时, 该表达式的静态编译时的类型决定了异常对象的类型.  如 如果抛出的是一个基类指针(该指针实际指向的是派生类), 则抛出的指针在解引用时, 派生类的部分将被抛弃.

18.1.2 捕获异常

catch的表达式 可以是左值引用不能是右值引用.

异常声明的静态类型将决定catch语句所能执行的操作. 如果catch的参数是基类类型, 则catch无法使用派生类特有的任何成员.

catch的参数最好是 引用类型.

第一个匹配的catch有可能不是最佳匹配, 所以越是专门的catch, 越应该置于整个catch列表的前端.

  • 允许从非常量向常量的类型转换. 即 一条非常量对象的throw可以匹配一个接受常量引用的catch
  • 允许从排成类向基类的类型转换
  • 数组被转换为指向数组类型的指针, 函数被转换成指向函数的指针.

其余标准算术类型转换和类类型转换 都不能在匹配过程中使用.

重新抛出:  即 空throw,  空throw只能出现在catch语句或者catch语句直接或间接调用的函数之内. 如果在处理代买之外的区域遇到空throw语句,编译器将调用terminate.  空throw不能指定新的表达式, 会将当前的异常对象沿着调用链向上传递.

捕获所有异常:

使用catch(...) 与任意异常匹配.

18.1.3 try与构造函数

因为初始值列表抛出异常时构造函数体内的try语句块还未生效, 所有构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常, 为解决此问题,可以将构造函数写成函数try语句块的形式:

template<typename T>
class Blob {
	T t;
public:
	Blob(std::initializer_list<T> il){}
};

// 通常的做法, 无法try到构造函数的异常
//template<typename T>
//Blob<T>::Blob(std::initializer_list<T> il) {}

template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :{
	// ...
}
catch (const std::bad_alloc& e) {
	// 异常处理代码
}

如果是参数初始化的过程中发生异常, 则只能在调用的地方拦截异常.

18.1.4 noexecpt异常说明

noexcept紧跟的函数 表示该函数不会抛出异常.

  • 该关键字要么出现在该函数的所有声明语句和定义语句中, 要么一次也不出现.
  • noexecpt 应该在函数的位置返回类型之前.
  • 在typedef或类型别名中不能出现.
  • 成员函数中, 需要跟在const及引用限定符之后, 在final override或虚函数的=0 之前

一旦在noexecpt中抛出了异常, 则程序会terminate.

为了老版本兼容, 如下定义等价, 不推荐throw():

void func(int) noexecpt;
void func(int) throw();
// 两种方式等价, 都承诺func不会抛出异常

一个多余的做法(自认为): noexcept可以接收一个能够转换为bool类型的参数, true表示不会抛出异常, false表示可能抛出异常.(不标noexcept 不就完了吗?)

noexcept运算符:

noexcept(func(i))  //如果func(int) 声明的noexcept 则返回true, 否则返回false
noexcept(e) // 当e调用的所有函数都做了不抛出异常的承诺, 且e本身也不含有throw, 则为true, 否则false

void f() noexcept(noexcept(g())); // f和g的异常说明保持一致

异常说明与指针/虚函数/拷贝控制:

  • 如果声明的noexcept的指针, 则只能指向不抛出异常的函数.
  • 如果没有声明noexcept的指针, 则可以指向任何函数, 即使是noexcept的函数.
void func1(int) noexcept {};
void func2(int) {};
void func3(int) throw() {};

void(*pf1)(int) noexcept = func1; // 正确
void(*pf2)(int) noexcept = func2; // 错误 不兼容的异常规范
  • 如果虚函数是noexcept的, 则派生的虚函数也必须是noexcept. 如果基类虚函数没有noexcept, 则派生的函数可以是 也可以不是.
  • 如果所有成员和基类的所有操作都是noexcept的, 则合成的成员也是noexcept的.  如果合成的成员调动的任何函数可能抛出异常, 则合成的是noexcept(false)的.  如果自定义了析构函数却没有提供异常说明的话, 编译器会自动合成一个. 合成的说明符将与假设由编译器为类合成析构函数时所得的异常说明一致.

18.1.5 异常类的层次结构

 

18.2 命名空间

18.2.1 命名空间定义

包含两部分: namespace + 命名空间的名字 { 声明, 定义}.

命名空间可以定义在全局作用域内, 其他命名空间内. 但是不能定义在函数或类内部.

namespace mynamespace {
	class c1 {};
	class c2 {};
	template<typename T>
	T operator+(const T&, const T&);
	// 重载类T的 运算符 +
}
  • 每个命名空间都是一个作用域
  • 命名空间可以是不连续的.
  • #include应该出现在打开命名空间的操作之前.
  • 命名空间中定义的成员可以直接使用名字, 此时无须前缀
  • 命名空间外定义的成员必须使用含有前缀的名字. (跟类一样)
  • 模板特例化必须声明在原始模板所属的命名空间内. 之后才能在命名空间外定义.

全局命名空间:

::member_name 表示全局命名空间中的一个成员

嵌套的命名空间:

  • 内层命名空间声明的名字将隐藏外层命名空间声明的同名成员.
  • 在嵌套的命名空间中定义的名字只在内层命名空间有效, 外层想用需要加空间名, 如A:B:Variable

内联命名空间 inline namespace:

  • 内联命名空间内的名字可以被外层命名空间直接使用.
  • inline 必须出现在命名空间第一次定义的地方.

未命名的命名空间:

  • namesapce {} 类似这种的定义
  • 未命名的空间中定义的变量拥有静态声明周期. 第一次使用前创建, 直到程序结束才销毁
  • 仅在特定的文件内部有效, 作用范围不会跨越多个不同的文件
  • 未命名空间中的名字可以直接使用, 不能使用作用域运算符.
  • 未命名的命名空间与所在作用域相同, 因此其中的名字必须和所在作用域中的名字不同.

18.2.2 使用命名空间

命名空间的别名:

namespace 别名 = 命名空间名;

namespace mynamespace {
	class c1 {};
}
namespace m = mynamespace;  // 命名空间的别名

不能在命名空间还没有定义前就声明别名.

using 声明:

一条using声明语句一次只引入命名空间的一个成员.

其作用域为: using开始到所在的作用域结束.

类内使用using 只能指向其基类成员.

using指示:

using namesapce namespace_name.  namespace_name中所有的名字都是可见的, 控制力度不如using声明仔细.

using指示不能出现在类内.

using指示与作用域:

头文件与using声明或指示:

头文件如果在其顶层作用域中含有using指示/声明, 则会将名字注入到所有包含该头文件的文件中.

应避免使用using指示.

18.2.3 类/命名空间与作用域

名字的查找规则是由内向外直至最外层作用域为止.

实参相关的查找与类类型形参:

一个列外: 当我们给函数传一个类类型的对象时, 除了在常规的作用域查找外还会查找实参类所属的命名空间. 对于传递类的引用或指针的调用同样有效. 如:

std::string s;
std::cin >> s;
operator>>(std::cin, s);
// operator>> 定义在string中, string又定义在std中, 
// 但是不用使用std::限定符就可以使用operatro>>

查找顺序:  当前作用域 - > 外层作用域 -> 实参类 所在的作用域

查找与std::move 和 std::forward

因为这俩货可以和任何类型的参数匹配, 所以如果同一个作用域内定义了move/forward几乎100%会和标准库中的版本冲突, 所以在使用时, 标准库的版本的最好是加上std:: 限定符.

友元声明与实参相关的查找:

namespace A {
	class C {
		// 两个友元声明, 除此之外没有在其他地方声明
		// 这俩货被隐式的成为命名空间A的成员
		friend void f();    // 除非另有声明, 否则不会被找到
		friend void f2(const C&);   // 根据实参相关的查找规则(operator>>那个) 可以被找到
	};
}
A::C cobj;
f(); //   错误, A::f 没有声明
f2(cobj); // 正确, 可以通过实参cobj类型所在的作用域找到f2

18.2.4 重载与命名空间

using声明或using指示能将某些函数添加到候选函数集. 实参所在的命名空间中的函数也会添加到候选函数中.

重载与using声明: using声明语句声明的是一个名字而非一个特定的函数. 在声明时不能指定形参类型. 当使用using声明时, 该函数(具有相同名字的)的所有版本都被引入到当前作用域中.

using声明所在的作用域中已经有一个函数与新引入的函数同名且同参, 则using声明将引发错误.

using声明的函数会隐藏外层作用域中的同名同参函数.

重载与using指示:

using指示引入的函数会和当前作用域中的同名函数一起作为候选函数.

using指示引入一个同名同参的函数不会产生错误.

跨越多个using指示的重载:

来自每个命名空间的名字都会成为候选函数集的一部分.

18.3 多重继承于虚继承

18.3.1 多重继承

class sub: public base1, public base2 {
    // xxx
};

派生类构造函数初始化所有基类:

基类的构造顺序与派生列表中基类的出现顺序保持一致. 与派生类构造函数初始值列表中基类的顺序无关.

继承的构造函数与多重继承:

如果派生类没有自己的构造函数而从多个基类中继承了相同的构造函数 则产生错误.

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;
	// 严重性	代码	说明	项目	文件	行	禁止显示状态
	// 错误	C3882	“Base2::Base2”: 构造函数已继承自“Base1”
	// D1 如果没有定义以下自己的构造函数, 上面引入的两个基类构造函数将产生上面的错误.
	D1(const std::string &s): Base1(s), Base2(s){}
	D1() = default;
};

析构函数与多重继承: 跟单继承的相同.

多重继承的派生类的拷贝与移动操作: 与单继承相同.

18.3.2 类型转换与多个基类

派生类绑定到任何一个基类都是相同的, 所以以下代码会出现二义性错误.

void print(const Base1&);
void print(const Base2&);
D1 d1("d1");
print(d1); //二义性错误, d1向Base1 或Base2 转换都可以

基于指针类型或引用类型的查找

跟单继承一样. 父类指针或引用 只能指向子类对象中的 父类的部分, 子类及该子类其他父类特有的部分将不可见.

18.3.3 多重继承下的类作用域

如果多重继承时继承了同名的成员, 在使用时需要前缀限定符,否则会产生二义性.

18.3.4 虚继承

尽管派生类表中同一个基类只能出现一次, 但是派生类可以通过间接的方式多次继承同一个类.

多次继承的基类部分 是共享的呢? 还是相同的呢?  为了避免 多次继承的基类部分是拷贝的这种情况, 引入的虚继承的机制. 虚继承的目的是令某个类做出声明: 承诺愿意共享它的基类. 其中 共享的基类子对象成为虚基类. 此种机制下, 无论虚基类在继承体系中出现了多少次, 在派生类中都只包含唯一一个共享的虚基类子对象.

虚派生值影响从指定了虚基类的派生类中进一步派生出的类, 不会影响派生类本身.

使用虚基类:

在派生类列表中添加关键字virtual, 该派生类的派生不受影响.

struct Base {
	// xxx
};
struct Base1: public virtual Base {
	void print(int) const;
protected:
	int ival;
	double dval;
	char cval;
private:
	int* id;
};
struct Base2: public virtual Base {
	void print(double) const;
protected:
	double fval;
private:
	double dval;
};

派生类对象都能被可访问基类的指针/引用操作 不受影响.

虚基类成员的可见性:

因为在每个共享的虚基类中只有唯一一个共享的子对象, 所有该基类的成员可以被直接访问, 不会产生二义性.

一种情况:

基类B中有成员X,  D1, D2 虚继承自B, D继承自D1, D2, 在D的作用域内:

  • D1, D2 中都没有X, 则X将被解析为B中的X, 不存在二义性. D对象只含有一个X的实例.
  • D1或D2中有一个X, 则优先调用D1/D2中的X, 也不存在二义性.
  • D1和D2中都有X, 则此时会产生二义性.

18.3.5 构造函数与虚继承

在虚派生中, 虚基类是由最底层的派生类初始化的.

虚继承的对象的构造方式:

首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分, 接下来按照直接基类在派生列表中出现的次序依次对其进行初始化.

Panda::Panda(std::string name, bool onExhibit)
	:ZooAnimal(name, onExhibit, "Panda"),
	Bear(name, onExhibit),
	Raccoon(name, onExhibit),
	Endangered(Endangered::critical),
	sleeping_flag(false) {}
  1. 使用Panda的构造函数初始值列表中提供的初始值构造虚基类ZooAnimal部分
  2. 构造Bear
  3. 构造Raccon
  4. 构造Endangered
  5. 构造Panda部分

 如果Panda没有显式的的初始化ZooAnimal基类, 则其默认构造函数将被调用,  如果其没有默认构造函数, 则代码将出错.

虚基类总是先于非虚基类构造, 与他们在继承体系中的次序和位置无关.

构造函数与析构函数的次序:

class Animal{};
class Bear: public virtual Animal{};
class Character {};
class BookCharacter: public Character {};
class ToyAnimal {};
class TeddyBear: public BookCharacter, public Bear, public virtual ToyAnimal {};

上面代码的构建/合成的拷贝/移动构造函数 顺序:

  1. Animal()
  2. ToyAnimal()
  3. Character()
  4. BookCharacter()
  5. Bear()
  6. TeddyBear()

而析构时正好相反.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yutao1131

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值