C++ primer 第十八章

C++语言的三大特性:异常处理、命名空间、多重继承。

1.异常处理

异常处理机制允许我们能够将问题的检测与解决过程分离开来。

1.1、抛出异常

在C++语言中,我们通过抛出一条表达式来引发一个异常。

当执行一个throw时,程序的控制权从throw转移到与之匹配的catch模块。

控制权的转移意味着一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。

当throw出现在一个try语句块内时,检查匹配顺序:

  1. 检查与该try块相关联的catch语句
  2. 继续检查与外层try匹配的catch子句
  3. 退出当前的函数,在调用当前函数的外层函数中继续寻找
  4. 执行terminate函数终止程序

上述匹配过程称为栈展开,栈展开过程沿着嵌套函数的调用链不断查找,直到匹配成功为止。

标准库函数terminate负责终止程序的执行过程。

在栈展开过程中,块退出后它的局部对象将随之销毁,若对象是类,则该类的析构函数将自动调用。

若析构函数需要执行某个可能抛出异常的操作,则该操作应放置在一个try语句块中。

异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。

由于异常对象会被编译器进行拷贝初始化,因此throw语句中的表达式必须拥有完全类型。

 抛出一个指向局部对象的指针几乎是一个错误的行为,你不知道该局部对象是否还存在。

当我们抛出一个表达式时,该表达式的静态编译时类型决定了异常对象的类型。

1.2、捕获异常

catch子句中的异常声明类似于函数的形参列表,若catch无须访问抛出的表达式,则可忽略捕获形参的名字。

声明的类型决定了处理代码所能捕获的异常类型,该类型必须是完全类型,不能是右值引用。

当进入一个catch语句后,通过异常对象初始化异常声明中的参数。

若catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。

异常声明的静态类型将决定catch语句所能执行的操作。

通常情况下,若catch接受的异常与某个继承体系相关,则最好将该catch的参数定义为引用类型。

在搜索catch语句的过程中,最终找到的catch未必是异常的最佳匹配(第一个与异常匹配的catch语句)。

异常和catch异常声明的匹配规则:

  • 允许从非常量向常量的类型转换:接受常量引用的catch语句能匹配非常量对象的throw对象。
  • 允许从派生类向基类的类型转换。
  • 数组被转换成指向数组类型的指针,函数被转换成指向该函数类型的指针。

如果在多个catch语句的类型之间存在着继承关系,则应将继承链最低端的类放在前面,继承链最顶端的类放在后面(catch语句从最低派生类到最高派生类型排序)。

在执行了某些校正操作之后,当前的catch可能会通过重新抛出操作将异常传递给调用链更上一层的函数接着处理异常。

重新抛出操作仍是一条throw语句,但不包含任何表达式,将异常抛出当前catch块。

空的throw语句只能出现在catch语句中或catch语句直接或间接调用的函数之内。

只有当catch异常声明是引用类型时我们对参数所做的改变才会被保留并继续传播。

catch(my_error &oj){oj.status = errcodes::severs; //修改了异常对象throw;  //异常对象的status成员是severs
}

为了一次性捕获所有异常,使用省略号来作为异常声明,这样的处理代码称为捕获所有异常。 

try{
}
catch(...){
}

若catch(...)与其他catch语句一起出现,则catch(...)必须在最后的位置。

 1.3、函数try语句与构造函数

要想处理构造函数初始值抛出的异常,需将构造函数写成函数try语句块(函数测试块)。

template<typename T> blob<T>::blob(std::initializer_list<T> il) try :
data(std::make_shared<std::vector<T>>(il)) {
}catch(...) {...}

1.4、noexcept异常声明 

对于用户以及编译器来说,知道函数不会抛出异常有助于简化代码和执行某些特殊的优化操作。

在C++新标准中,可以通过提供noexcept说明来指定某个函数不会抛出异常。

void recoup(int) noexcept;

 对于一个函数来说,noexcept说明应出现在该函数的所有声明语句和定义语句中,或一次都不出现。

noexcept不能出现在typedef或类型别名中。

在成员函数中,noexcept说明符需要跟在const及引用限定符之后。

编译器并不会在编译时检查noexcept说明。

尽管函数声明了它不会抛出异常,但实际上还是抛出了,那么程序将会调用terminate来遵守规则

若函数被设计为是throw()的,则意味着该函数将不会抛出异常。

void recoup(int) noexcept;
void recoup(int) throw(); //与上式等价的声明

noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型。

void recoup(int) noexcept(true);  //函数不会抛出异常
void recoup(int) noexcept(false); //函数可能会抛出异常

 noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。

noexcept(recoup(i)); //若recoup不抛出异常则结果为true,反之则为false
noexcept(e); //若e调用的函数都做了不抛出说明且e本身不含有throw语句时,返回true

noexcept有两层含义:当跟在函数参数列表后时它是异常说明符,而当作noexcept异常说明的实参出现时,它是一个运算符。

函数指针及该指针所指的函数必须具有一致的异常说明。 

void recoup(int) noexcept;
void (*pf)(int) noexcept = recoup; //正确

 若一个虚函数使用noexcept说明,则后续派生出来的虚函数也必须做出相同的操作。

若基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常

class base{
public:virtual double f1(double) noexcept;virtual int f2();
};
class derived : public base{double f1(double); //错误,base::f1不会抛出异常int f2() noexcept; //正确,derived的f2做了更严格的限定

当编译器合成拷贝控制成员时,同时也生成一个异常说明。

1.5、异常类层次

标准库异常类的继承体系如下图所示:

类型exception定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what的虚成员。

类型runtime_error和logic_error没有默认构造函数,但是有一个接受C风格字符串或string类型实参的构造函数,这些实参负责提供关于错误的更多信息

 what成员负责返回用于初始化异常对象的信息,在catch异常后用于提取异常基本信息的函数。

实际的应用程序通常会自定义exception的派生类来扩展其继承体系。

与其他继承体系一致,异常类的层次越低,表示的异常情况就越特殊。

2.命名空间                                                                                  

大型程序往往使用多个独立开发的库,库中又定义了大量的全局名字,这就不可避免地发生了名字相冲突。

命名空间为防止名字冲突提供了更加可控的机制:命名空间分割了全局命名空间,每个命名空间是一个作用域。 

2.1、命名空间的定义

一个命名空间的定义使用到关键字namespace和命名空间的名称。

namespace cplus{ //新的命名空间class sales_data{...};
}

命名空间的名字必须在定义它的作用域内保持一致。

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

 不同命名空间内可以有相同名字的成员。

定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问

命名空间允许是不连续的,可以为已存在的命名空间添加新成员

命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。

命名空间的组织方式:

  • 命名空间的一部分成员的作用是定义类、声明作为类接口的函数及对象,则这些成员应置于头文件中。
  • 命名空间成员的定义部分则置于另外的源文件中。

在通常情况下,我们不把#include放在命名空间内部,防止将头文件中所有的名字定义成该命名空间的成员。

可以在命名空间定义的外部定义该命名空间的成员,但命名空间对于名字的声明必须在作用域内。

该名字的定义需要明确指出其所属的命名空间

尽管命名空间的成员可以定义在命名空间内部,但定义必须出现在所属命名空间的外层空间中。

只要我们在命名空间中声明了模板特例化,就能在命名空间的外部定义它。

namespace std{template<> struct hash<sales_data>; //在命名空间中声明
}template<> struct std::hash<sales_data> {...}; //在命名空间外定义

全局作用域中定义的名字被隐式地添加到全局命名空间中。

作用域运算符可以用于全局作用域的成员。

::member_name; //作用域是隐式的

嵌套的命名空间是指定义在其他命名空间中的命名空间。

namespace cplus{namespace cpluss{......
}
}

内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。

在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间的成员若想访问它必须在名字前添加限定符。

 内联命名空间中的名字可以被外层命名空间直接使用。

关键字inline必须出现在命名空间第一次定义的地方,后续再打开空间时无须再写inline关键字。

inline namespace fifth{......
}namespace fifth{......
}

未命名的命名空间中定义的变量拥有静态生命周期。

一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。 

namespace {......
}

定义在未命名的命名空间中的名字可以直接使用,但我们不能对未命名的命名空间的成员使用作用域运算符

 若未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别,否则将出现二义性问题。

int i;
namespace{int i;
}i = 10; //二义性问题

 一个未命名的命名空间也能嵌套在其他命名空间中,此时,未命名的命名空间中的成员可以通过外层命名空间的名字来访问

namespace local{namespace {int i;}
}local::i = 42; //正确

 2.2、使用命名空间成员

命名空间的别名使得我们可以为命名空间的名字设定一个同义词。

namespace cplusplus {...};
namespace primer = cplusplus; //使用关键字namespace来定义命名空间的别名

不能在命名空间还没有定义前就声明别名,否则将产生错误。 

命名空间的别名可以指向一个嵌套的命名空间。

namespace qlib = cplusplus::querylib;

一个命名空间可以有好几个同义词或别名。

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

using指示可以使得某个特定的命名空间中所用的名字都可见。

using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但不能出现类的作用域中。

 using指示具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。

namespace ab{int i;double j;
}void f()
{using namespace ab;   //把ab中的名字注入到全局作用域中cout << i*j << endl;
}

 当命名空间被注入到它的外层作用域之后,有可能会发生名字冲突问题。

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

2.3、类、命名空间与作用域

对命名空间内部名字查找遵守由内向外依次查找每个外层作用域。

当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参所属的命名空间。

查找规则允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。

std::string s;
operator>>(std::cin,s):
//operator函数定义在标准库string中,string又定义在命名空间std中
//不用std::限定符和using声明就可以调用operator>>

 在函数模板中,右值引用形参能匹配任何类型。

一个未声明的类或函数若第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员。

namespace ab{class c{//两个友元,在友元声明外没有其他声明,则这些函数被隐式地成为命名空间ab的成员friend void f2(): //使用时除非另有声明,否则不会被找到friend void f(const c&); //查找实参的命名空间可以被找到};
}int main()
{ ab::c ob;f(ob);  //正确,通过ob可以找到f函数f2();   //错误
}

 2.4、重载与命名空间

对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行。

using声明语句声明的是一个名字,而非一个特定的函数。

当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。

一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。

using指示将命名空间的函数提升到外层作用域中,函数被添加到重载集合中。

对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。

3.多重继承与虚继承

多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。

3.1、多重继承

在派生类的派生列表中可以包含多个基类:

class panda : public bear, public endangered {......};

对于派生类能够继承的基类个数,C++并没有明确的规定,但是在一个给定的派生列表中,同一个基类只能出现一次。

在多重继承关系中,派生类的对象包含有每个基类的子对象。

构造一个派生类的对象将同时构造并初始化他的所有基类子对象。

派生类的构造函数初始值列表将实参分别传递给每个直接基类,基类的构造顺序与派生列表中基类的出现顺序保持一致。

panda::panda(std::string name,bool hibit) :
bear(name,hibit,"panda"),endangerd(endangered::critical) {}

在C++新标准中,允许派生类从它的一个或几个基类中继承构造函数,但若从多个基类中继承了相同的构造函数,则程序将产生错误。

struct base1{base1() = default;base1(const std::stirng&);
};struct base2{base2() = default;base2(const std::string&);
};struct ab : public base1,public base2{using base1::base1;   //从base1继承构造函数using base2::base2;   //从base2继承构造函数
//但是发生错误,ab试图从两个基类中继承相同构造函数base(const std::string&)
}

若一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本。

析构函数的调用顺序与构造函数相反。

3.2、类型转换与多个基类

在只有一个基类的情况下,派生类的指针或引用能自动转换成一个可访问基类的指针或引用。

void print(const bear&);
panda ying("ying_yang");
print(ying); //把一个panda对象传递给一个bear的引用

编译器不会在派生类向基类的几种转换中进行比较和选择,因为这几种转换都是等价的。 

对象、指针和引用的静态类型决定了我们能够使用哪些成员。

3.3、多重继承下的类作用域

在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。

在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。

当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名的成员。此时,不加前缀限定符直接使用该名字将引发二义性。

上述情况是完全合法的,派生仅仅是产生了潜在的二义性,只要不调用该函数就能避免二义性错误。

由于编译器是先查找名字后再进行类型检查,因此有时派生类继承的两个函数形参列表不同也有可能发生二义性错误。

要想避免上述情况的潜在二义性,最好还是在派生类中为该函数定义一个新版本

3.4、虚继承

在默认情况下,派生类中含有继承链上每个类对应的子部分,若某个类在派生过程多次出现,则派生类中将包含该类的多个子对象。

C++的虚继承机制能有效地解决上述问题,虚继承能令某个类做出声明,承诺共享它的基类

虚继承机制是继承方式,不是某个特殊基类。

在虚继承机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一的共享的虚基类子对象。

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

我指定虚基类的方式是在派生列表中添加关键字virtual

class bear : public virtual zooanimal {......};
//zooanimal是bear的虚基类

virtual说明符表明希望在后续的派生类当中共享虚基类的同一份实例。

不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。

void dance(const bear&);
panda ping_ying;
dance(ping_ying); //正确,把一个panda对象当成bear传递

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

当成员被多于一个基类覆盖时,直接访问该成员将产生二义性错误。

3.5、构造函数与虚继承

在虚派生中,虚基类是由最低层的派生类初始化的,这能有效避免虚基类被重复初始化。

含有虚基类的对象的构造顺序使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接着按照直接基类在派生列表中出现的次序依次对其进行初始化。

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

一个类可以有多个虚基类,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/306474.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(学习日记)2024.04.12:UCOSIII第四十节:软件定时器函数接口讲解

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

ArcGIS和ArcGIS Pro快速加载ArcGIS历史影像World Imagery Wayback

ArcGIS在线历史影像网站 World Imagery Wayback(网址:https://livingatlas.arcgis.com/wayback/)提供了数期历史影像在线浏览服务,之前不少自媒体作者在文中宣称其能代表Google Earth历史影像。 1、一点对比 (1)同一级别下的版本覆盖面 以下述区域为例,自2014年2月20…

java 邮件发送表格

邮件发送表格 问题导入效果图 实现方案1. 拼接HTML文件&#xff08;不推荐&#xff09;2. excel 转HTML使用工具类来转化依赖工具类代码示例 使用已工具包 如 aspose-cells依赖代码示例 3.使用模板生成流程准备模板工具类代码示例 问题导入 在一些定时任务中&#xff0c;经常会…

liunx环境变量学习总结

环境变量 在操作系统中&#xff0c;环境变量是一种特殊的变量&#xff0c;它们为运行的进程提供全局配置信息和系统环境设定。本文将介绍如何自定义、删除环境变量&#xff0c;特别是对重要环境变量PATH的管理和定制&#xff0c;以及与环境变量相关的函数使用。 自定义环境变…

《哈迪斯》自带的Lua解释器是哪个版本?

玩过《哈迪斯》&#xff08;英文名&#xff1a;Hades&#xff09;吗&#xff1f;最近在研究怎么给这款游戏做MOD&#xff0c;想把它的振动体验升级到更高品质的RichTap。N站下载了一些别人做的MOD&#xff0c;发现很多都基于相同的格式&#xff0c;均是对游戏.sjon文件或.lua文…

LeetCode-64. 最小路径和【数组 动态规划 矩阵】

LeetCode-64. 最小路径和【数组 动态规划 矩阵】 题目描述&#xff1a;解题思路一&#xff1a;动态规划五部曲。定推初遍举解题思路二&#xff1a;动态规划优化空间&#xff0c;直接改grid解题思路三&#xff1a;dfs 题目描述&#xff1a; 给定一个包含非负整数的 m x n 网格 …

Python代码识别minist手写数字【附pdf】

一、概述 对于人类而言&#xff0c;要识别图片中的数字是一件很容易的事情&#xff0c;但是&#xff0c;如何让机器学会理解图片上的数字&#xff0c;这似乎并不容易。那么&#xff0c;能否找出一个函数&#xff08;模型&#xff09;&#xff0c;通过输入相关的信息&#xff0…

最新版手机软件App下载排行网站源码/App应用商店源码

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 一款简洁蓝色的手机软件应用app下载排行&#xff0c;app下载平台&#xff0c;最新手机app发布网站响应式织梦模板。 主要有&#xff1a;主页、app列表页、app介绍详情页、新闻资讯列…

Linux虚拟网络设备:底层原理与性能优化深度解析

在深入探讨Linux虚拟网络设备的底层原理之前&#xff0c;重要的是要理解这些设备如何在Linux内核中实现&#xff0c;以及它们如何与操作系统的其他部分交互以提供高效且灵活的网络功能。虚拟网络设备在现代网络架构中发挥着关键作用&#x1f511;&#xff0c;特别是在云计算☁️…

LeetCode-1143. 最长公共子序列【字符串 动态规划】

LeetCode-1143. 最长公共子序列【字符串 动态规划】 题目描述&#xff1a;解题思路一&#xff1a;动规五部曲解题思路二&#xff1a;1维DP解题思路三&#xff1a;0 题目描述&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。…

关于转义符 \ 在php正则中的匹配问题

今天做题遇到一个很经典的问题&#xff0c;记录一下&#xff0c;先看一段代码 <?php $str&#xff0c;&#xff0c;"\\"; $pattern&#xff0c;&#xff0c;"/\\/"; if(preg_match($partern,$str,$arr)) { &#xff0c;&#xff0c;&#xff0c;&…

windows wireshark抓包rtmp推流出现TCP Retransmission

解决办法&#xff1a;tcp.port1935 && !(tcp.analysis.retransmission)

每日一题---OJ题: 合并两个有序链表

嗨!小伙伴们,好久不见啦! 今天我们来看看一道很有意思的一道题---合并两个有序链表 嗯,题目看上去好像不难,我们一起画图分析分析吧! 上图中,list1有3个结点,分别为1,2,3 ; list2中有3个结点,分别为1,3,4, 题目要求我们要将这两个链表合并到一起,并且是升序,最后将链表返回。 …

使用DSP28335在CCS中生成正弦波

DSP芯片支持数学库&#xff0c;那如何通过DSP芯片生成一个正弦波呢&#xff1f;通过几天研究&#xff0c;现在将我的方法分享一下&#xff0c;如有错误&#xff0c;希望大家及时指出&#xff0c;共同进步。 sin函数的调用 首先看下一sin函数 的使用。 //头文件的定义 #includ…

OpenHarmony开发学习:【源码下载和编译】

本文介绍了如何下载鸿蒙系统源码&#xff0c;如何一次性配置可以编译三个目标平台&#xff08;Hi3516&#xff0c;Hi3518和Hi3861&#xff09;的编译环境&#xff0c;以及如何将源码编译为三个目标平台的二进制文件。 坑点总结&#xff1a; 下载源码基本上没有太多坑&#xff…

如何实现小程序滑动删除组件+全选批量删除组件

如何实现小程序滑动删除组件全选批量删除组件 一、简介 如何实现小程序滑动删除组件全选批量删除组件 采用 uni-app 实现&#xff0c;可以适用微信小程序、其他各种小程序以及 APP、Web等多个平台 具体实现步骤如下&#xff1a; 下载开发者工具 HbuilderX进入 【Dcloud 插…

部署GlusterFS群集

目录 一、部署GlusterFS群集 1. 服务器节点分配 2. 服务器环境&#xff08;所有node节点上操作&#xff09; 2.1 关闭防火墙 2.2 磁盘分区&#xff0c;并挂载 2.3 修改主机名&#xff0c;配置/etc/hosts文件 3. 安装、启动GlusterFS&#xff08;所有node节点上操作&…

Postman实现API自动化测试

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 背景介绍 相信大部分开发人员和测试人员对 postman 都十分熟悉…

Python 爬虫基础——http请求和http响应

写本篇文章&#xff0c;我认为是能把自己所理解的内容分享出来&#xff0c;说不定就有和我一样有这样思维的共同者&#xff0c;希望本篇文章能帮助大家&#xff01;✨✨ 文章目录 一、 &#x1f308;python介绍和分析二、 &#x1f308;http请求三、 &#x1f308;http响应四、…

Day:005 | Python爬虫:高效数据抓取的编程技术(爬虫效率)

爬虫之多线程-了解 单线程爬虫的问题 因为爬虫多为IO密集型的程序&#xff0c;而IO处理速度并不是很快&#xff0c;因此速度不会太快如果IO卡顿&#xff0c;直接影响速度 解决方案 考虑使用多线程、多进程 原理&#xff1a; 爬虫使用多线程来处理网络请求&#xff0c;使用线程…