友元类用来表示一种关系,类似于电视与遥控器,它俩并不属于 is-a 的关系,也不是 has-a 的关系,但是遥控器可以控制电视,这种情况下,遥控器就可以作为电视的友元类。
class TV
{
private:
int state;
int volume;
int channel;
...
public:
friend class Remote; // 友元类的声明
enum { Off, On };
enum { TV, DVD };
bool volup() ;
...
};
class Remote
{
private:
int mode;
public:
Remote(int m = TV::TV) : mode(m) {}
bool volup(TV &t) { return t.volup(); }
void set_chan(TV &t, int t) { t.channel = c; }
...
};
友元可以声明在公有、保护、私有部分,它声明的位置无关紧要。
也可以选择只将 Remote 的某些方法作为 TV 的友元函数,而不是整个 Remote 作为友元类。这需要前置声明:
class TV; // 前置声明 TV 类
class Remote
{
private:
int mode;
public:
enum { TV, DVD };
Remote(int m = TV:TV) : mode(m) {}
bool volup(TV &t);
void set_chan(TV &t, int t);
...
};
class TV
{
private:
int state;
int volume;
int channel;
...
public:
friend void Remote::set_chan(TV &t, int c); // 类某方法作为友元函数的声明
enum { Off, On };
enum { TV, DVD };
bool volup() ;
...
};
inline bool Remote::volup(TV &t)
{
return t.volup();
}
inline void Remote::set_chan(TV &t, int c)
{
t.channel = c;
}
Remote 类的公有方法的参数几乎都是 TV 类对象,所以应该先声明 TV 类,但是 TV 类中有 Remote 类的友元函数,若不先声明 Remote 类的话,编译器并不知道 Remote 类的方法 set_chan(),解决这个矛盾的方法是前置声明 TV 类,这样就有了 TV 类,然后声明的 Remote 类就可以知道有 TV 类对象,最后声明的 TV 类的友元函数也找到了。
但是 Remote 类的方法要定义成内联函数,就需要在声明完 TV 类后,再定义,因为如果在 Remote 类内直接定义的话,当时并没有声明 TV 类的数据成员和方法,这样编译器找不到 TV 类的这些成员。内联函数是内部链接的,所以最好定义在头文件里。
Remote 类和 TV 可以互为友元类:
class TV
{
private:
friend class Remote;
public:
void buzz(Remote & r);
...
};
class Remote
{
private:
friend class TV;
public:
bool volup(TV & t) { t.volup(); }
...
}
inline void buzz(Remote & r)
{
...
}
TV::buzz() 方法要在 Remote 类声明后定义,因为它的参数表明,它可能会用到 Remote 类的方法,因此在定义之前,需要先声明 Remote 类。
有两个类 Pro 和 Ana,它们都有内部时钟,如果希望实现时钟同步:
class Ana; // 前置声明
calss Pro
{
friend void sync(Ana &a, const Pro & p);
friend void sync(Pro & p, const Ana &a);
...
};
class Ana
{
friend void sync(Ana &a, const Pro & p);
friend void sync(Pro & p, const Ana &a);
...
};
void sync(Ana &a, const Pro & p)
{
...
}
void sync(Pro & p, const Ana &a)
{
...
}
与包含类不同,嵌套类只是在类内定义一种类类型,并不创建类成员。包含嵌套类的类成员函数可以创建和使用嵌套类对象;当嵌套类定义在公有部分,可以在类外使用嵌套类。
class Queue
{
class Node // 嵌套类定义
{
public:
Item item;
Node * next;
Node(const Item & i) : item(i), next(0) {}
};
...
};
如果要在方法定义文件中定义 Node 构造函数,需要两次作用域解析运算符:
Queue::Node::Node(const Item & i) : item(i), next(0) {}
嵌套类如果在私有部分,那么包含类的方法可以创建使用 Node 类对象,类外不行,包含类的派生类同样不能直接创建 Node 类对象。
如果在保护部分,则派生类方法可以创建 Node 类对象,类外不行。
如果在公有部分,则派生类和类外都可以直接创建 Node 类对象,不过类外要使用类限定符:
Queue::Node node;
嵌套类的作用域与类内枚举相同,使用方法也类似,下面是嵌套类、枚举、结构的作用域:
声明位置 | 包含它的类能否使用它 | 派生类能否使用它 | 类外能否使用 |
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,通过类限定符 |
具体包含类对嵌套类的访问控制,看嵌套类的访问控制,即包含类方法创建的嵌套类对象,对嵌套类的访问同常规类一样,能访问公有数据和方法,私有和保护不能直接访问。
总之,嵌套类声明的位置决定了类的作用域或可见性(能否创建这个类对象),嵌套类的访问控制(公有、保护、私有、友元)决定对嵌套类成员的访问权限。
假设一个程序的功能是求两个数的调和平均数(倒数的平均数的倒数),当两个数互为相反数时,分母是0,这个时候程序会异常中断,异常处理可以作为一种工具预防这些情况。
#include <iostream>
#include <cstdlib>
double hmean(double a, double b);
using namespace std;
int main()
{
double x, y, z;
cout << "Enter two numbers: ";
while (cin >> x >> y)
{
z = hmean(x, y);
cout << "hmean is :" << z << endl;
cout << "Enter two numbers<q to quit>: ";
}
return 0;
}
double hmean(double a, double b)
{
if (a == -b)
{
cout << "not allow" << endl;
std::abort();
}
else
return 2.0 * a *b / (a + b);
}
abort() 函数位于头文件 cstdlib(或 stdlib.h)中,典型实现像标准错误流(cerr)发送消息 abnormal program termination(消息异常终止),终止后返回一个随具体实现而差异的值,告诉操作系统(若程序由另一个程序调用,则告诉父进程),处理失败。是否刷新缓冲区,取决于实现。也可以不用abort 使用 exit() ,但是后者不显示消息。
#include <iostream>
double hmean(double a, double b);
using namespace std;
int main()
{
double x, y, z;
cout << "Enter two numbers: ";
while (cin >> x >> y)
{
try
{
z = hmean(x, y);
}
catch (const char * s)
{
cout << s << endl;
cout << "Enter two numbers<q to quit>: ";
continue;
}
cout << "hmean is :" << z << endl;
cout << "Enter two numbers<q to quit>: ";
}
return 0;
}
double hmean(double a, double b)
{
if (a == -b)
{
throw "bad hmean() arguments: a = -b";
}
else
return 2.0 * a *b / (a + b);
}
这种异常处理机制,由 try、catch 和 throw 三部分组成,try 块(花括号内部)内部表示可能引发异常的代码段,如果在 try 块外面才调用 hmean() 函数,则异常无法处理;catch 块类似 switch 块,根据 throw 后面的类型选择处理哪种异常,catch 块可以有多个,它的参数类似于函数定义时的参数,上述例程表明,throw 后面的是一个字符串,catch 的参数是字符串指针,抛出的字符串会赋值到这个指针上,进而处理这个异常。
throw 语句类似于返回语句,它也终止函数的执行,回退到包含 try 块的函数,上面的例程是 main() 函数,main 会寻找 catch 块中是否有处理 throw 类型的块。
当没有 try 块或处理这种类型的 catch 时,会调用 abort() 函数。
class bad_mean
{
private:
double v1;
double v2;
public:
bad_mean(double a = 0, double b = 0) : v1(a), v2(b) {}
void msg();
};
void bad_mean::msg()
{
std::cout << "hmean :" << v1 << ", " << v2
<< "invalid arguments: a = -b\n";
}
catch(bad_mean &b)
{
b.msg();
std::cout << "try again.\n";
continue;
}
double hmean(double a, double b)
{
if (a == -b)
{
throw bad_mean(a, b);
}
else
return 2.0 * a *b / (a + b);
}
将例程中的函数进行如上更改,可以使 throw 一个类对象,而 catch 参数为该类对象,进而处理异常。
C++11 建议忽略异常规范,并新增了关键字 noexcept 指出函数不会引发异常:
double marm() noexcept;
程序在运行过程中,每次调用函数时,都会将调用函数指令的地址放到栈中,期间的自动变量也会放到栈中,函数调用完后,逐步释放栈顶,如果释放的是类对象,也会调用析构函数,直到调用指令的地址,然后继续执行程序。
而当调用的函数出现异常而终止时,栈并不返回到第一个调用指令地址上,而是回退到 try 块中的第一个调用指令地址上,然后程序控制权会转移到异常处理块上,而不是调用指令后的下一条指令。这个过程叫做栈解退。栈解退的特性是与函数返回一样,也会在回退到 try 的过程中释放途经的自动变量等,是对象的也会调用析构函数,与函数返回不同的是,函数返回只会释放函数调用中,放在栈中的自动变量,而栈解退会释放 throw 和 try 之间整个函数调用序列放在栈中的对象。异常的栈解退,是throw 后栈会返回到能 catch 到这个异常的位置,并释放过程中的对象。
throw—catch 是这样的机制:throw 将控制权返回到能够捕获这种异常的 catch 语句,即当有嵌套式的 try—catch—throw 结构时,内部 throw 的对象,内部的 catch 没有对应的情况下,会返回到上一层的 catch,直到有捕获这种异常的 catch,若没有对应的 catch,程序将异常中断。
throw 异常时,编译器总会创建这个异常的副本,即使 catch 块指定的是引用,它也是指向的这个副本的引用。这样的好处是引发异常的块调用完毕后,异常对象会被释放。catch 选择指向引用的另一个好处是,若 throw 的异常包括基类和派生类的话,catch 也可以捕获。注意,当 throw 的对象是派生类时,会被第一个对应的 catch 捕获,因此 catch 的排列顺序最好与派生顺序相反,即捕获基类对象的 catch 放在最后。
当一个调用另一个函数的函数可能会引发异常,但是你并不知道异常的类型,可以:
catch (...) // 使用省略号
{
// statement
}
用省略号来表示异常类型,从而捕获任何异常,这类似于 switch 对应的 default。若多个catch,catch(...) 放最后。
exception头文件(exception.h 或 except.h) 定义了 exception 类,可以将这个类作为其他异常的基类,它有一个虚成员函数 what() ,它返回一个字符串,该字符串随实现而定,因此可以在派生类中重新定义它:
#include <exception>
class bad_hmean : public std::exception
{
public:
const char * what() { return "bad arguments to hmean()"; }
...
};
try
{
...
}
catch(std::exception & e)
{
cout << e.what() << endl;
...
}
当 throw 的对象没有对应的 catch 捕获时,发生意外异常,默认情况下,程序异常终止;或者当异常不是在 try 块中引发的,则异常称为未捕获异常,将引起程序异常终止。可以通过修改程序对意外异常和未捕获异常的反应。
程序并不会立刻终止,而是先调用函数 terminate()。默认情况下,terminat() 函数调用 abort() 函数。可以通过调用 set_terminat() 修改 terminat() 调用的函数,set_terminat() 声明在 exception 头文件中:
typedef void (*teminate_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
typedef 语句使得 terminate_handler 成为了一个指向没有返回值和参数的函数的指针。set_terminate() 的参数是,没有返回值和参数的函数的名称(或地址),若多次调用 set_terminate()函数,则terminat() 调用的函数时最后一次调用 set_terminate() 设置的函数:
void myQuit()
{
cout << "Terminating due to uncaught exception.\n";
exit(5);
}
set_terminate(myQuit);
这样在发生未捕获异常时,terminate 函数会调用 myQuit 函数,打印log。
当程序发生意外异常时,程序会调用 unexcepted() 函数,这个函数将调用 terminat() 函数,后者默认情况下调用 abort() 函数。同样有 set_unexcepted() 函数设置 unexcepted 函数的行为。
typedef void (*unexcepted_handler) ();
unexcepted_handler set_unexcepted(unexcepted_handler f) throw(); // C++ 98
unexcepted_handler set_unexcepted(unexcepted_handler f) noexcept; // C++ 11
void unexcepted(); // C++ 98
void unexcepted() noexcept; // C+0x
通过 set_unexcepted 函数,可以选择调用 terminate 函数、abort() 函数或者 exit() 函数终止程序,也可以选择重新抛出异常来与原 catct 匹配,继续程序。当选择后者时,如果新引发的异常也与 catch 不匹配,且异常规范中没有包括 std::bad_exception 类型,则程序会调用 terminate() 函数。std::bad_exception 是从 exception 类派生来的,声明位于头文件 exception 中;如果新异常与 catch 不匹配,但是包含了 std::bad_exception 类型,则不匹配的类型会被 std::bad_exception 异常取代。
可以这样做来捕获所有异常(无论是预期的异常还是意外异常):
#include <exception>
using namespace std;
void myUnexcepted()
{
throw std::bad_exception(); // or just throw
}
set_unexcepted(myUnexcepted);
try
{
...
}
catch(out_of_bounds & ex)
{
...
}
catct(bad_exception & ex)
{
...
}
异常处理应该在设计程序时就加入,而不是以后再添加,这会增加程序代码并降低程序运行速度。异常规范不适用于模板,以为模板引发的异常可能不同的具体化都不一样。而且在动态内存分配时异常可能会出现问题:
void test1(int n)
{
string mesg("I'm trapped in an endless loop");
...
if (oh_no)
throw exception();
...
return;
}
当出现异常时,程序终止,但由于栈解退,在释放 mesg 对象时,会调用 string 的析构函数,释放string 类的 new;
void test2(int n)
{
double * ar = new double [n];
...
if (oh_no)
throw exception();
...
delete [] ar;
return;
}
当引发异常时,栈解退时,将直接释放 ar。但函数在 delete 之前终止,指针消失了,这块内存被泄露了。这种情况可以在 catch 块中增加一些清理代码:
void test2(int n)
{
double * ar = new double [n];
...
if (oh_no)
throw exception();
...
catch (exception & ex)
{
delete [] ar;
throw;
}
delete [] ar;
return;
}
这将增加疏忽和产生其他错误的机会。也可以通过智能指针来解决。
RTTI指运行阶段类型识别( Runtime Type Identification) 的简称。
当有一个基类派生出了多个类,则基类指针可以指向所有的派生类,当有一个这样的函数:处理一些信息后,选择一个类,创建该类的对象,并返回它的地址,这个地址当然可以赋给基类指针,但是如何知道这个地址指向的是哪个派生类呢?
我们要确定该地址指向哪个派生类的原因是,如果我们需要调用一个虚函数时,当然可以不知道是哪个派生类,但是如我们需要调用一个派生类的方法不是继承来的时,就需要知道具体是哪个派生类了;或者在调试代码时,想追踪生成的对象。
C++ 有 3 个支持 RTTI 的元素:
1. dynamic_cast 运算符将使用一个基类指针来生成一个指向派生类的指针;否则,返回空指针。
2. typeid 运算符返回一个指出对象类型的值。
3. type_info 结构储存了有关特定类型的信息。
RTTI 只能用在包含虚函数的类层次结构,因为只有这种类层次用基类指针指向派生类调用方法才更有意义。
4.2.1. dynamic_cast 运算符
该运算符负责回答:是否可以安全的将对象地址赋给特定类型的指针,这个问题:
class Grand
{
private:
int hold;
public:
Grand(int h = 0) : hold(h) {}
virtual void speak() const { cout << "I'm a grand class!\n"; }
};
class Superb : public Grand
{
public:
Superb(int h = 0) : Grand(h) {}
void speak() const { cout << "I'm a superb class!\n"; }
virtual void say() const { cout << "I hold superb value: " << h << endl; }
};
class Magn : public Superb
{
private:
char ch;
public:
Magn(int h = 0, c = 'A') : Superb(h), ch(c) {}
void speak() const { cout << "I'm a Magn class!\n"; }
void say() const { cout << "I hold magn value: " << h
<< endl; }
};
Superb * pm = dynamic_cast<Superb *>(pg);
上述代码表示:指针 pg 的类型是否能够安全的转换为 Superb * ,成功则返回对象,否则,返回空指针。
若指向的对象 (*pt) 的类型为 Type 或是从 Type 直接、间接派生而来的类型,则下面的表达式将指针转换为 Type 类型的指针:
dynamic_cast<Type *>(pt); // 否则,结果为空指针
当有程序为:
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <typeinfo>
using namespace std;
Grand * Getone();
int main()
{
srand(time(0));
Grand * pg;
Superb * ps;
for (int i = 0; i < 5; i++)
{
pg = Getone();
pg->speak();
if ( ps = dynamic_cast<Superb *>(pg))
ps->say();
if (typeid(Magn) == typeid(*pg))
cout << "Yes, you are Magn.\n";
}
return 0;
}
Grand * Getone()
{
Grand * p;
switch(rand() % 3)
{
case 0 : p = new Grand(rand() % 100);
break;
case 1 : p = new Superb(rand() % 100);
break;
case 2 : p = new Magn(rand() % 100, 'A' + rand() % 26);
break;
}
return p;
}
当 pg 指向可以转换为 Superb 的对象时,就会调用虚方法 say()。
dynamic_cast 也可以用于引用,但是由于没有于空指针对应的引用值,因此无法用特殊的引用值表示失败。当引用的请求不正确时,dynamic_cast 将引发类型为 bad_cast 的异常,这种异常也是由 exception 类派生出来的,它在头文件 typeinfo 中定义,因此可以这样用:
#include <typeinfo>
...
try
{
Superb & rs = dynamic_cast<Superb &>(rg);
...
}
catch(bad_cast &)
{
...
}
4.2.2. typeid 运算符和 type_info 类
typeid 运算符能够确定两个对象是否为同种类型,可以接受两种参数:类名;结果为对象的表达式。
它返回一个 type_info 对象的引用,type_info 是在头文件 typeinfo (以前为 typeinfo.h)中定义的一个类。该类重载了 == 和 != 运算符,以便用于比较。如上面例程里的 typeid(Magn) == typeid(*pg)
如果 pg 是一个空指针,将引发 bad_typeid 异常,该异常也是 exception 类派生的,在头文件 typeinfo 中声明。
type_info 类的实现一般由厂商自己实现,但包含一个 name() 成员,该方法返回一个随实现而异的字符串,一般是类的名字:
cout << "Now processing type: " << typeid(*pg).name() << endl;
由于 C 语言的类型转换太过松散,C++ 采取了 4 个更严格的类型转换运算符:dynamic_cast;const_cast;static_cast;reinterpret_cast。
const_cast 运算符执行于一种类型转换,即 const 或 volatile,语法为:
const_cast<type-name>(expression);
其中 expression 与 type-name 的类型必须相同,只有 const 或者 volatile 可以不同,即:
High bar;
const High * pbar = &bar;
High * pb = const_cast<High *>(pbar); // valid
const Low * pl = const_cast<const Low *>(pbar) // invalid
上述例子,将 pbar 转换为非 const 指针,并将地址赋给了 pb,使得 *pb 可以修改 bar 的值;而 pbar 与 Low * 类型不同,因此不行。这个运算符是为了应对这样的情况:有一个值,它大部分时间是常量,但是有时也会需要修改。
而通用的 C 语言的类型转换,也可以修改 const,但是也可以修改类型,使用 const_cast 运算符更安全。但有的时候,const_cast 修改的结果是不确定的:
void change(const int * pt, int n);
int main()
{
int p1 = 38383;
const int p2 = 2000;
cout << p1 << endl;
cout << p2 << endl;
change(&p1, -103);
change(&p2, -103);
cout << p1 << endl;
cout << p2 << endl;
return 0;
}
void change(const int * pt, int n)
{
int * pc;
pc = const_cast<int *> pt;
*pc += n;
}
这种情况下,因为 change 的参数是 const 指针,因此当 p2 传入函数中时,可能被编译器阻止修改 p2 的值,这样两次的输出,可能只有 p1 的值改变了。
static_cast 运算符的用法:
static_cast <type-name> (expression);
这个运算符表示:仅当 type-name 所属类型可以隐性转换为 expression 类型,或者 expression 所属类型可以隐性转换为 type-name 类型时,转换才是合法的,否则将出错。假设 Low 是 High 的派生类,Pond 是一个无关类,则运用该运算符,Low 到 High 和 High 到 Low 都是合法的,而 Low 或 High 转换为 Pond 是非法的:
High bar;
Low blow;
High * pb = static_cast<High *>(&blow); // valid
Low * pl = static_cast<Low * >(&bar); // valid
Pond * pp = static_cast<Pond *>(&blow); // invalid
reinterpret_cast 运算符用于天生危险的类型转换,它的用法:
reinterpret_cast <type-name> (expression);
struct dat {short a; short b;};
long value = 0xA224B118;
dat * pd = reinterpret_cast<dat *> (&value);
cout << hex << pd->a; // 输出value的前两个字节
该运算符并非支持所有的类型转换。如,可以将指针类型转换为足以储存指针表示的整型,但是不能将指针转化为更小的整型或浮点型,也不能将函数指针转换为数据指针,反之也不行。