友元、异常和其他相关问题

友元

友元类

友元函数用于类的扩展接口,类并非只能拥有友元函数,还能够将类作为友元类。友元类的所有方法都可以访问原始类的私有成员和保护成员。
以电视机和遥控器的关系,解释友元的作用以及相关问题

友元的声明

友元声明的位置无关紧要,可以在原始类的私有、公有或保护部分。简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TV
{
private:
friend class Remote;
...
};
class Remote
{
...
public:
void set_chan(TV & t);
...
};

如上面代码所示,只需要在原始类中任意位置friend class Remote,就可以声明友元类了。

  1. 但是问题来了,Remote类中提到了TV类,,所以编译器必须先了解TV类,才能处理Remote类。上面的例子就是把TV的定义放在Remote前面。那么如果TV中包含了Remote成员函数,Remote中包含了TV类怎么办呢???这就用到前向声明(forward declaration)。【如果让Remote类成为友元类,是不用前向声明的,因为那条语句就说明Remote是个类】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Remote;
    class TV
    {
    private:
    friend void Remote::set_chan(TV & t);
    ...
    };
    class Remote
    {
    ...
    public:
    void set_chan(TV & t);
    ...
    };
  2. 是上述代码这样吗?显然,这样是错误的!!!,因为虽然Remote的声明放在前面,但是编译器并没有看到随后TV类中Remote的那个方法的声明。正确打开方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class TV;
    class Remote
    {
    ...
    public:
    void set_chan(TV & t);
    ...
    };
    class TV
    {
    private:
    friend void Remote::set_chan(TV & t);
    ...
    };
  3. 那么问题又来了,如果void set_chan(TV & t) {t.function();},这样就会造成2中提到的问题,就循环了。所以这里采用内联函数吧。
    友元还有很多内容,比如两个类互相为友元,两个类有一个共同的友元等,处理原则和上面说的几个点一样:在你使用某个类、某个类的成员函数之前,你声明这个类、这个成员函数了吗?编译器就是看一点。

    异常

    异常就是像我这种程序猿给自己挖的坑,异常处理就是程序猿自己给自己兜底。程序运行会遇到很多异常情况导致程序无法运行下去,为了防止这种情况,就有了C++异常:为处理这些情况强大、灵活的工具。通常情况有这样几种处理方法。

    调用abort()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include <iostream>
    #include <cstdlib> // abort() 函数头文件
    using namespace std;
    double hmean(double a, double b);
    int main()
    {
    double x, y, z;
    while(cin >> x >> y)
    {
    z = hmean(x, y);
    cout << "The harmonic mean of " << x << " and " << y << " is " << z << endl;
    cout << "Please enter next set of numbers : <q to quit>: " << endl;
    }
    cout << "Done.\n";
    system("pause");
    return 0;
    }
    double hmean(double a, double b)
    {
    if(a == -b)
    {
    cout << "untenable arguments to hmean().\n";
    abort();
    }
    return 2.0 * a * b / ( a + b );
    }

这个例子就是求两个数a、b的调和平均数,但是a、b不能为相反数,如果有,程序就崩了,所以如果出现这种情况就调用abort();终止程序运行。【我并没有意识到这个有什么用】

返回错误码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <cfloat>

bool hmean(double a, double b, double * ans);

using namespace std;

int main()
{
double x, y, z;
cout << "Please enter one set of number : ";
while(cin >> x >> y)
{
if(hmean(x, y, &z))
{
cout << "Harmonic mean of "<< x << " and " << y << " is " << z << endl;
}
else
{
cout << "One value should not be the negative of the other - try again.\n";
}
cout << "Please enter next set of numbers <q to quit >: ";
}
system("pause");
return 0;
}

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;
}
}

这个例子和上面那个例子没有什么区别,就是检测到如果为相反数,不调用abort(),而是返回一个false……………………………………【我也没有感觉这个有什么用】

异常机制 try–catch

C++异常对异常的处理由3各部分:
引发异常。程序出现问题时将引发异常,之前两种方法在异常引发后,是调用abort终止了程序,但是throw语句是跳转,即命令程序跳转到另一条语句。throw关键字表示引发异常,参数表示异常的特征,用以指导处理。
使用处理程序捕获异常。采用异常处理程序捕获异常,catch关键字表示捕获异常,其参数指出异常处理程序要响应的异常类型。异常处理程序也叫catch块。
使用try块。try块表示可能会引发异常的代码块,表面需要注意这些代码引发的异常;它后面跟多个catch块,表明try块引发的异常由这些catch块处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>
double hmean(double a, double b);

using namespace std;

int main()
{
double x, y, z;
cout << "Enter one set of numbers : ";
while(cin >> x >> y)
{
try{
z = hmean(x, y);
}
catch(const char * s)
{
cout << s << endl;
cout << "Enter next set of numbers : ";
continue;
}
cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl;
cout << "Enter next set of numbers : ";
}
system("pause");
return 0;
}

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

这个例子和前面的两个一样。但是处理方法采用了try-catch的方法,就是检测到是相反数,就抛出异常(throw “bad hmean() arguments”;),然后根据这个抛出的异常信息,在catch中找到对应的catch处理部分。需要注意的是,执行throw语句类似于执行返回语句,因为它也将终止函数的执行,但是throw不是返回至调用程序的地方,而是使程序沿函数调用序列后退,直至try块函数。在寻找与异常类型匹配的异常处理程序(catch块)。如果没有引发异常,则跳过catch块。
还有就是,也可以将对象作为异常类型throw掉……………………具体实现方法和上面差不多。

异常规范

C++11把异常规范从标准中剔除了,感兴趣有时间可以看看。

运行阶段类型识别(RTTI)

RTTI(Runtime Type Identification)是运行阶段类型识别,简单点就是在运行阶段确定对象的类型的一种标准方法。
在实际编程中,会遇到这样的情况,在总多类层次结构中,我们会选择使用基类指针来指向其中任意的派生类对象,但是问题来了,我想使用某个类的成员函数,那么这个指针能否直接调用这个函数呢,我们就会搞忘这个指针指向的到底是什么对象类型???当然如果这个函数是类层次结构中所有成员都拥有的虚函数,就无所谓了。

RTTI的工作原理

C++有3个支持RTTI的元素:dynamic_cast运算符、typeid运算符、type_info结构。

  1. dynamic_cast
    dynamic_cast运算符,如果可以,使用一个指向基类的指针来生成一个派生类指针;否则,返回空指针。
    假设有如下的类层次结构:
    1
    2
    3
    class Grand {//has virtual methods};
    class Superb : public Grand {}
    class Magnificent : public Superb {}

有如下指针:

1
2
3
4
5
6
7
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

可以得到的是,以上三种指针转换第一种和第三种是正确的,但是第二种是错误,因为pg是基类指针,强制转换为派生类指针,要求其可以调用派生类方法,显然会导致错误。
需要注意的是,与“指针指向的是什么类型的对象”相比,问题“类型转换是否安全正确”更加通用。因此来看看dynamic_cast的用法。
Superb * pm = dynamic_cast<Super *> pg;,这各语句就指出pg能否安全的转换为Superb *,,如果可以赋值给pm, 如果不可以,pm被赋值为空指针;
具体的来看一个详细的例子::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// dynamic_case\<\> 也可以用于引用,但是与指针用法稍有不同:没有与空指针对于的引用值,无法使用特殊的引用值指示失败,因此dynamic_case将引发bad_cast的异常 P646
#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

class Grand{
private:
int hold;
public:
Grand(int h = 0) : hold(h) {}
virtual void Speak() const { cout << "I am a grand class!\n";}
virtual int Value() const { return hold;}
};

class Superb : public Grand
{
public:
Superb(int h = 0) : Grand(h) {}
void Speak() const {cout << "I am a Superb class!!\n";}
virtual void Say() const { cout << "I holde the superb value of " << Value() << "!!\n";}
};

class Magnificent : public Superb
{
private:
char ch;
public:
Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
void Speak() const { cout << "I am a magnificent class \n";}
void Say() const { cout << "I hold the character " << ch << "and the integer " << Value() << "\n";}
};

Grand * GetOne();

int main()
{
srand(time(0)); //srand() cstdlib time(0) ctime
Grand * pg;
Superb * ps;
for(int i = 0; i < 5; i++)
{
pg = GetOne();
pg->Speak();
if(ps = dynamic_cast<Superb * >(pg))
ps->Say();
}
system("pause");
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 Magnificent(rand() % 100, 'A' + rand() % 26);
break;
}
return p;
}

  1. typeid运算符和typeinfo类
    typeid运算符合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()成员,返回一个随实现而异的字符串。具体看下面的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    #include <iostream>
    #include <cstdlib>
    #include <ctime>

    using namespace std;

    class Grand{
    private:
    int hold;
    public:
    Grand(int h = 0) : hold(h) {}
    virtual void Speak() const { cout << "I am a grand class!\n";}
    virtual int Value() const { return hold;}
    };

    class Superb : public Grand
    {
    public:
    Superb(int h = 0) : Grand(h) {}
    void Speak() const {cout << "I am a Superb class!!\n";}
    virtual void Say() const { cout << "I holde the superb value of " << Value() << "!!\n";}
    };

    class Magnificent : public Superb
    {
    private:
    char ch;
    public:
    Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
    void Speak() const { cout << "I am a magnificent class \n";}
    void Say() const { cout << "I hold the character " << ch << "and the integer " << Value() << "\n";}
    };

    Grand * GetOne();

    int main()
    {
    srand(time(0)); //srand() cstdlib time(0) ctime
    Grand * pg;
    Superb * ps;
    for(int i = 0; i < 5; i++)
    {
    pg = GetOne();
    cout << "Now processing type " << typeid(*pg).name() << endl;
    pg->Speak();
    if(ps = dynamic_cast<Superb *>(pg))
    ps->Say();
    if(typeid(Magnificent) ==typeid(*pg))
    cout << "Yes, you're really magnificent.\n";
    }
    system("pause");
    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 Magnificent(rand() % 100, 'A' + rand() % 26);
    break;
    }
    return p;
    }

其他

嵌套类

这个内容并没有太多可讲的,需要重点关注的点:嵌套类的作用域和访问控制

类型转换运算符

在RTTI中讲了一个类型转换运算符,但是还有几个其他的运算符,用法和dynamic_cast一样,运用的场景不一样而已:
dynamic_cast: 在类层次结构中进行向上转换,而不允许其他转换
const_cast: 用来修改类型的const或volatile属性。但是这个运算符没有很好的理解,可以改变类型,却又不能改变const的值,那么有什么用???
static_cast: static_cast<type-name> (expression),仅当type-name可被隐式转换为expression所属类型或expression所属类型可被隐式转换为type-name,转换成立,否则将出错。有些不能隐式转换的也可使用static_cast强制转换,但是可能出错。同时也可以用于数值转换中。
reinterpret_cast:用于比如将一个long型数据转换为一个结构体(包含两个short型成员),这类危险转换依赖于底层编程技术,不可移植。