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.mzph.cn/news/810950.shtml

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

相关文章

谷歌不收录怎么办?

谷歌不收录首先你要确认自己网站有没有出问题&#xff0c;比如你的网站是否已经公开&#xff0c;rboot是否允许搜索引擎进来&#xff0c;网站架构有没有问题&#xff0c;面包屑的结构是否有问题&#xff0c;确保你的网站没问题 接下来就是优化这个过程&#xff0c;有内容&#…

IPSec简介

起源 随着Internet的发展&#xff0c;越来越多的企业直接通过Internet进行互联&#xff0c;但由于IP协议未考虑安全性&#xff0c;而且Internet上有大量的不可靠用户和网络设备&#xff0c;所以用户业务数据要穿越这些未知网络&#xff0c;根本无法保证数据的安全性&#xff0…

path环境变量的作用

当我把一个运行文件的路径加入到了path环境变量&#xff0c;就可以在cmd命令行随时使用运行。 在path中有两个path上面的是用户的path&#xff0c;下面的是计算机的path

蓝桥杯 每日2题 day5

碎碎念&#xff1a;哦哈呦&#xff0c;到第二天也是哦哈哟&#xff0c;&#xff0c;学前缀和差分学了半天&#xff01;day6堂堂连载&#xff01; 0.单词分析 14.单词分析 - 蓝桥云课 (lanqiao.cn) 关于这题就差在input前加一个sorted&#xff0c;记录一下下。接下来就是用字…

【MATLAB源码-第10期】基于matlab的pi/4DQPSK,π/4DQPSK的误码率BER理论和实际对比仿真。

1、算法描述 蓝牙是一种被广泛应用的无线通信标准&#xff0c;工作在2.4GHz-2.4835GHz频段范围&#xff0c;所用的调制方式有:GFSK&#xff0c;PI/4-DQPSK。北美第二代数字蜂窝移动通信系统D-AMPS和日本的JDC蜂窝系统均采用PI /4-DQPSK&#xff0c;欧洲的GSM系统采用GMSK。PI /…

水质溶解氧控制器的优势特点

在全球水资源日益紧缺、水质问题愈发严重的现状下&#xff0c;如何科学有效地监测与管理水体溶解氧含量&#xff0c;成为了关乎生态环境保护、水生生物生存以及人类饮水安全的重要课题。溶解氧作为衡量水体自净能力、判断水体是否缺氧、评估水生生态系统健康状况的一项关键指标…

类和对象【一】类和对象简介

文章目录 C的类与C语言结构体的区别【引入类】类的定义类体中的成员函数的实现类中的访问限定符C中class和struct的区别 类的作用域类的实例化类中成员的存储位置类的大小 C的类与C语言结构体的区别【引入类】 类里面不仅可以定义变量还可以定义函数 例 类具有封装性【将在该…

关于nvm node.js的按照

说明&#xff1a;部分但不全面的记录 因为过程中没有截图&#xff0c;仅用于自己的学习与总结 过程中借鉴的优秀博客 可以参考 1,npm install 或者npm init vuelatest报错 2&#xff0c;了解后 发现是nvm使用的版本较低&#xff0c;于是涉及nvm卸载 重新下载最新版本的nvm 2…

云原生数据库海山(He3DB)PostgreSQL版核心设计理念

本期深入解析云原生数据库海山PostgreSQL版&#xff08;以下简称“He3DB”&#xff09;的设计理念&#xff0c;探讨在设计云原生数据库过程中遇到的工程挑战&#xff0c;并展示He3DB如何有效地解决这些问题。 He3DB是移动云受到 Amazon Aurora 论文启发而独立自主设计的云原生数…

Excel---一个工作簿中的多个sheet合并成一个PDF

0 Preface/Foreword 1 操作方法 1.1 方法一 文件》 导出 》创建PDF/XPS 》 选项 》发布内容 》“整个工作簿” 1.2 方法二 文件》 打印》 打印机选项中&#xff0c;选择一种PDF阅读器 》设置选项中&#xff0c;选择打印整个工作簿。

三步就能在OpenHarmony中实现车牌识别

介绍 本车牌识别项目是基于开源项目 EasyPR&#xff08;Easy to do Plate Recognition&#xff09;实现。EasyPR 是一个开源的中文车牌识别系统&#xff0c;基于 OpenCV 开源库开发。 本项目使用润和 HiSpark Taurus AI Camera(Hi3516DV300) 摄像头开发板套件(以下简称 Hi351…

对拍器/对数器 赛前抱佛脚

“对拍器/对数器的使用前提是该题你会暴力解法&#xff0c;如果不会&#xff0c;那么对拍器/对数器也没啥用。” 对拍器/对数器 应用背景 你有一个绝对对的暴力cpp代码&#xff0c;但时间会T&#xff0c;只适用于小范围的数据。所以你写了一个聪明的非暴力cpp代码&#xff0c…

【ctf.show】获得百分之百的快乐

1.打开靶场 2.根据页面代码&#xff0c;get请求值只能小于4位数&#xff0c;否则会回显hack&#xff01; 尝试后确实是这样的&#xff1a; 请求值小于4位数&#xff0c;页面无变化&#xff1a; 发送请求值ls查看内容 3.根据2返回的值&#xff0c;发送值为?1>nl 创建一个nl…

vivado 使用基本触发器模式

使用基本触发器模式 基本触发器模式用于描述触发条件 &#xff0c; 即由参与其中的调试探针比较器组成的全局布尔公式。当“触发器模式 (Trigger Mode) ”设置为 BASIC_ONLY 或 BASIC_OR_TRIG_IN 时 &#xff0c; 即启用基本触发器模式。使用“基本触发器设置 (Basic Trig…

【面试题】细说mysql中的各种锁

前言 作为一名IT从业人员&#xff0c;无论你是开发&#xff0c;测试还是运维&#xff0c;在面试的过程中&#xff0c;我们经常会被数据库&#xff0c;数据库中最经常被问到就是MySql。当面试官问MySql的时候经常会问道一个问题&#xff0c;”MySQL中有哪些锁&#xff1f;“当我…

数据结构-----Lambda表达式

文章目录 1 背景1.1 Lambda表达式的语法1.2 函数式接口 2 Lambda表达式的基本使用2.1 语法精简 3 变量捕获3.1 匿名内部类3.2 匿名内部类的变量捕获3.3 Lambda的变量捕获 4 Lambda在集合当中的使用4.1 Collection接口4.2 List接口4.3 Map接口 HashMap 的 forEach() 5 总结 1 背…

第十三届蓝桥杯真题:x进制减法,数组切分,gcd,青蛙过河

目录 x进制减法 数组切分 gcd 青蛙过河 x进制减法 其实就是一道观察规律的题。你发现如果a这个位置上的数x&#xff0c;b这个位置上的数是y&#xff0c;那么此位置至少是max(x,y)1进制。一定要把位置找对啊 #include <bits/stdc.h> using namespace std; typedef l…

如何卸载干净 IDEA(图文讲解)

更新时间 2022-12-20 11:一则或许对你有用的小广告 星球 内第一个项目&#xff1a;全栈前后端分离博客项目&#xff0c;演示地址&#xff1a;Weblog 前后端分离博客, 1.0 版本已经更新完毕&#xff0c;正在更新 2.0 版本。采用技术栈 Spring Boot Mybatis Plus Vue 3.x Vit…

如何开辟动态二维数组(C语言)

1. 开辟动态二维数组 C语言标准库中并没有可以直接开辟动态二维数组的函数&#xff0c;但我们可以通过动态一维数组来模拟动态二维数组。 二维数组其实可以看作是一个存着"DataType []"类型数据的一维数组&#xff0c;也就是存放着一维数组地址的一维数组。 所以&…

【C++成长记】C++入门 | 命名空间、输入输出、缺省参数

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;C​​​​​​​❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、C和C语言的区别和联系 二、命名空间 1、命名空间定义 2、命名空间使用 三、C输…