《C++ Primer Plus》(第6版)中文版—学习笔记—友元、异常和其他

第15章 友元、异常和其他

友元

友元类

友元类的作用是将一个类成为另一个类的友元,那么具体什么时候会用到友元呢?

比如电视和遥控机,用C++语言表示为Tv和Remote类,我们知道,这两个类既不是is-a关系,也不是has-a关系,但是实际生活中,遥控机是可以控制电视的,也就是说Remote类需要可以改变Tv类中的某些值,这就需要Remote类作为Tv类的一个友元。

class Tv
{
    
    
public:
	friend class Remote;
	...
}

上面就是Remote作为Tv类的一个友元的Cpp写法。friend class Remote

友元声明可以位于公有、私有或保护部分,其所在的位置也无关紧要。由于Remote类提到了Tv类,所以编译器必须了解Tv类后,才能处理Remote类,为此,最简单的方法是首先定义Tv类。也可以使用前向声明(后面说)。

// tv.h -- Tv and Remote classes
#ifndef TV_H_
#define TV_H_

class Tv
{
    
    
public:
    friend class Remote;   // Remote can access Tv private parts
    enum {
    
    Off, On};
    enum {
    
    MinVal,MaxVal = 20};
    enum {
    
    Antenna, Cable};
    enum {
    
    TV, DVD};

    Tv(int s = Off, int mc = 125) : state(s), volume(5),
        maxchannel(mc), channel(2), mode(Cable), input(TV) {
    
    }
    void onoff() {
    
    state = (state == On)? Off : On;}
    bool ison() const {
    
    return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {
    
    mode = (mode == Antenna)? Cable : Antenna;}
    void set_input() {
    
    input = (input == TV)? DVD : TV;}
    void settings() const; // display all settings
private:
    int state;             // on or off
    int volume;            // assumed to be digitized
    int maxchannel;        // maximum number of channels
    int channel;           // current channel setting
    int mode;              // broadcast or cable
    int input;             // TV or DVD
};

class Remote
{
    
    
private:
    int mode;              // controls TV or DVD
public:
    Remote(int m = Tv::TV) : mode(m) {
    
    }
    bool volup(Tv & t) {
    
     return t.volup();}
    bool voldown(Tv & t) {
    
     return t.voldown();}
    void onoff(Tv & t) {
    
     t.onoff(); }
    void chanup(Tv & t) {
    
    t.chanup();}
    void chandown(Tv & t) {
    
    t.chandown();}
    void set_chan(Tv & t, int c) {
    
    t.channel = c;}
    void set_mode(Tv & t) {
    
    t.set_mode();}
    void set_input(Tv & t) {
    
    t.set_input();}
};
#endif

友元成员函数

其实也可以使得一个类中的成员函数成为另一个类中的友元成员函数,但是这样做会有一些麻烦,即在声明和定义的时候需要注意顺序。比如要使得Remote::set_chan()成为Tv类的友元的方法是,在Tv类声明中将其声明为友元

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

要使编译器能够处理这条语句,它必须知道Remote的定义。否则,它无法知道Remote是一个类,而set_chan是这个类的方法。这意味着应将Remote的定义放在Tv的定义前面。Remote的方法提到了Tv对象,而这意味着Tv定义应当位于Remote定义之前。避开这种循环依赖的方式是,使用**前向声明。**为此,需要在Remote定义的前面插入下面的语句:

class Tv;		// forward declaration

这样,排序次序应如下:

class Tv;
class Remote {
    
    ...};
class Tv {
    
    ...};

那么能不能这样排序呢?

class Remote;
class Tv {
    
    ...};
class Remote {
    
    ...};

答案是不能。因为编译器在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和set_chan方法的声明。

通过在方法定义中使用inline关键字,任然可以使其成为内联方法

其他友元关系

class Tv
{
    
    
friend class Remote;
public:
	void buzz(Remote & r);
	...
};
class Remote
{
    
    
friend class Tv;
public:
	void Bool volup(Tv & t) {
    
     t.volup(); }
	...
};
inline void Tv::buzz(Remote & r)
{
    
    
	...
}

共同的友元

class Analyzer;
class Probe
{
    
    
	friend void sync(Analyzer & a, const Probe & p);		// sync a to p
	friend void sync(Probe & p, const Analyzer & a);		// sync p to a
	...
}
class Analyzer
{
    
    
	friend void sync(Analyzer & a, const Probe & p);		// sync a to p
	friend void sync(Probe & p, const Analyzer & a);		// sync p to a
	...
}

嵌套类

将一个类声明放在另一个类中,这就叫嵌套类,它通过提供新的类型类作用域来避免名称混乱。

包含类的成员函数可以创建和使用被嵌套类的对象,而仅当申明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

对类进行嵌套与包含并不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。之前就有过类中嵌套结构体的例子出现。

class Queue
{
    
    
	class Node
	{
    
    
	public:
		Item item;
		Node * next;
		Node(const Item & i) : item(i), next(0) {
    
     }
	};
	...
};
// 由于前面所说,嵌套类中在类中只是声明,所以如果有必要,需要在构造函数中给其定义
bool Queue::enqueue(const Item & item)
{
    
    
    if (isfull())
        return false;
    Node * add = new Node(item);
    ...
}
// 假设像在方法文件中定义构造函数,则定义必须指出Node类是在Queue类中定义的。这是通过使用两次作用域运算符来完成的。
Queue::Node::Node(const Item & i) : item(i), next(0) {
    
     }

嵌套类、结构和枚举的作用域特征

声明设置 包含它的类是否可以使用它 从包含它的类派生而来的类是否可以使用它 在外部是否可以使用
私有部分
保护部分
公有部分 是,通过类限定符来使用

异常

调用abort()

abort函数位于cstdlib或者stdlib.h头文件中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缓冲区(用于储存读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

int main()
{
    
    
	...
	hmean(1, -1);
	...
}
double hmean(double a, double b)
{
    
    
	if (a == -b)
	{
    
    
        std::cout << "untenabel arguments to hmean()\n";
        std::abort();
	}
}

注意,在hmean()中调用abort()函数将直接终止程序,而不是先返回到main()。

返回错误码

下面是返回错误码的方式

#include <cfloat>

bool hmean(double a, double b, double * ans)
{
    
    
	if (a == -b)
	{
    
    
		*ans = DBL_MAX;
		return false;
	}
	else
	{
    
    
		*ans = 2.0 * a * b / (a + b);
		return true;
	}
}

他不同之前的实现方式,将函数返回修改为bool类型,这样就能通过返回的类型判断是否存在错误,DBL_MAX需要提供头文件cfloat

异常机制

对异常的处理有3个组成部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用try块

throw语句实际上是跳转,即命令程序调到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块。

try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引起的异常。

那么上面这三个元素是如何协同工作的呢?请看下面的例子

// error3.cpp -- using an exception
#include <iostream>
double hmean(double a, double b);

int main()
{
    
    
    double x, y, z;

    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y)
    {
    
    
        try {
    
                       // start of try block
            z = hmean(x,y);
        }                       // end of try block
        catch (const char * s)  // start of exception handler
        {
    
    
            std::cout << s << std::endl;
            std::cout << "Enter a new pair of numbers: ";
            continue;
        }                       // end of handler
        std::cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b)
{
    
    
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b); 
}

原文有详细流程图。

将对象用作异常类型

设计一个类,用于异常类

class bad_hmean
{
    
    
private:
	double v1;
	double v2;
public:
	bad_mean(int a = 0, int b = 0) : v1(a), v2(b) {
    
     }
	void mesg();
};
inline void bad_hmean::mesg()
{
    
    
	std::cout << "hmean(" << v1 << ", " << v2 << "): "
			  << "invalid arguments: a = -b\n";
}

上面就是用于异常特制的一个类,在原本的hmean函数中我们就可以这么写

if (a == -b)
{
    
    
	throw bad_hmean(a,b);
}

异常规范和C++11

栈解退

假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。

首先来看一看C++通常是如何处理函数调用和返回的。C++通常通过将信息放在栈中来处理函数调用。具体地说,程序将调用函数的指令的地址(返回地址)放在栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放在栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,依次类推。当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,依次类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。

现在假设函数由于出现异常(而不是由于返回)而终止,则程序也被释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

其他异常特性

exception类

需要包含exception头文件

代码可以引发exception异常,也可以将exception类用作基类。有一个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特性随实现而异。然而,由于这是一个虚方法,因此可以在从exception派生而来的类中重新定义:

#include <exception>
class bad_hmean : public std::exception
{
    
    
public:
	const char * what() {
    
     return "bad arguments to hmean()"; }
	...
};
class bad_gmean : public std::exception
{
    
    
public:
	const char * what() {
    
     return "bad arguments to gmean()"; }
};

如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获他们:

try {
    
    
...
}
catch(std::exception & e)
{
    
    
	cout << e.what() << endl;
	...
}

C++库定义了很多基于exception的异常类型

stdexcept异常类

头文件stdexcept定义了其他几个异常类。logic_error和runtime_error类都以公有方式从exception派生而来

class logic_error : public exception {
public:
	explicit logic_error(const string& what_arg);
	...
};
class domain_error : public logic_error {
public:
	explicit domain_error(const string& what_arg);
	...
};
  1. logic_error
    • domain_error
    • invalid_argument
    • length_error
    • out_of_bounds
  2. runtime_error
    • range_error
    • overflow_error
    • underflow_error

bad_alloc异常和new

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

空指针和new

很多代码都是在new在失败时返回空指针时编写的。为处理new的变化,有些编译器提供了一个标记(开关),让用户选择所需的行为。当前C++标准提供了一种在失败时返回空指针的new,其用法如下:

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

异常、类和继承

异常何时会迷失方向

未捕获异常

未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。在默认情况下,terminate()调用abort()函数。可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为。为此,可以调用set_terminate()函数。set_terminate()和terminate()都是在头文件exception中声明的:

typedef void (*terminate_handler) ();
terminate_handler set_terminate(terminate_handler f) throw(); // C++98
terminate_handler set_terminate(terminate_handler f) noexcept(); // C++11
void terminate();		// C++98
void terminate() noexcept;	// C++11

下面是如何修改terminate函数所要调用的函数

void myQuit()
{
    
    
	cout << "Terminating due to uncaught exception\n";
	exit(5);
}
// 最后,在程序的开头,将终止操作指定为调用该函数
set_terminate(myQuit);

意外异常

未捕获异常是没有try{}catch{}块,与未捕获异常不同,意外异常虽有try块,但是并没有正确指出,所以会引发意外异常。而意外异常调用unexpected函数,与terminate一样,也可以自己定义函数替换abort

typedef void (*unexpected_handler) ();
unexpected_handler set_terminate(unexpected_handler f) throw(); // C++98
unexpected_handler set_terminate(unexpected_handler f) noexcept(); // C++11
void unexpected();		// C++98
void unexpected() noexcept;	// C++11

有关异常的注意事项

RTTI

Runtime Type Identification,运行阶段类型识别,RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方法。

C++有3个支持RTTI的元素

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

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

dynamic_cast运算符

先举个例子

class Grand {
    
     // has virtual methods};
class Superb : public Grand {
    
     ... };
class Magnificent : public Superb {
    
     ... };
// 假设有下面的指针
Grand * pg = new Grand;
Grand * ps = new Superb;
Grand * pm = new Magnificent;
    
Magnificent *p1 = (Magnificent *) pm;		//#1
Magnificent *p2 = (Magnificent *) pg;		//#2
Superb *p3 = (Magnificent *) pm;			//#3

我们来探讨类型转换是否安全的问题。其中#1是安全的,Grand为基类,pm指针又指向派生类Magnificent,后又将这地址赋给Magnificent类型的指针变量。#2就不安全,pg类型为基类Grand,而基类对象的地址赋给派生类是不可能的。

dynamic_cast的语法,其中pg是一个指针,指向一个对象

Superb * pm = dynamic_cast<Superb *>(pg)

那么指针pg的类型是否可以被安全地转换为Superb*?如果可以,运算符将返回对象的地址,否则返回一个空指针。

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

#include <typeinfo>// for bad_cast
...
try {
    
    
	Superb & rs = dynamic_cast<Superb &>(rg);
	...
}
catch(bad_cast &) {
    
    
	...
};

typeif运算符和type_info类

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

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

typeid运算符返回一个对type_info对象的引用,其中type_info是在头文件typeinfo(以前为typeinfo.h)中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。

typeif(Magnificent) == typeid(*pg)

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

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

name()成员是type_info类的一个成员,该函数返回一个随实现而变异的字符串。

误用RTTI的例子

类型转换运算符

  • dynamic_cast
  • const_cast
  • static_cast
  • reinterpret_cast

此内容我认为太细化,请以后细看原文

猜你喜欢

转载自blog.csdn.net/weixin_49643423/article/details/114233625