第15章 友元、异常和其他

1. 友元类的所有方法都可以访问原有类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数、或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相违背;相反,它们提高了公有接口的灵活性。

2. 下面的语句使Remote成为一个友元类:friend class Remote;

友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。先给出Tv类声明,再给出Remote类声明时,不需要在Tv类声明前前向声明Remote类,因为Tv类中的friend class Remote;已经指出Remote是一个类。

3. P606-P609可以选择让特定的类成员称为另一个类的友元,而不必让整个类都成为友元,但这样做必须小心排列各种声明和定义的顺序。

class Tv

{

friend void Remote::set_chan(Tv & t, int c);

}

在编译器在Tv类的声明中看到Remote类的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和set_chan()方法的声明。这意味着Remote类的完整声明(数据成员和成员函数的声明)必须放在Tv类的完整声明之前,而Remote的方法中提到了Tv对象,所以必须在Remote的完整声明前使用前向声明。排列顺序为

class Tv;        //前向声明

class Remote    //Remote声明

{

};

class Tv         //Tv声明+定义

{

};

//Remote方法定义

……

Remote方法中如果调用了Tv的方法,则Remote的声明中只能包含方法声明,并将实际的定义放在Tv类之后。

4. 在类方法定义中使用inline关键字,可以使其成为内联方法,此时类声明中不需要inline。

5. 在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类,包含类的成员函数可以创建和使用被嵌套类的对象,而仅当声明位于公有部分,才能在包含类的外面使用嵌套类。对类进行嵌套和包含不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型。

6. abort()函数在cstdlib中,其典型实现是向标准错误流发送消息abnormal program termination,然后终止程序。

7. C++异常是对程序运行过程中发生的异常情况的一种响应。异常提供了将控制权从程序的一部分传递到另一个部分的途径。异常类型可以是字符串或其他C++类型,通常为类类型。

8. 执行throw语句类似于执行返回语句,因为它将终止函数的执行,但函数不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。

9. 如果函数引发了异常,而没有try块或没有匹配的处理程序时,程序默认将调用abort函数,但可以修改这种行为。

10. 不建议使用异常规范,但至少应该知道它长什么样:

double harm(double a) throw(bad_thing);//可能抛出bad_thing异常

double marm(double a) throw();//不抛出异常

异常规范的作用之一是告诉用户可能需要使用try块。然而,这项工作也可使用注释轻松地完成。异常规范的另一个作用是让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。

C++11建议您忽略异常规范。然而,C++11确实支持一种特殊的异常规范:您可以使用新增的关键字noexcept指出函数不会引发异常:

double marm() noexcept;//有关这种异常规范是否必要和有用存在一些争议。

11. 假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这涉及栈解退(unwinding the stack)。

现在假设函数由于异常而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转移到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程称为栈解退。一个重要机制是,整个函数调用序列中的自动类对象的析构函数将被调用。

12. 虽然throw-catch机制类似于函数参数和函数返回机制,但还是有些不同之处。其中之一是函数的返回语句将控制权返回到调用该函数的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使catch块中指定的是引用。

class problem{…};

void super() throw (problem)

{

if(oh_no)

{

problem oops;

throw oops;

}

try{

super();

}

catch(problem & p)

{

}

p将指向oops的副本而不是oops本身。这是件好事,因为函数super()执行完毕后,oops将不复存在。顺便说一句,将引发异常和创建对象组合在一起将更简单:throw problem();

13. 您可能会问,既然throw语句将生成副本,为何代码中使用引用呢?毕竟,将引用作为返回值的通常原因是避免创建副本以提高效率。答案是,引用还有另一个特征:基类引用可以指向派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。

14. 如果有一个异常类继承层次结构,应该这样排列catch块,将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

15. 使用省略号…来表示异常类型时,可以捕获任何异常:

catch(…){//statement}

类似于switch中的default语句,适用于当不知道被调用的函数会引发哪些异常的情况。

16. 对于使用new导致的内存分配问题,C++最新的处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。但在以前,当无法分配请求的内存量时,new返回一个空指针。P633

17. 为了确保new在失败时返回空指针而不是抛出异常,可以如下使用new:

int *pi = new (std::nothrow) int;

int *pa = new (std::nothrow) int[500];

18. 异常被引发后,在两种情况下,会导致问题,即意外异常和未捕获异常。首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类极其派生类对象匹配),否则称为意外异常。如果异常不是在函数中引发的或者函数没有异常规范,则必须捕获它。如果没捕获(在没有try块或匹配的catch块时,将出现这种情况),则异常被称为未捕获异常。在默认情况下,都将导致程序异常终止。然而,可以修改程序对意外异常和未捕获异常的反应。

19. 未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。在默认情况下,terminate()调用abort()。可以调用set_terminate()函数来修改terminate()的这种行为。set_terminate()和terminate()都是在头文件exception中声明的。如果发生意外异常,程序将调用unexpected()函数,这个函数将调用terminate(),后者在默认情况下将调用abort()。set_unexpected()函数可以修改unexpected()的行为。

20. 与提供给set_terminate()的函数相比,提供给set_unexpected()的函数的行为受到更为严格的限制。具体地说,unexpected_handler函数可以为:

  • 通过调用terminate()、abort()或exit()来终止程序。
  • 引发异常(引发异常的结果见P640)

21. 仅使用throw,而不指定异常将导致重新引发原来的异常。

22. RTTI是运行阶段类型识别(Runtime Type Identification)的简称。这是新添加到C++的特性之一,很多老式实现不支持。另一些实现可能包含开关RTTI的编译器设置。RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。

23. RTTI的用途:基类指针可以指向派生类对象,并调用类方法的正确版本,在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,则并不需要真正知道对象的类型。但派生对象可能包含不是继承而来的方法,在这种情况下,只有某些类型的对象可以使用该方法,所以必须使用RTTI确定该基类指针指向的对象是否可以使用该方法。

24. C++有三个支持RTTI的元素。

  • 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0——空指针。
  • typeid运算符返回一个对type_info对象的引用。
  • type_info结构存储了有关特定类型的信息。

只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。

25. 通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针:

dynamic_cast<Type *> (pt)

否则,结果为0,即空指针。

26. 也可以将dynamic_cast用于引用,其用法稍微有些不同:没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的,它是在头文件typeinfo中定义的。因此,

27. 可以像下面这样使用该运算符,其中rg是对Grand对象的引用:

#include <typeinfo>

class Grand{//has virtual method};

class Superb : public Grand{…};

class Magnificent : public Superb{…};

try{

Superb & rs = dynamic_cast<Superb &>(rg);

}

catch(bad_cast &)

{

};

28. P647typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参数:

  • 类名
  • 结果为对象的表达式

typeid运算符返回一个对type_info对象的引用,其中,type_info是在头文件typeinfo中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。如果pg指向的是一个Magnificent对象,则下述表达式的结果为true,否则为false:

typeid(Magnificent) == typeid(*pg)

如果pg是一个空指针,程序将引发bad_typeid异常。该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。

type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串,通常是类的名称。例如,下面的语句显示指针pg指向的对象所属的类定义的字符串:

cout << “Now processing type ” << typeid(*pg).name() << “.\n”;

29. P648误用RTTI的例子,如果在if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast。

30. 类型转换运算符

在C++的创始人Bjarne Stroustrup看来,C语言的类型转换太过松散,为了更严格地限制允许的类型转换,Stroustrop添加了4个类型转换运算符,使转换过程更加规范。

  • dynamic_cast
  • const_cast
  • static_cast
  • reinterpret_cast

dynamic_cast使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换。

const_cast只能改变const或volatile属性。

const_cast <type_name> (expression)

也就是说,除了const和volatile特征可以不同外,type_name和expression的类型必须相同。通常用它来删除指向const值的指针的const标签。

注意:

const_cast不是万能的,它可以修改指向一个值的指针,但修改const的结果是不确定的。

#include <iostream>

using std::cout;

using std::endl;

void change(const int *pt, int n);

int main()

{

int pop1 = 38383;

const int pop2 = 2000;

cout << "pop1, pop2:" << pop1 << ", " << pop2 << endl;

change(&pop1, -103);

change(&pop2, -103);

cout << "pop1, pop2:" << pop1 << ", " << pop2 << endl;

return 0;

}

void change(const int *pt, int n)

{

int *pc;

pc = const_cast<int *>(pt);

*pc += n;

}

程序的运行结果为:

pop1, pop2:38383, 2000

pop1, pop2:38280, 2000

请按任意键继续. . .

可以看到,调用change()时,修改了pop1,但没有修改pop2。在change中,指针pc删除了const特征,因此可以用来修改指向的值,但仅当指向的值不是const才行。

static_cast的语法也相同:

static_cast<type-name> (expression)

仅当type_name可被隐式转换为expression所属的类型expression所属的类型可被隐式转换为type_name时,上述转换才是合法的。因此,在基类和派生类之间,可以用static_cast来进行向上转换或向下转换。也可以用在枚举值和整型值之间的转换,浮点值和整型值之间的转换等等。

reinterpret_cast运算符用于天生危险的类型转换P652。它不允许删除const,但会执行其他令人生厌的操作。

然而,reinterprete_cast运算符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是不能将函数指针转换为数据指针,反之亦然。

31. dynamic_cast和static_cast的区别:dynamic_cast运算符只允许沿类层次结构向上转换,而static_cast运算符运行向上转换或向下转换。static_cast运算符还允许枚举型和整型之间以及数值类型之间的转换。

C++ primer plus读书笔记——第15章 友元、异常和其他的更多相关文章

  1. C++ primer plus读书笔记——第17章 输入、输出和文件

    第17章 输入.输出和文件 1. 对键盘进行输入缓冲可以让用户在将输入传输给程序之前返回并更正.C++程序通常在用户按下回车键时刷新输入缓冲区. 2. 一些I/O类 streambuf类为缓冲区提供了 ...

  2. C++ primer plus读书笔记——第16章 string类和标准模板库

    第16章 string类和标准模板库 1. string容易被忽略的构造函数: string(size_type n, char c)长度为n,每个字母都为c string(const string ...

  3. C++ primer plus读书笔记——第14章 C++中的代码重用

    第14章 C++中的代码重用 1. 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现).获得接口是is-a关系的组成部分.而使用组合,类可以获得实现,但不能获得接口. ...

  4. C++ primer plus读书笔记——第13章 类继承

    第13章 类继承 1. 如果购买厂商的C库,除非厂商提供库函数的源代码,否则您将无法根据自己的需求,对函数进行扩展或修改.但如果是类库,只要其提供了类方法的头文件和编译后的代码,仍可以使用库中的类派生 ...

  5. C++ primer plus读书笔记——第12章 类和动态内存分配

    第12章 类和动态内存分配 1. 静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域运算符来指出静态成员所属的类.但如果静态成员是整形或枚举型const,则可以在类声明中初始化 ...

  6. C++ primer plus读书笔记——第11章 使用类

    第11章 使用类 1. 运算符重载是一种形式的C++多态. 2. 不要返回指向局部变量或临时对象的引用.函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据. 3. 运算符重载的格式如下: ...

  7. C++ primer plus读书笔记——第9章 内存模型和名称空间

    第9章 内存模型和名称空间 1. 头文件常包含的内容: 函数原型. 使用#define或const定义的符号常量. 结构声明. 类声明. 模板声明. 内联函数. 2. 如果文件名被包含在尖括号中,则C ...

  8. C++ primer plus读书笔记——第10章 对象和类

    第10章 对象和类 1. 基本类型完成了三项工作: 决定数据对象需要的内存数量: 决定如何解释内存中的位: 决定可使用数据对象执行的操作或方法. 2. 不必在类声明中使用关键字private,因为这是 ...

  9. C++ primer plus读书笔记——第8章 函数探幽

    第8章 函数探幽 1. 对于内联函数,编译器将使用相应的函数代码替换函数调用,程序无需跳到一个位置执行代码,再调回来.因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存. 2. 要使用内 ...

随机推荐

  1. Trie、并查集、堆、Hash表学习过程以及遇到的问题

    Trie.并查集.堆.Hash表: Trie 快速存储和查找字符串集合 字符类型统一,将单词在最后一个字母结束的位置上打上标记 练习题:Trie字符串统计 import java.util.*; pu ...

  2. JAVA题目:正整数n若是其平方数的尾部,则称n为同构数 如:5*5=25, 25*25=625 问: 求1~99中的所有同构数

    1 /*题目:正整数n若是其平方数的尾部,则称n为同构数 2 如:5*5=25, 25*25=625 3 问: 求1~99中的所有同构数 4 */ 5 //分析:将1-99分为1-9和10-99,用取 ...

  3. OGG-集成模式抽取与数据库参数streams_pool_size关系

    一.学习目标 Oracle数据库,使用OGG集成模式抽取进程启动时,如果没有配置合理的streams_pool_size参数可能会过一段时间就报错abend! 那么我们如何配置这个参数的大小?如何计算 ...

  4. 【Java】流、IO(初步)

    (这部分比较抽象且写的不是很好,可能还要再编辑) [概述] 流:流是一系列数据,包括输入流和输出流.你可以想象成黑客帝国的"代码雨",只要我们输入指令,这些数据就像水一样流进流出了 ...

  5. 敏捷史话(十四):敏捷之峰的攀登者 —— Jim Highsmith

    "我们希望,一起组成的敏捷联盟能够帮助到其他同行,帮他们用新的更'敏捷'的方式去思考软件开发.方法论和组织.做到这一点,我们就得偿所愿了."Jim Highsmith 在雪鸟会议结 ...

  6. 「Spring Boot 2.4 新特性」启动耗时详细监控

    背景 Spring Boot 项目随着项目开发过程中引入中间件数量的增加,启动耗时 逐渐增加. 笔者在 <Spring Boot 2.4.0 正式 GA,全面拥抱云原生>文章评论下发现了 ...

  7. Linux 递归修改后缀名

    1 修改命令 需要用到: find awk xargs 递归修改命令如下: find . -name '*.XXX' | awk -F "." '{print $2}' | xar ...

  8. IDEA main 函数的快捷键

    1.main()快捷键:psvm 当输入ps的时候编辑器就会有如下提示,可见比eclipse要很方便 2.System.out.println()快捷键:sout

  9. SpringBoot 启动慢?那是因为你不知道它

    前言 在 2021 年这个小学作文中的未来年份,没有想象中的汽车满天飞,也没有实现机器人满地跑.但牛逼的是我们都有一个共识: 知乎达到了人均 "谢邀~ 人在美国刚下飞机"的生活水平 ...

  10. k8s 运行单实例 mysql

    配置文件mysql.yaml --- apiVersion: v1 kind: Service metadata: name: mysql-01 spec: ports: - port: 3306 s ...