C++ primer plus读书笔记——第15章 友元、异常和其他
第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运算符还允许枚举型和整型之间以及数值类型之间的转换。