C++面向对象(五):多态性

C++面向对象:多态性

会有点长,不过读过就全学会喽!!!!!!
会有点长,不过读过就全学会喽!!!!!!
会有点长,不过读过就全学会喽!!!!!!

1 . 编译时的多态性与运行时的多态性

所谓多态性就是不同对象收到相同的消息时,产生不同的动作。
通俗地说, 多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作, 即用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。
在 C + + 中,多态性的实现和联编这一概念有关。一个源程序经过编译、连接, 成为可执行文件的过程是把可执行代码联编(或称装配) 在一起的过程。其中在运行之前就完成的联编称为静态联编,又叫前期联编; 而在程序运行时才完成的联编叫动态联编, 也称后期联编。

静态联编

系统在编译时就决定如何实现某一动作。静态联编要求在程序编译时就知道调用函数的全部信息。因此, 这种联编类型的函数调用速度很快。效率高是静态联编的主要优点。
静态联编支持的多态性称为编译时多态性,也称静态多态性。在 C + + 中, 编译时多态性是通过函数重载和运算符重载实现的。

动态联编

系统在运行时动态实现某一动作。采用这种联编方式, 一直要到程序运行时才能确定调用哪个函数。动态联编的主要优点是:提供了更好的灵活性、问题抽象性和程序易维护性。
动态联编所支持的多态性称为运行时多态性,也称动态多态性。在 C + + 中, 运行时多态性是通过继承和虚函数来实现的。

2 . 函数重载

编译时的多态性可以通过函数重载来实现。
在基类和派生类中函数重载的例子如下:

#include <iostream>
using namespace std;
class point
{
    int x, y;
public:
    point(int a, int b)
    {
        x = a;
        y = b;
    }
    float area () // 基类中的area() //函数
    {
        return 0.0 ;
    }
};
class circle: public point
{
    int radius;
public:
    circle (int x, int y, int rad) : point( x, y )
    {
        radius = rad;
    }
    float area() // 派生类中的 area ( ) 函数
    {
        return 3.1416 * radius * radius;
    }
};
int main ()
{
    point p(20, 20 );
    circle c(8,8,30);
    cout << p.area () << endl;
    // 执行基类 point 中的 area ( )函数
    cout << c.area() << endl;
    // 执行派生类 circle 中的 area( )函数
    cout << c.point::area() << endl;
    // 执行基类 point 中的 area ( )函数
    return 0;
}

程序运行结果为:
0
2827 .439941
0

在基类和派生类中的函数重载有两种情况:一种是参数有所差别的重载, 这种重载函数的定义和调用方法在前面章节已进行了介绍;另一种是函数所带的参数完全相同, 只是它们属于不同的类。以下两种方法可以在编译时区别这类函数:

  1. 使用对象名加以区分。例如: p .area ( ) 和 c .area ( ) 分别调用类 point 的 area ( ) 函数和类 circle 的 area ( )函数。
  2. 使用“类名∷”加以区分。例如: point∷area ( ) 和 circle∷area ( )分别调用类 point和类 circle 的 area ( )函数。

说明:
C + + 在编译时常采用“名字压延”的方法来区分重载函数。名字压延是一种计算机术语,指在编译器“看”到函数后改变函数名。
为了进行名字压延, C + + 把重载函数的本名和参数结合起来, 以创造函数的新名字。程序中每一处说明原型、定义和调用这些函数的地方, C + + 都用其压延名字来替代。
例如,有以下两个函数原型:

int myAns(float x, int j) ;
int myAns(int i , char c) ;

并用以下语句调用它们:

exam1 = myAns( 15 .3 ,15) ;
exam2 = myAns( 45 ,′a′) ;

则在编译完成之前, C + + 也许会将函数名改变成如下形式:

int myAnsFLTINT (float x, int j) ;
int myAnsINTCHAR(int i, char c) ;

同时 C + + 也会在函数调用的地方改变名字,如:

exam1 = myAnsFLTINT( 15 .3 ,15) ;
exam2 = myAnsINTCHAR( 45,′a′) ;

有了新的函数名(实际上 C + + 使用一种更短的扩展名字算法, 此处只是给出一种思路) , C + + 在 要求调用 myAns( float x, int j) 时调用 myAnsFLTINT ( ) , 而 在要求调 用myAns(int i, char c )时调用 myAnsINTCHAR( )。在程序完成编译后, 函数名又恢复原来的名字。因此,依据名字压延, C + + 就能把重载函数区分开来。

3 . 运算符重载

运算符重载通过创建运算符函数 operator ( ) 来实现。
运算符函数定义了重载的运算符将要进行的操作, 这种操作通常作用在一个类上。函数operator( )可以是它将要操作的类的成员, 也可以不是类的成员。但非成员 operator ( ) 函数通常是类的友元函数。

3.1 类以外的运算符重载

若有一个复数类 complex:

class complex
{
public:
    double real, imag;
    complex( double r = 0, double i = 0)
    {
        real = r;
        imag = i;
    }
};

若要把类 complex 的两个对象 com1 和 com2 加在一起, 下面的语句是不能实现的:

int main()
{
    complex com1( 1 .1, 2 .2), com2( 3 .3, 4 .4), totaltotal = com1 + com2;
    // 错误
    // …
    return 0;
}

不能实现的原因是类 complex 的类型不是基本数据类型, 而是用户自定义的数据类型。
C + + 知道如何相加两个 int 型数据, 或相加两个 float 型数据, 甚至知道如何把一个int 型数据与一个 float 型数据相加,但是 C + + 还无法直接将两个 complex 类对象相加。
为了表达上的方便,人们希望预定义的内部运算符 (如“ + ”、“ - ”、“ * ”、“/ ”等 )在特定类的对象上以新的含义进行解释, 如希望能够实现 total = com1 + com2, 这就需要用重载运算符来解决。
C + + 为运算符重载提供了一种方法, 即在进行运算符重载时, 必须写一个运算符函数,其名字为 operator 后随一个要重载的运算符。例如, 要重载“ + ”号, 应该写一个名字为 operator + 的函数。其它的重载运算符也应该以同样的方式命名, 如表所示。

运算符函数
函数功能
operator + ( )加法
operator - ( )减法
operator * ( )乘法
operator / ( )除法
operator < ( )小于

这样,在编译时遇到名为 operator@的运算符函数( @表示所要重载的运算符) , 就检查传递给函数的参数的类型。如果编译器在一个运算符的两边“看”到自定义的数据类型,就执行用户自己的函数, 而不是内部运算符的常规程序。
因此,若要将上述类 complex 的两个对象相加,只要写一个运算符函数 operator + ( ) :

complex operator + (complex om1, complex om2)
{
    complex temp;
    temp.real = om1.real + om2.real;
    temp.imag = om1.imag + om2.imag;
    return temp;
}

我们就能方便地使用语句:
total = com1 + com2;
将类 complex 的两个对象 com1 和 com2 相加。当然, 在程序中也可以使用以下的调用语句,将两个 complex 类对象相加:
total = operator + ( com1 , com2 ) ;
这两个调用语句是等价的,但显然后者不如前者简明和方便。
以下就是使用运算符函数 operator + ( )将两个 complex 类对象相加的完整程序。

#include <iostream>
using namespace std;
class complex
{
public:
    double real;
    double imag;
    complex( double r = 0, double i = 0)
    {
        real = r;
        imag = i;
    }
};
complex operator + ( complex co1, complex co2 )
{
    complex temp;
    temp.real = co1.real + co2.real;
    temp.imag = co1.imag + co2.imag;
    return temp;
}
int main()
{
    complex com1( 1.1, 2.2), com2( 3.3, 4.4), total1, total2;
    total1 = operator + ( com1, com2) ;
    // 调用运算符函数
    // operator + ( )的第一种方式
    cout << "real1 = "<< total1 .real << " "<< "imag1 = "<< total1.imag << endl;
    total2 = com1 + com2;
    // 调用运算符函数
    // operator + ( )的第二种方式
    cout << "real2 = "<< total2 .real << " " << "imag2 = "<< total2.imag << endl;
    return 0;
}

程序运行结果如下:
real1 = 4 .4 imag1 = 6 .6
real2 = 4 .4 imag2 = 6 .6
注意: 在本例中, complex 的类对象分别使用了两种不同的方式相加, 显然使用一个简单的“ + ”号将两个类对象相加更方便明了。
说明:

  1. 重载运算符与预定义运算符的使用方法完全相同, 它不能改变原有运算符的参数个数(单目或双目) ,也不能改变原有的优先级和结合性。
  2. 在 C + + 中, 大多数系统预定义的运算符都能被重载,例如

在这里插入图片描述

3.2 成员运算符函数

运算符重载是通过创建运算符函数来实现的。运算符函数定义了重载的运算符将要进行的操作。

实际上, 这种操作通常作用在一个类上。运算符函数可以定义为它将要操作的类的成员(称为成员运算符函数) ,也可以定义为非类的成员, 但是非成员的运算符函数大多是类的友元函数(称为友元运算符函数) 。

1 .成员运算符函数定义的语法形式

成员运算符函数在类中的声明格式为:

class X{
// …
type operator @ (参数表 ) ;
// …
} ;

其中 type 为函数的返回类型, @为所要重载的运算符符号, X 是重载此运算符的类名, 参数表中罗列的是该运算符所需要的操作数。
成员运算符函数定义的形式一般为:

type X∷operator @ (参数表)
{
// 函数体
}

其符号的含义与声明时相同。
在成员运算符函数的参数表中,若运算符是单目的, 则参数表为空; 若运算符是双目的,则参数表中有一个操作数。

2 . 双目运算符重载

对双目运算符而言,成员运算符函数的参数表中仅有一个参数, 它作为运算符的右操作数,此时当前对象作为运算符的左操作数, 它是通过 this 指针隐含地传递给函数的。
例如:

class X
{
    // …
    int operator + (X a ) ;
};

在类 X 中声明了重载“ + ”的成员运算符函数, 返回类型为 int ,它具有两个操作数, 一个是当前对象,另一个是对象 a。
下面是一个用双目运算符函数进行复数运算的例子。
两个复数 a + bi 和 c + di 进行加、减、乘、除的方法如下:
加法: ( a + bi) + ( c + di) = ( a + c ) + ( b + d)i
减法: ( a + bi) - ( c + di) = ( a - c ) + ( b + d)i
乘法: ( a + bi) * ( c + di) = ( ac - bd) + ( ad + bc)i
除法: ( a + bi)/ ( c + di) = ( ( a + bi) * ( c - di) )/ ( c ^ 2 + d ^ 2)
在 C + + 中,不能直接进行复数的加、减、乘、除运算, 但是我们可以定义四个成员运算符函数,通过重载“ + ”、“ - ”“、 * ”、“/ ”运算符来实现复数运算。
在本例中, 声明了一个复数类 complex, 类中含有两个数据成员, 即复数的实数部分real 和复数的虚数部分 imag。

#include<iostream>
using namespace std;
class complex
{
private:
    double real;
    // 复数的实数部分
    double imag;
    // 复数的虚数部分
public :
    complex( double r = 0.0, double i = 0.0) ;
    // 构造函数
    void print( ) ;
    // 显示输出复数
    complex operator + ( complex c ) ;
    // 重载复数“ + ”运算符
    complex operator - ( complex c ) ;
    // 重载复数“ - ”运算符
    complex operator * ( complex c ) ;
    // 重载复数“ * ”运算符
    complex operator / ( complex c) ;
    // 重载复数“/ ”运算符
};
complex::complex( double r, double i) // 定义构造函数
{
    real = r;
    imag = i;
}
complex complex::operator + ( complex c ) // 重载“ + ”定义
{
    complex temp;
    temp.real = real + c.real;
    temp.imag = imag + c.imag;
    return temp;
}
complex complex::operator - ( complex c ) // 重载“ - ”定义
{
    complex temp;
    temp.real = real - c.real;
    temp.imag = imag - c.imag;
    return temp;
}
complex complex::operator * ( complex c ) // 重载“ * ”定义
{
    complex temp;
    temp.real = real * c.real - imag * c.imag;
    temp.imag = real * c.imag + imag * c.real;
    return temp;
}
complex complex::operator/ ( complex c) // 重载“/ ”定义
{
    complex temp;
    double t ;
    t = 1/( c.real * c.real + c.imag * c.imag) ;
    temp.real = ( real * c.real + imag * c.imag) * t ;
    temp.imag = ( c.real * imag - real * c.imag) * t ;
    return temp;
}
void complex::print() // 显示复数的实数部分和虚数部分
{
    cout << real;
    if (imag > 0 ) cout << "+ ";
    if (imag != 0) cout << imag << "i\n";
}
int main ()
{
    complex A1( 2.3, 4.6), A2( 3.6, 2.8), A3, A4, A5, A6;
    // 定义六个复数类对象
    A3 = A1 + A2;
    // 复数相加
    A4 = A1 - A2;
    // 复数相减
    A5 = A1 * A2;
    // 复数相乘
    A6 = A1/ A2;
    // 复数相除
    A1 .print();
    // 输出复数 A1
    A2 .print();
    // 输出复数 A2
    A3 .print();
    // 输出复数相加的结果 A3
    A4 .print();
    // 输出复数相减的结果 A4
    A5 .print();
    // 输出复数相乘的结果 A5
    A6 .print();
    // 输出复数相除的结果 A6
    return 0 ;
}

程序运行结果如下:
2 .3 + 4 .6i
3 .6 + 2 .8i
5 .9 + 7 .4i

  • 1 .3 + 1 .8i
  • 4 .6 + 23i
    1 .017308 + 0 .486538i
    从本例可以看出,对复数重载了这些运算符后, 再进行复数运算时, 不再需要按照给出的表达式进行繁琐的运算,只需像基本数据类型的运算一样书写即可, 这样给用户带来了很大的方便,并且很直观。
    在主函数 main( )中的语句
    A3 = A1 + A2;
    A4 = A1 - A2;
    A5 = A1 * A2;
    A6 = A1/ A2;
    所用的运算符“ + ”“、 - ”、“ * ”、“/ ”是重载后的运算符。程序执行到这四条语句时, C ++ 将其解释为:
    A3 = A1 .operator + ( A2 ) ;
    A4 = A1 .operator - ( A2 ) ;
    A5 = A1 .operator * ( A2 ) ;
    A6 = A1 .operator / (A2) ;
    由此我们可以看出,成员运算符函数 operator @ 实际上是由双目运算符左边的对象A1 调用的, 尽管双目运算符函数的参数表只有一个操作数 A2 ,但另一个操作数是由对象A1 通过 this 指针隐含地传递的。
    以上两组语句的执行结果是完全相同的。
    一般而言,采用成员函数重载双目运算符@后, 可以用以下两种方法来使用:
aa @ bb; / / 隐式调用
aa .operator @ ( bb) ; / / 显式调用

成员运算符函数 operator @ 所需的一个操作数由对象 aa 通过 this 指针隐含地传递。因此,它的参数表中只有一个操作数 bb。

3 . 单目运算符重载

对单目运算符而言,成员运算符函数的参数表中没有参数, 此时当前对象作为运算符的一个操作数。
下面是一个重载单目运算符“ + + ”的例子。

#include<iostream>
using namespace std;
class coord
{
    int x, y;
public :
    coord(int i = 0, int j = 0) ;
    void print() ;
    coord operator++() ;
    // 声明成员运算符函数 operator + + ( )
} ;
coord::coord (int i, int j)
{
    x = i;
    y = j;
}
void coord::print()
{
    cout << " x:"<< x << ", y:"<< y << endl;
}
coord coord::operator ++ ( ) // 定义成员运算符函数 operator + + ( )
{
    ++ x;
    ++ y;
    return * this;
}
int main()
{
    coord ob (10, 20 ) ;
    ob.print() ;
    ++ ob;
    // 隐式调用成员运算符函数 operator + + ( )
    ob.print() ;
    ob.operator ++() ;
    // 显式调用成员运算符函数 operator + + ( )
    ob.print() ;
    return 0;
}

程序运行结果如下:
x:10, y: 20
x:11, y: 21
x:12, y: 22
不难看出,对类对象重载了运算符 + + 后, 对类对象的加 1 操作变得非常方便, 就像对整型数进行加 1 操作一样。
例主函数 main( )中调用成员运算符函数 operator + + ( )采用了两种方式:

    • ob;与ob .operator + + ( ) ;两者是等价的,其执行效果是完全相同的。
      从本例还可以看出,当成员函数重载单目运算符时, 没有参数被显式地传递给成员运算符函数。参数是通过 this 指针隐含地传递给函数的。
      一般而言,采用成员函数重载单目运算符时, 可以采用以下两种方法来使用:
@aa ; / / 隐式调用 ~
aa.operator @ ( ) ; / / 显式调用

成员运算符函数 operator @所需的一个操作数由对象 aa 通过 this 指针隐含地传递。因此,在它的参数表中没有参数。
这里给出的是成员函数重载单目运算符前缀的表示方式,其重载单目运算符后缀的表示方法在第五部分给出。
说明:

  1. 运算符重载函数 operator @ ( )可以返回任何类型,甚至可以是 void 类型, 但通常返回类型与它所操作的类的类型相同,这样可使重载运算符用在复杂的表达式中。
  2. 在重载运算符时, 运算符函数所作的操作不一定要保持 C + + 中该运算符原有的含义。例如,可以把加运算符重载成减操作, 但这样容易造成混乱。所以保持原含义,容易被接受,也符合人们的习惯。
  3. 在 C + + 中, 用户不能定义新的运算符, 只能从 C + + 已有的运算符中选择一个恰当的运算符重载。
  4. C + + 编译器根据参数的个数和类型来决定调用哪个重载函数。因此,可以为同一个运算符定义几个运算符重载函数来进行不同的操作。
3.3 友元函数运算符

在 C + + 中,还可以把运算符函数定义成某个类的友元函数, 称为友元运算符函数。

1 . 友元运算符函数定义的语法形式

友元运算符函数与成员运算符函数不同,后者本身是类中的成员函数, 而前者是类的友元函数。
友元运算符函数在类的内部声明格式如下:

friend type operator@ (参数表 ) ;

与成员运算符函数的声明格式相比较, 只是在前面多了一个关键字 friend,其它项目的含义相同。
定义友元运算符函数与定义一般的友元函数相似,其格式如下:

type operator@ (参数表 )
{
// 函数体
}

由于此函数不是类的成员函数,所以不需要缀上类名。
与成员运算符函数不同,友元运算符函数是不属于任何类对象的, 它没有 this 指针。若重载的是双目运算符,则参数表中有两个操作数; 若重载的是单目运算符, 则参数表中只有一个操作数。

2 . 双目运算符重载

当用友元函数重载双目运算符时, 两个操作数都要传递给运算符函数。
现在我们采用友元运算符函数来完成复数运算。

#include<iostream>
using namespace std;
class complex
{
private:
    double real;
    double imag;
public :
    complex( double r = 0.0, double i = 0.0) ;
    void print() ;
    // 用友元运算符函数重载复数“ + ”
    friend complex operator + ( complex a, complex b);
    // 用友元运算符函数重载复数“ - ”
    friend complex operator - ( complex a, complex b);
    // 用友元运算符函数重载复数“ * ”
    friend complex operator * ( complex a, complex b);
    // 用友元运算符函数重载复数“/ ”
    friend complex operator / ( complex a, complex b);
} ;
complex::complex( double r, double i) // 构造函数
{
    real = r;
    imag = i;
}
complex operator + ( complex a, complex b) // 重载“ + ”定义
{
    complex temp;
    temp.real = a.real + b.real;
    temp.imag = a.imag + b.imag;
    return temp;
}
complex operator - ( complex a, complex b) // 重载“ - ”定义
{
    complex temp;
    temp.real = a.real - b.real;
    temp.imag = a.imag - b.imag;
    return temp;
}
complex operator *( complex a, complex b) // 重载“ * ”定义
{
    complex temp;
    temp.real = a.real * b.real - a.imag * b.imag;
    temp.imag = a.real * b.imag + a.imag * b.real;
    return temp;
}
complex operator / ( complex a, complex b) // 重载“/ ”定义
{
    complex temp;
    double t ;
    t = 1/ ( b.real * b.real + b.imag * b.imag) ;
    temp.real = ( a.real * b.real + a.imag * b.imag) * t ;
    temp.imag = ( b.real * a.imag - a.real * b.imag) * t;
    return temp;
}
void complex::print( ) // 显示输出复数
{
    cout << real;
    if (imag > 0 ) cout << "+ ";
    if (imag != 0) cout << imag << "i \n";
}
int main ()
{
    complex A1( 2.3, 4.6), A2( 3.6, 2.8), A3, A4, A5, A6;
    // 定义六个复数类对A3 = A1 + A2;
    // 复数相加 A4 = A1 - A2;
    // 复数相减
    A5 = A1 * A2;
    // 复数相乘
    A6 = A1/ A2;
    // 复数相除
    A1.print();
    // 输出复数 A1
    A2.print();
    // 输出复数 A2
    A3.print();
    // 输出复数相加结果 A3
    A4.print();
    // 输出复数相减结果 A4
    A5.print();
    // 输出复数相乘结果 A5
    A6.print();
    // 输出复数相除结果 A6
    return 0 ;
}

程序运行结果如下:
2 .3 + 4 .6i
3 .6 + 2 .8i
5 .9 + 7 .4i

  • 1 .3 + 1 .8i
  • 4 .6 + 23i
    1 .017308 + 0 .486538i
    一般而言,采用友元函数重载双目运算符@后, 可以采用以下两种方法使用:
aa @ bb; // 隐式调用
operator @ ( aa, bb) ; // 显式调用

双目友元运算符函数 operator @ 所需的两个操作数都在参数表中由对象 aa 和 bb 显式地提供。

3 . 单目运算符重载

用友元函数重载单目运算符时,需要一个显式的操作数。下面的例子中, 用友元函数重载单目运算符“ - ”。

#include<iostream>
using namespace std;
class nclass
{
    int a, b;
public :
    nclass(int x = 0, int y = 0)
    {
        a = x;
        b = y;
    }
    friend nclass operator - ( nclass obj) ;
    // 声明重载单目运算符“ - ” /
    void show( ) ;
};
nclass operator - ( nclass obj) // 定义重载单目运算符“ - ”
{
    obj.a = - obj.a;
    obj.b = - obj.b;
    return obj;
}
void nclass::show( )
{
    cout << "a = "<< a <<"b = "<< b << endl;
}
int main()
{
    nclass ob1(10, 20 ), ob2;
    ob1.show();
    ob2 = - ob1;
    ob2.show();
    return 0 ;
}

程序运行结果如下:
a = 10 b = 20
a = - 10 b = - 20
需要注意的是:使用友元函数重载“ + + ”、“ - - ”这样的运算符, 可能会出现一些问题。我们先回顾一下, 用成员函数重载“ + + ”的那个成员运算符函数, 为了说明的方便,现将它写在下面:

coord coord∷operator + + ( )
{
+ + x;
+ + y;
return * this;
} ;

由于所有的成员函数都有一个 this 指针, this 指针指向该函数所属类对象的指针, 因此对私有数据的任何修改都将影响实际调用成员运算符函数的对象, 因此例 5 .4 的执行情况是完全正确的。但是, 如果像下面那样用友元函数改写上面的运算符函数将是错误的:

coord operator + + ( coord op)
{
+ + opx;
+ + opy;
return op;
}

原因在于友元函数没有 this 指针,所以不能引用 this 指针所指的对象。这个函数是通过传值的方法传递参数的,函数体内对 op 的所有修改都无法传到函数体外。因此, 在operator + + 函数中, 任何内容的改变不会影响产生调用的操作数, 也就是说, 实际上对象 x 和 y 并未增加,而运算符“ + + ”和“ - - ”的原义是要改变操作数自身的值, 因此造成了错误。

为了解决以上问题,使用友元函数重载单目运算符“ + + ”或“ - - ”时, 应采用引用参数传递操作数,这将导致函数参数的任何改变都影响产生调用的操作数, 从而保持了两个运算符的原义。

#include<iostream>
using namespace std;
# include <iostream >
class coord
{
    int x, y;
public :
    coord(int i = 0, int j = 0) ;
    void print() ;
    friend coord operator ++ ( coord &op) ;
    // 声明友元运算符函数 operator + + ( )
} ;
coord::coord (int i, int j)
{
    x = i;
    y = j;
}
void coord::print( )
{
    cout << " x:"<< x << ", y:"<< y << endl;
}
coord operator ++ ( coord &op) // 定义友元运算符函数 operator + + ( )
{
    ++ op .x;
    ++ op .y;
    return op;
}
int main ()
{
    coord ob (10, 20 ) ;
    ob.print() ;
    ++ ob;
    // 隐式调用友元运算符函数 operator + + ( )
    ob.print() ;
    operator ++ ( ob) ;
    // 显式调用友元运算符函数 operator + + ( )
    ob.print() ;
    return 0 ;
}

程序运行的结果如下:
x:10, y: 20
x:11, y: 21
x:12, y: 22
从上述程序可以看出,当用友元函数重载单目运算符时, 参数表中有一个操作数。一般而言,采用友元函数重载单目运算符@后, 可以采用以下两种方法来使用:

@aa ; / / 隐式调用
operator@ ( aa) ; / / 显式调用

注意:关于友元函数重载单目运算符后缀方式的表示方法, 将在 3 .5 节介绍。

说明:

  1. 不能用友元函数重载的运算符是: = 、( )、[ ] 、- > , 其余的运算符都可以使用友元函数来实现重载。
  2. 由于单目运算符“ - ”可不改变操作数自身的值, 所以在例 5 .6 重载单目运算符“ - ”的友元运算符函数的原型可写成:
    friend nclass operator - ( nclass obj) ;
    通过传值的方式传递参数。
3.4 成员运算符函数与友元运算符函数的比较

成员运算符函数与友元运算符函数作一比较。

  1. 对双目运算符而言, 成员运算符函数带有一个参数,而友元运算符函数带有两个参数;对单目运算符而言, 成员运算符函数不带参数,而友元运算符函数带一个参数。
  2. 双目运算符一般可以被重载为友元运算符函数或成员运算符函数, 但有一种情况,必须使用友元函数。
    例如, 在类 nclass 中, 用成员运算符函数重载“ + ”运算符:
nclass nclass::operator + (int x )
{
    nclass temp;
    temp.a = a + x;
    temp.b = b + x;
    return temp;
}

若类 nclass 的对象 ob 要做赋值运算和加法运算, 以下是一条正确的语句:

ob = ob + 100;

由于对象 ob 是运算符“ + ”的左操作数, 所以它调用了“ + ”运算符重载函数, 把一个整数加到了对象 ob 的某些元素上去。然而,下一条语句就不能工作了:

ob = 100 + ob;

不能工作的原因是运算符的左操作数是一个整数,而整数是一个内部数据类型, 100 不能产生对成员运算符函数的调用。

如果用两个友元函数来重载运算符函数“ + ”,就能消除由于运算符“ + ”的左操作数是内部数据类型而带来的问题,这样, 两个参数都显式地传递给运算符函数, 可以像别的重载函数一样调用。用友元运算符函数来重载运算符“ + ”(或其它双目运算符) 就使得内部数据类型能出现在运算符的左边。

#include<iostream>
using namespace std;
class nclass
{
    int a, b;
public :
    nclass(int x = 0, int y = 0) ;
    friend nclass operator + ( nclass ob, int x) ;
    // 声明友元运算符函数
    friend nclass operator + (int x, nclass ob) ;
    // 声明友元运算符函数
    void show() ;
} ;
nclass::nclass(int x, int y)
{
    a = x;
    b = y;
}
nclass operator + ( nclass ob, int x) // 定义友元运算符函数
{
    nclass temp;
    temp.a = ob.a + x;
    temp.b = ob.b + x;
    return temp;
}
nclass operator + (int x, nclass ob) // 定义友元运算符函数
{
    nclass temp;
    temp.a = x + ob.a ;
    temp.b = x + ob.b;
    return temp;
}
void nclass::show()
{
    cout << "a = "<< a <<"  "<< "b = "<< b << "\n";
}
int main ()
{
    nclass ob1 (30, 40 ), ob2;
    ob2 = ob1 + 30;
    ob2.show() ;
    ob2 = 50 + ob1;
    ob2.show() ;
    return 0 ;
}

  1. 成员运算符函数和友元运算符函数都可以用习惯方式调用, 也可以用它们专用的方式调用。

运算符函数调用形式

习惯形式友元运算符函数调用形式成员运算符函数调用形式
a + boperator + ( a ,b)a . operator + ( b)
- aoperator - ( a )a .operator - ( )
a + +operator + + ( a ,0)a .operator + + (0 )

单目运算符的后缀方式,将在 3 .5 节中介绍。

  1. C + + 的大部分运算符既可说明为成员运算符函数, 又可说明为友元运算符函数。究竟选择哪一种运算符函数好一些, 没有定论, 这主要取决于实际情况和程序员的习惯。

一般而言,对于双目运算符, 将它重载为一个友元运算符函数比重载为一个成员运算符函数便于使用。若一个运算符的操作需要修改类对象的状态, 则选择成员运算符函数较好。如果运算符所需的操作数(尤其是第一个操作数 )希望有隐式类型转换,则运算符重载必须用友元函数,而不能用成员函数。

3.5 “+ +”和“ - - ”的重载

运算符“ + + ”和“ - - ”放置在变量的前面与后面, 其作用是有区别的。但是 C + + 2 .1 之前的版本在重载“ + + ”(或“ - - ”) 时, 不能显式地区分是前缀方式还是后缀方式。也就是说,在之前的例子中的 main( ) 函数中,以下两条语句是完全相同的:

ob + + ;
+ + ob;

在 C + + 2 .1 及以后的版本中,编辑器可以通过在运算符函数参数表中是否插入关键字 int 来区分这两种方式。
对于前缀方式 + + ob, 可以用运算符函数重载为:

ob .operator + + ( ) ; / / 成员函数重载
operator + + (X &ob) ; / / 友元函数重载, 其中 ob 为 X 类对象

对于后缀方式 ob + + ,可以用运算符函数重载为:

ob .operator + + (int) ; / / 成员函数重载
operator + + (X &ob, int) ; / / 友元函数重载

调用时,参数 int 一般被传递给值 0,例如:

#include<iostream>
using namespace std;
class X
{public :
    …
    X operator ++ ( ) ;
    // 前缀方式
    X operator ++ (int) ;
    // 后缀方式};
int main ()
{
    X ob;++ ob;
    // 隐式调用 ob .operator + + ( )
    ob ++ ;
    // 隐式调用 ob .operator + + (int)
    ob.operator ++ ( ) ;
    // 显式调用 ob .operator + + ( ),意为 + + ob
    ob.operator ++ ( 0) ;
    // 显式调用 ob .operator + + (int), 意为 ob + +} ;

类似地,也可重载为友元函数, 例如:

#include<iostream>
using namespace std;
class Y
{friend operator ++ ( Y & ) ;
    // 前缀方式
    friend operator ++ ( Y &, int) ;
    // 后缀方式} ;
int main ()
{
    Y ob;++ ob;
    // 隐式调用 operator + + ( Y& )
    ob ++ ;
    // 隐式调用 operator + + ( Y&, int)
    operator ++ ( ob) ;
    // 显式调用 operator + + ( Y& ), 意为 + + ob
    operator ++ ( ob, 0) ;
    // 显式调用 operator + + ( Y&, int), 意为 ob + +}

重载运算符“ - - ”也可用类似的方法。

下面看一个包含这两种重载运算符“ + + ”和“ - - ”的例子。

#include<iostream>
using namespace std;
class over
{
    int i1, i2, i3 ;
public :
    void init(int I1, int I2, int I3 ) ;
    void print() ;
    over operator ++ () ;
    // 成员函数重载“ + + ”(前缀方式 )原型
    over operator ++ (int) ;
    // 成员函数重载“ + + ”(后缀方式 )原型
    friend over operator -- (over & ) ;
    // 友元函数重载“ - - ”(前缀方式 )原型
    friend over operator -- (over &, int) ;
    // 友元函数重载“ - - ”(后缀方式 )原
} ;
void over::init(int I1, int I2, int I3 ) // 给数据成员赋初值
{
    i1 = I1;
    i2 = I2;
    i3 = I3;
}
void over::print() // 显示输出数据成员
{
    cout << " i1: "<< i1 << " i2 : "<< i2 <<" i3: "<< i3 << endl;
}
over over::operator ++ () // 定义成员函数重载“ + + ”(前缀方式 )
{
    ++ i1 ;
    ++ i2 ;
    ++ i3;
    return * this;
}
over over::operator ++ (int) // 定义成员函数重载“ + + ”(后缀方式 )
{
    i1 ++ ;
    i2 ++ ;
    i3 ++ ;
    return * this;
}
over operator -- (over &op) // 定义友元函数重载“ - - ”(前缀方式 )
{
    -- op.i1;
    -- op.i2;
    -- op.i3;
    return op;
}
over operator -- (over &op, int) // 定义友元函数重载“ - - ”(后缀方式 )
{
    op.i1 -- ;
    op.i2 -- ;
    op.i3 -- ;
    return op;
}
int main ()
{
    over obj1,obj2, obj3, obj4;
    obj1.init( 4, 2, 5);
    obj2.init( 2, 5, 9);
    obj3.init( 8, 3, 8);
    obj4.init( 3, 6, 7);
    ++ obj1;
    // 隐式调用 over operator + + ( )
    obj2 ++ ;
    // 隐式调用 over operator + + (int)
    -- obj3;
    // 隐式调用 over operator - - ( over & )
    obj4 -- ;
    // 隐式调用 over operator - - ( over &, int)
    obj1 .print();
    obj2 .print();
    obj3 .print();
    obj4 .print();
    cout << " --------------------- \n";
    obj1.operator ++();
    // 显式调用, 意为 + + obj1
    obj2.operator ++(0);
    // 显式调用, 意为 obj2 + +
    operator -- ( obj3 );
    // 显式调用, 意为 - - obj3
    operator -- ( obj4, 0 );
    // 显式调用, 意为 obj4 - -
    obj1 .print();
    obj2 .print();
    obj3 .print();
    obj4 .print();
    return 0 ;
}

程序运行结果如下:
i1: 5 i2 :3 i3: 6
i1: 3 i2 :6 i3: 10
i1: 7 i2 :2 i3: 7
i1: 2 i2 :5 i3: 6


i1: 6 i2 :4 i3: 7
i1: 4 i2 :7 i3: 11
i1: 6 i2 :1 i3: 6
i1: 1 i2 :4 i3: 5
以上例子中,使用成员函数分别以前缀方式和后缀方式重载了运算符“ + + ”, 使用友元函数分别以前缀方式和后缀方式重载了运算符“ - - ”。

3.6 赋值运算符“ = ”的重载

对任一类 X ,如果没有用户自定义的赋值运算符函数, 那么系统将自动地为其生成一个缺省的赋值运算符函数,例如:

X &X∷operator = ( const X &source)
{
// 成员间赋值
}

若 obj1 和 obj2 是类 X 的两个对象, obj2 已被创建,则编译程序遇到如下语句:

obj1 = obj2;

就调用缺省的赋值运算符函数,将对象 obj2 的数据成员逐域拷贝到对象 obj1 中。
通常,缺省的赋值运算符函数是能够胜任工作的。但在某些特殊情况下, 如类中有指针类型时,使用缺省的赋值运算符函数会产生错误。

#include<iostream>
#include<string.h>
using namespace std;
class stringg
{
    char * ptr;
public :
    stringg( char * s)
    {
        ptr = new char[strlen(s) + 1 ];
        strcpy( ptr,s) ;
    }
    ~stringg()
    {
        delete ptr;
    }
    void print()
    {
        cout << ptr << endl ;
    }
};
int main()
{
    stringg p1 ("chen");
    {
        stringg p2(" ");
        p2 = p1 ;
        // string 类对象间赋值
        cout << "p2: ";
        p2 .print() ;
    }
    cout << "p1: ";
    p1 .print() ;
    return 0;
}

这里我的运行结果和书上写的不一样,我的是这样的

p2: chen
p1: chen

但书上写的是下面的样子,我觉得是书有点老,现在的编译器愈来愈智能,可能编译器本身就已经帮我们重载好了=运算符。
p2 : chen
p1 : ♀ ♀
Null Point assignment
从上述程序运行结果可以看出: p1 的输出结果是错误的。
原因是执行到赋值语句 p2= p1 时, 实际是使对象 p2 的指针 ptr 与对象 p1 的指针 ptr 指向 new 开辟的同一个空间。当 p2 的生命周期( 内层的一对花括号间 ) 结束时, 系统自动调用析构函数将这一空间撤消。这样, p2 中的 ptr 原来所指向的区域被封锁起来, 并不能再被使用, 这就产生了指针悬挂问题。这时尽管 p1 的指针 ptr 存在,其所指向的空间却无法访问了。当 p1 的生命周期结束时, 将再次调用析构函数,释放同一空间, 从而导致运行错误。
在这种情况下,必须显式地定义一个赋值运算符重载函数, 使 p1 和 p2 有各自的存储区,其实现方法如下例所示。

#include<iostream>
#include<string.h>
using namespace std;
class stringg
{
    char * ptr;
public :
    stringg( char * s)
    {
        ptr = new char[strlen(s) + 1 ] ;
        strcpy( ptr,s) ;
    }
    ~stringg()
    {
        delete ptr;
    }
    void print()
    {
        cout << ptr << endl;
    }
    stringg &operator = ( const stringg & ) ; // 声明赋值运算符重载函数
};
stringg &stringg::operator = ( const stringg &s) // 定义赋值运算符重载函数
{
    if ( this == &s) return * this; // 防止 s = s 的赋值
    delete ptr; // 释放掉原区域
    ptr = new char[strlen(s.ptr) + 1]; // 分配新区域
    strcpy( ptr,s.ptr) ; // 字符串拷贝
    return * this;
}
int main()
{
    stringg p1 ("chen") ;
    {
        stringg p2(" ") ;
        p2 = p1 ;
        cout << "p2:";
        p2.print() ;
    }
    cout << "p1:";
    p1 .print() ;
}

运行修改后的程序,就不会产生上面的问题了。因为已释放掉了旧区域, 又按新长度分配了新区域,并且将简单的赋值, 变成了内容的拷贝。程序运行结果如下:
p2 : chen
p1 : chen
说明:

  1. 类的赋值运算符“ = ”只能重载为成员函数, 而不能把它重载为友元函数, 因为若把上述赋值运算符“ = ”重载为友元函数:
friend string &operator = (string &p2,string &p1 )

这时,表达式:p1 = ”chen”将被解释为operator = ( p1,”chen”)这显然是没有什么问题的,但对于表达式”chen”= p1将被解释为:operator = (”chen”, p1)
即 C + + 编译器首先将“chen”转换成一个隐藏的 stringg 对象, 然后使用对象 p2 引用该隐藏对象,并不认为这个表达式是错误的, 从而将导致赋值语句上的混乱。因此双目赋值运算符应重载为成员函数的形式,而不能重载为友元函数的形式。

  1. 类的赋值运算符“ = ”可以被重载, 但重载了的运算符函数 operator = ( ) 不能被继承。
3.7 函数调用运算符“( )”与下标运算符“[ ]”的重载
1 . 重载函数调用运算符“( )”

C + + 中函数调用运算符的一般使用格式为:

基本表达式( < 表达式表> )

可以认为函数调用运算符“ ( )”是一个双目运算符, 其操作数为 < 基 本表达式 > 和< 表达式表 > (可能为空) ,对应的运算符函数为 operator( ) 。
设 obj 是类 myclass 的对象, 而且类 myclass 中定义了重载函数调用运算符“ ( )”的 operator 函数,则函数调用
obj( arg1, arg2 ,…) ;
可被解释为:
obj .operator( ) ( arg1 , arg2, …) ;
双目运算符是由左边的对象产生对 operator 函数调用的。this 指针总是指向产生调用的对象。重载运算符“ ( )”的 operator 函数可以带任何类型的操作数, 返回任何有效的类型。

#include<iostream>
#include<string.h>
using namespace std;
class myclass
{
    int i;
    float j;
public :
    myclass(int x = 0, float y = 0 )
    {
        i = x;
        j = y;
    }
    myclass operator () (int k, float m);
    void display()
    {
        cout << i << " "<< j << "\n";
    }
} ;
myclass myclass::operator() (int k, float m ) // 定义重载运算符“ ( )”函数
{
    i = k + 10;
    j = m + 10.5;
    return * this;
}
int main()
{
    myclass obj1 (10, 11.5 ) ;
    obj1 .display() ;
    obj1( 100, 6.9) ;
    // 重载运算符“ ( )”的调用,可解释为 obj1 .operator( ) (100, 6 .9 )
    obj1.display() ;
    return 0;
}

程序运行结果为:
10 11 .5
110 17 .4

说明:函数调用运算符“ ( )”必须重载为一个成员函数。

2 . 重载下标运算符“[ ]”

C + + 中数组下标运算符的一般使用格式为:

< 基本表达式 > [ < 表达式 > ]

在重载“[ ]”时, C + + 也把它看成双目运算符, 其操作数为 < 基本表达式 > 和 < 表达式 > , 相应的运算符函数为 operator[ ]。
设 obj 是类 myclass 的对象, 类 myclass 中定义了重载“[ ]”的 operator 函数, 则表达式obj[ 5] ;可被解释为:obj .operator[ ] (5 ) ;
对下标运算符重载定义只能使用成员函数,其形式如下:

类型 类名∷operator [ ] (形参)
{
函数体
}

注意:形参只能有一个。
例如,可以对 stringg 数据类型定义以下的下标运算符重载函数:

class stringg
{
    char * ptr;
// …
public:
    stringg( const char * ) ;
    char & operator[] (int) ;
// …
} ;

其中重载“[ ]”的 operator 函数定义为:

char & string∷operator [ ] (int i)
{
    return ptr[ i] ;
}

由于 string∷operator[ ] ( )的返回值在这里是一个 char & , 所以函数可以被用于一个赋值运算符的任一边,例如:

string op = ”abcd”;
op[ 1] = op[ 3] ;

相当于:op .operator[ ] ( 1) = op .operator [ ] (3 ) ;
因而 op 的新值为“adcd”。
在下面的例子中,利用重载下标运算符“ [ ]”来处理数组。

#include<iostream>
#include<string.h>
#include<iomanip>
#include<stdlib.h>
using namespace std;
class sales
{
    char compName [25] ;
    int divisionTotals[5] ;
public :
    void init( char[ ] ) ;
    int &operator[ ] (int) ;
    // 重载运算符“ [ ]”函数的原型
    char * getName()
    {
        return compName ;
    }
};
void sales::init( char CN[ ] )
{
    strcpy( compName, CN) ;
}
int &sales::operator[ ] (int sub) // 定义重载运算符“ [ ]”函数
{
    if (sub < 0 || sub > 4) // 检查数据边界
    {
        cerr << "Bad subscript !"<< sub <<"is not allowed . \n";
        exit( 1) ;
    }
    return divisionTotals[sub] ;
}
int main ()
{
    int totalSales = 0, avgSales;
    sales company;
    company.init("Swiss Cheese") ;
    company[0] = 123;
    // 调用重载运算符“[ ]”, 初始化 divisionTotals[ ]
    company[1] = 456;
    company[2] = 676;
    company[3] = 234;
    company[4] = 786;
    cout << "Here are the sales for "<< company.getName( ) ;
    cout << "′s divisions:\n";
    for (int i = 0; i< 5; i ++ )
    {
        cout << company[i] << "\n";
    }// 调用重载运算符“[ ]”
    for (int i = 0; i < 5 ; i ++ )
    {
        totalSales += company[i] ;
    } // 调用重载运算符“[ ]”
    cout <<"The total sales are "<< totalSales << "\n";
    avgSales = totalSales/ 5 ;
    cout <<"The average sales are "<< avgSales << "\n";
    return 0 ;
}

程序运行结果如下:
Here are the sales for Swiss Cheese′s divisions:
123
456
676
234
786
The total sales are 2275
The average sales are 455
在上述程序中,并没有创建 company 的对象数组。程序中的下标被重载, 以便在使用 company 时用一种特殊的方式工作。下标总是返回和下标对应的那个部门销售额 division Totals[ ] 。
重载下标运算符“[ ]”时, 返回一个 int 的引用, 可使重载的“[ ]”用在赋值语句的左边,因而在 main ( )中, 可对某个部门的销售额 divisionTotals[ ] 赋值。这样, 虽然 division Totals[ ]是私有的, main( ) 还是能够直接对其赋值,而不需要使用函数 init( ) 。
在上述程序中,设有对下标的检验, 以确保被赋值的数组元素的存在。当程序中一旦出现向超出所定义的数组下标范围的数组元素的赋值,便自动终止程序, 以免造成不必要的破坏。
与函数调用运算符“( )”一样, 下标运算符“ [ ]”不能用友元函数重载, 只能采用成员函数重载。

3.8 类型转换
1 . 系统预定义类型间的转换

类型转换是将一种类型的值转换为另一种类型值。对于系统预定义的类型, C + +提供两种类型转换方式, 一种是隐式类型转换 (或称标准类型转换 ) , 另一种是显式类型转换。

  1. 隐式类型转换
    隐式类型转换主要遵循以下规则:
    ① 在赋值表达式 A = B 的情况下, 赋值运算符右端 B 的值需转换为 A 类型后进行赋值。
    ② 当 char 或 short 类型变量与 int 类型变量进行运算时,将 char 或 short 类型转换成int 类型。
    ③ 当两个操作对象类型不一致时, 在算术运算前, 级别低的自动转换为级别高的类型。
  2. 显式类型转换
    显式类型转换常用下述方法表示:
    ① 强制转换法 强制转换法的格式为:(类型名) 表达式
    例如:
    int i, j
    / / …
    cout < < ( float) (i + j) ;

    将整数 i + j 的值强制转换成 float 型后输出。
    ② 函数法 函数法的转换格式为:
    类型名(表达式)
    在上面的例子中,若采用函数法则变为:
    int i, j;
    / / …
    cout < < float(i + j) ;

    此时也将 i + j 的值强制转换成 float 型后输出。
1 . 类类型与系统预定义类型间的转换

前面介绍的是一般数据类型之间的转换。

通过构造函数、通过类类型转换函数、通过运算符重载来实现用户自己定义的类与其它数据类型之间的转换。

  1. 通过构造函数进行类型转换
    构造函数具有类型转换的作用, 它将其它类型的值转换为它所在类的类型的值。
#include<iostream>
using namespace std;
class example
{
private:
    int num;
public :
    example (int) ;
    void print( ) ;
};
example::example( int n )
{
    num = n;
    cout << "Initializing with: "<< num << endl;
}
void example::print( )
{
    cout << "num = "<< num << endl;
}
int main( )
{
    example X = example( 3) ;//①
    X.print( ) ;
    cout << "--------------- \n";
    example Y = 6;//①
    Y.print() ;
}

程序执行结果如下:
Initializing with: 3
num = 3


Initializing with: 6
num = 6
在类 example 中有一个参数为 int 的构造函数 example (int) , 此构造函数可以用来进行类型转换,即完成由 int 型向 example 类类型的转换。
main( ) 函 数中的语 句① 将整 数 3 转 换为 类类型 example 后 赋给对 象 X , 若没 有example (int)构造函数,此语句将是被禁止的。语句②将 6 转换为类类型 example 后赋给对象 Y, 转换也是通过构造函数 example (int)完成的。

  1. 通过类型转换函数进行类型转换
    通过构造函数可以进行类型转换, 但是它的转换功能受到限制。它只能从基本类型(如 int ,float 等)向类类型转换, 而想将一个类类型向基本类型转换是办不到的。**类型转换函数则可以用来把用户定义的类类型转换成基本类型。**它是一种类似显式类型转换的机制。在类中,类型转换函数定义的一般格式为:
class X{
// …
operator type ( )
{
// …
return type 类型的数据;
}
// …
} ;

**其中 type 为要转向的目标类型 (通常是基本类型)。**这个函数既没有参数, 又没有返回类型,但在函数体中必须返回具有 type 类型的一个数据。类类型转换函数的功能是将类 X的对象转换为类型为 type 的数据, 例如:

class X{
private:
int a;
// …
public :
operator int( )
{
return a;
}
// …
} ;

使用类型转换函数的方法与对基本类型强制转换一样,例如:

X obj;
…
cout << int(obj) ;

下面看一个具体的例子。

#include<iostream>
using namespace std;
class complex
{
private:
    float real, imag;
public :
    complex( float r = 0, float i = 0 )
    {
        real = r;
        imag = i;
        cout << "Constructing...";
    }
    operator float()
    {
        cout << "Type changed to float... \n";
        return real;
    }
    operator int()
    {
        cout << "Type changed to int... \n";
        return int( real) ;
    }
    void print()
    {
        cout <<"("<< real <<","<< imag <<")"<< endl;
    }
};
int main( )
{
    complex A(2.2,4.4 ) ;
    A.print() ;
    cout << float(A) * 0.5 << endl;
    complex B(4, 6 ) ;
    B.print() ;
    cout << int(B) * 2 << endl;
}

程序运行结果如下:
Constructing…
(2 .2 ,4 .4 )
Type changed to float …
1 .1
Constructing…
(4 ,6 )
Type changed to int …
8
在以上程序中, 两次调用了类型转换函数。第一次采用显式调用的方式, 将类 complex 的对象 A 转换成 float 类型。第二次采用显式调用的方式, 将类 complex 的对象 B 转换成 int 类型。
说明:

① 类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。类型 转换函数与普通的成员函数一样,也可以在类定义体中声明函数原型, 而将函数体定义在 类外部。
② 在一个类中可以定义多个类型转换函数。C + + 编译器将根据操作数的类型自动地选择一个合适的类型转换函数与之匹配。当在可能出现二义性的情况下, 应显式地使 用类型转换函数进行类型转换。

  1. 运算符重载与类型转换
    在某些情况下, 用户希望将基本类型的数据与类对象进行运算。通过运算符重载可以提供这种方法。例如,下列程序中通过重载运算符“ + ”, 使类 complex 的对象能够与一个整型变量相加。
#include<iostream>
using namespace std;
class complex
{
private:
    float real, imag;
public :
    complex( float r = 0, float i = 0 )
    {
        real = r;
        imag = i;
    }
    friend complex operator + (int x, complex y)
    {
        complex temp;
        temp.real = x + y.real;
        temp.imag = y.imag;
        return temp;
    }

    void print()
    {
        cout<<"("<< real<<","<< imag <<")"<< endl;
    }
};

int main()
{
    complex ob1( 10.5,20.5),ob2;
    int x = 100;
    ob1.print() ;
    ob2 = x + ob1 ; // 整型变量 x 与类 complex 的对象 ob1 相加
    ob2 .print( ) ;
    return 0;
}

程序运行结果如下:
(10 .5, 20 .5 )
(110 .5 ,20 .5)

4 . 虚 函 数

虚函数是重载的另一种表现形式。这是一种动态的重载方式, 它提供了一种更为灵活的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立, 也就是在运行时才决定如何动作,即所谓的动态联编。
先介绍引入派生类后的对象指针, 然后再介绍虚函数。

4 .1 引入派生类后的对象指针

一般对象的指针, 它们彼此独立, 不能混用。引入派生类后,由于派生类是由基类派生出来的,因此指向基类和派生类的指针也是相关的。请看下面的例子。

#include<iostream>
using namespace std;
class mybase
{
    int a, b;
public :
    mybase(int x, int y)
    {
        a = x;
        b = y;
    }
    void show()
    {
        cout << "mybase---------- \n";
        cout << a << " "<< b << endl;
    }
};
class myclass : public mybase
{
    int c;
public :
    myclass(int x, int y, int z) : mybase( x, y)
    {
        c = z;
    }
    void show()
    {
        cout << "myclass--------- \n";
        cout << "c = "<< c << endl;
    }
};
int main( )
{
    mybase mb(50, 50 ), * mp;
    myclass mc( 10,20, 30 ) ;
    mp = &mb;
    mp -> show() ;
    mp = &mc;
    mp -> show() ;
}

程序运行结果如下:
mybase----------
50 50
mybase----------
10 20
从程序运行的结果可以看出,虽然执行语句**mp = &mc;后,指针 mp 已经指向了对象 mc, 但是它所调用的函数( mp -> show( ) ) ,**仍然是其基类对象的 show( ) , 显然这不是我们所期望的。
引入派生类后,使用对象指针应注意的几个问题:

  1. 声明为指向基类对象的指针可以指向它的公有派生的对象, 但不允许指向它的私有派生的对象,例如:
class base
{
// …
};
class derive:private base
{
// …
};
int main()
{
    base op1, * ptr;  // 定义基类 base 的对象 op1 及 base 类指针 ptr #
    derive op2 ; // 定义派生类 derive 的对象 op2
    ptr = &op1 ; // 将指针 ptr 指向对象 op1
    ptr = &op2 ; // 错误,不允许将 base 类指针 ptr
// 指向它的私有派生类对象 op2
// …
}

  1. 允许将一个声明为指向基类的指针指向其公有派生类的对象, 但是不能将一个声明为指向派生类对象的指针指向其基类的对象。
  2. 声明为指向基类对象的指针, 当其指向公有派生类对象时, 只能用它来直接访问派生类中从基类继承来的成员,而不能直接访问公有派生类中定义的成员, 例如:
#include<iostream>
using namespace std;
class A
{
    // …
public :
    void print1 () ;
} ;
class B: public A
{
    // …
public :
    print2() ;
};
int main()
{
    A op1, * ptr;
    // 定义基类 A 的对象 op1 和基类指针 ptr
    B op2;
    // 定义派生类 B 的对象 op2
    ptr = &op1 ;
    // 将指针 ptr 指向基类对象 op1
    ptr -> print1() ;
    // 调用基类函数 print1( )
    ptr = &op2 ;
    // 将指针 ptr 指向派生类对象 op2
    ptr -> print1() ;
    // 调用对象 op2 从其基类继承来的成员函数 print1( )
    ptr -> print2() ;
    // 错误,基类指针 ptr
    // 不能访问派生类中定义的成员函数 print2( )
}

若想访问其公有派生类的特定成员,可以将基类指针用显式类型转换为派生类指针。例如那条错误的语句可改写成:
**( (B * ) ptr) - > print2 ( ) ;**外层的括号表示对 ptr 的强制转换, 而不是返回类型。

4 .2 虚函数的定义及使用
1 . 虚函数的作用

在上例中, 虽然基类指针 mp 已经指向了派生类对象 mc, 但是它所调用的函数**( mp - > show( ) )仍然是其基类对象的 show( )。**这说明, 不管指针 mp 当前指向哪个对象(基类对象或派生类对象) , **mp - > show( )**调用的都是基类中定义的 show( )函数版本。
**其原因在于普通成员函数的调用是在编译时静态联编的。**在这种情况下, 若要调用派生类中的成员函数,必须采用显式的方法, 例如:

mc .show( ) ;

或者采用对指针强制类型转换的方法,如:

( ( my - class * ) mp) - > show( ) ;

使用对象指针的目的是为了表达一种动态的性质, 即当指针指向不同对象时执行不同的操作, 但是采用以上两种方法都没有起到这种作用。其实只要将函数show( )说明为虚函数,就能实现这种动态调用的功能。
下面的程序将上例中的函数 show( )定义为虚函数, 就能实现动态调用的功能。

#include<iostream>
using namespace std;
class mybase
{
    int a, b;
public :
    mybase(int x, int y)
    {
        a = x;
        b = y;
    }
    virtual void show() // 定义虚函数 show( )
    {
        cout << "mybase----------- \n";
        cout << a << " "<< b << endl;
    }
};
class myclass :public mybase
{
    int c;
public :
    myclass(int x, int y, int z) : mybase( x, y)
    {
        c = z;
    }
    void show( ) // 重新定义虚函数 show( )
    {
        cout << "myclass---------- \n";
        cout << c << endl;
    }
} ;
int main()
{
    mybase mb(50, 50 ), * mp;
    myclass mc( 10,20, 30 ) ;
    mp = &mb;
    mp -> show() ; // 调用基类 my - base 的 show( )版本
    mp = &mc;
    mp -> show() ; // 调用派生类 my - class 的 show( )版本
    return 0;
}

程序运行结果如下:
my - base----------
50 50
my - class---------
30
关键字 virtual 指示 C + + 编译器,函数调用 my - > show( )要在运行时确定所要调用的函数,即要对该调用进行动态联编。因此, 程序在运行时根据指针 mp 所指向的实际对象,调用该对象的成员函数。
可见,虚函数同派生类的结合可使 C + + 支持运行时的多态性, 而多态性对面向对象的程序设计是非常重要的,实现了在基类定义派生类所拥有的通用接口, 而在派生类定义具体的实现方法,即常说的“同一接口,多种方法”,它帮助程序员处理越来越复杂的程序。

2 . 虚函数的定义

虚函数就是在基类中被关键字 virtual 说明, 并在派生类中重新定义的函数。在派生类中重新定义时,其函数原型, 包括返回类型、函数名、参数个数与参数类型的顺序, 都必须与基类中的原型完全相同。

#include<iostream>
using namespace std;
class parent
{
protected:
    char version;
public :
    parent()
    {
        version ='A';
    }
    virtual void print() // 定义虚函数 print( )
    {
        cout << "\n The parent . version "<< version;
    }
};
class derived1: public parent
{
private:
    int info;
public :
    derived1 (int number)
    {
        info = number;
        version ='1';
    }
    void print() // 重新定义虚函数 print( ) Q
    {
        cout <<"\n The derived 1 info:"<< info << "version "<< version;
    }
};
class derived2: public parent
{
private:
    int info;
public :
    derived2 (int number)
    {
        info = number;
    }
    void print() // 重新定义虚函数 print( )
    {
        cout << "\n The derived2 info:"<< info << "version "<< version;
    }
};
int main()
{
    parent ob, * op;
    op = &ob;
    op -> print() ; // 调用基类 parent 的 print( )
    derived1 d1(3) ;
    op = &d1;
    op -> print() ; // 调用派生类 derived1 的 print( )
    derived2 d2(15 ) ;
    op = &d2;
    op -> print() ; // 调用派生类 derived2 的 print( )
}

该程序在基类 parent 中定义了一个虚函数 print ( ) , 这个类有一个保护数据 version具有 char 类型。类 parent 派生出两个子类 derived1 和 derived2 ,分别具有虚函数print( )。
在主函数 main( )中说明了三个对象, 基类 parent 的 ob,派生类 derived1 的 d1 和派生类 derived2 的 d2。在程序中,语句
op - > print( ) ;
出现了三次,由于 op 指向的对象不同,每次出现都执行了 print( ) 的不同版本。程序运行的结果为:
The parent . version A
The derived1 info: 3 version 1
The derived2 info: 15 version A
对虚函数的定义作几点说明:

  1. 在基类中, 用关键字 virtual 可以将其 public 或 protected 部分的成员函数声明为虚函数,如上例中的
    virtual void print( ) {…}
  2. 在派生类对基类中声明的虚函数进行重新定义时, 关键字 virtual 可以写也可以不写。但在容易引起混乱的情况下,最好在对派生类的虚函数进行重新定义时也加上关键字 virtual。
  3. 虚函数被重新定义时, 其函数的原型与基类中的函数原型必须完全相同。
  4. 定义了虚函数后, 在 main( )中声明的指向基类的指针 op 允许指向其派生类。在执行过程中, 不断改变它所指向的对象, op - > print( ) 就能调用不同的版本, 而且这些动作都是在运行时动态实现的。可见用虚函数充分体现了面向对象程序设计的动态多态性。
  5. 虽然使用对象名和点运算符的方式也可以调用虚函数, 例如语句**d1 .print( ) ;**可以调用虚函数 derived1∷print( ) , 但是这种调用是在编译时进行的静态联编, 它没有充分利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。
  6. 一个虚函数无论被公有继承多少次, 它仍然保持其虚函数的特性。
  7. 虚函数必须是其所在类的成员函数, 而不能是友元函数, 也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。但是虚函数可以在另一个类中被声明为友元函数。
  8. 构造函数不能是虚函数, 但析构函数可以是虚函数。
3 . 虚函数与重载函数的关系

在一个派生类中重新定义基类的虚函数是函数重载的另一种形式, 但它不同于一般的函数重载。
当普通的函数重载时,其函数的参数或参数类型必须有所不同, 函数的返回类型也可以不同。但是,当重载一个虚函数时, 也就是说在派生类中重新定义虚函数时, 要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。如果仅仅返回类型不同,其余均相同, 系统会给出错误信息; 若仅仅函数名相同, 而参数的个数、类型或顺序不同,系统将它作为普通的函数重载, 这时虚函数的特性将丢失。

#include<iostream>
using namespace std;
class base
{
public :
    virtual void func1() ;
    virtual void func2() ;
    virtual void func3() ;
    void func4() ;
};
class derived: public base
{
public :
    virtual void func1() ; // 是虚函数,这里可不写 virtual
    void func2(int x) ; // 作为普通函数重载,虚特性消失
    char func3 () ; // 错误,因为只有返回类型不同, 应删去
    void func4() ; // 是普通函数重载,不是虚函数
} ;
void base::func1 ( )
{
    cout << "- - base func1 - - \n";
}
void base::func2 ( )
{
    cout << "- - base func2 - - \n";
}
void base::func3 ( )
{
    cout << "- - base func3 - - \n";
}
void base::func4 ( )
{
    cout << "- - base func4 - - \n";
}
void derived::func1( )
{
    cout << "- - derived func1 - - \n";
}
void derived::func2(int x)
{
    cout << "- - derived func2 - - \n";
}
void derived::func4( )
{
    cout << "- - derived func4 - - \n";
}
int main( )
{
    base d1, * bp;
    derived d2;
    bp = &d2 ;
    bp -> func1 ( ) ; // 调用 derived∷func1( )
    bp -> func2 ( ) ; // 调用 base∷func2( )
    bp -> func4 ( ) ; // 调用 base∷func4( )
}

删除语句 char func3( )后, 程序执行结果如下:
– derived func1 - -
– base func2 - -
– base func4 - -

由于派生类 derived 中的函数 fun( ) 有不同的继承路径,所以呈现不同的性质。
区分的地方就在于,fun()方法在基类中到底是虚函数还是一般成员函数,前者则调用派生类方法,后者相当于一个重载函数,只呈现普通函数的重载特性。
在 main( )主函数中, 定义了一个基类指针 bp, 当 bp 指向派生类对象 d2 时, b - >func1 ( )执行的是派生类中的成员函数, 这是因为 func1 ( )为虚函数; b - > func2 ( ) 执行的是基类的成员函数,因为函数 func2 ( ) 丢失了虚特性,故按照普通的重载函数来处理; 函数func3 ( )是错误的, 本例中将其删除; bp - > func4 ( ) 执行的是基类的成员函数, 因为 func4( ) 为普通的重载函数,不具有虚函数的特性。

4 . 多重继承与虚函数

多重继承可以视为多个单继承的组合。因此,多重继承情况下的虚函数调用与单继承情况下的虚函数调用有相似之处。

#include<iostream>
using namespace std;
class base1
{
public :
    virtual void fun() // 定义 fun( )是虚函数 {
    {
        cout << "-------base1------- \n";
    }
} ;
class base2
{
public :
    void fun() // 定义 fun()为普通的成员函数
    {
        cout << "-------base2------- \n";
    }
};
class derived: public base1, public base2
{
public :
    void fun( )
    {
        cout << "-------derived------ \n";
    }
} ;
int main()
{
    base1 * ptr1; // 定义指向基类 base1 的指针 ptr1
    base2 * ptr2; // 定义指向基类 base2 的指针 ptr2
    derived obj3; // 定义派生类 derived 的对象 obj3
    ptr1 = &obj3;
    ptr1 -> fun () ;
    //  此处的 fun( )为虚函数, 因此调用派生类 derived 的 fun( )
    ptr2 = &obj3;
    ptr2 -> fun () ; // 此处的 fun( )为非虚函数, 而 ptr2 又为 base2 的指针, 因此调用基类 base2 的 fun ( )
}

程序运行结果如下:
-------derived-------
-------base2-------

从程序运行结果可以看出,由于派生类 derived 中的函数 fun( ) 有不同的继承路径,所以呈现不同的性质。相对于 base1 的派生路径, 由于 base1 中的 fun( ) 是虚函数,当声明为指向 base1 的指针指向派生类 derived 的对象 obj3 时, 函数 fun ( )呈现出虚特性。因此, 此时的 ptr - > fun( )调用的是 derived∷fun ( ) 函数; 相对于 base2 的派生路径, 由于 base2 中的 fun( )是一般成员函数, 所以此时它只能是一个重载函数, 当声明为指向 base2 的指针指向 derived 的对象 obj3 时, 函数 fun( )只呈现普通函数的重载特性。因此, 此时的 ptr -> fun ( )调用的是 base2∷fun ( )函数。

4 .3 纯虚函数和抽象类

纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义, 但要求在它的派生类中定义自己的版本,或重新说明为纯虚函数。
纯虚函数的一般形式如下:

virtual type func - name (参数表) = 0;

这里, type 是函数的返回类型, func - name 是函数名,此形式与一般的虚函数形式基本相同,只是在后面多了“ = 0”。
下面是一个使用纯虚函数的例子。

#include<iostream>
using namespace std;
class circle
{
protected:
    int r;
public :
    void setr(int x)
    {
        r = x;
    }
    virtual void show( ) = 0;
    // 纯虚函数
};
class area: public circle
{
public :
    void show() // 重新定义虚函数 show()
    {
        cout << "area is "<< 3.14 * r * r << endl;
    }
} ;
class perimeter: public circle
{
public :
    void show() // 重新定义虚函数 show( )
    {
        cout << "perimeter is "<< 2 * 3.14 * r << endl;
    }
};
int main( )
{
    circle * ptr;
    area ob1 ;
    perimeter ob2;
    ob1.setr( 10 ) ;
    ob2.setr( 10 ) ;
    ptr = &ob1 ;
    ptr -> show( ) ;
    ptr = &ob2 ;
    ptr -> show( ) ;
}

程序运行结果如下:
area is 314
perimeter is 62 .8
在以上例子中, circle 是一个基类,它表示一个圆。从它可以派生出面积类 area 和周长类 perimeter。显然, 基类中定义的 show( ) 函数是没有任何意义的, 它只是用来提供派生类使用的公共接口,所以在程序中将其定义为纯虚函数, 但在派生类中, 则根据它们自身的需要,具体地重新定义虚函数。
如果一个类至少有一个纯虚函数,那么就称该类为抽象类。因此, 上述程序中定义的类 circle 就是一个抽象类。对于抽象类的使用有以下几点规定:

  1. 由于抽象类中至少包含有一个没有定义功能的纯虚函数, 因此抽象类只能用作其它类的基类,不能建立抽象类对象。
  2. 抽象类不能用作参数类型、函数返回类型或显式转换的类型。但可以声明指向抽象类的指针或引用,此指针可以指向它的派生类, 进而实现多态性。
    下面分析一个例子。
class shape
{
// …
public :
// …
    virtual void rotateshape (int) = 0 ;
    virtual void drawshape ( ) = 0;
    virtual void hiliteshape( ) = 0 ;
};
shape s1; // 错误,不能建立抽象类的对象
shape * ptr; // 正确,可以声明指向抽象类的指针
shape f() ; // 错误,抽象类不能作为函数的返回类型
shape g(shape s) ; // 错误,抽象类不能作为函数的参数类型
shape &h (shape & ) ; // 正确,可以声明抽象类的引用

  1. 如果在抽象类的派生类中没有重新说明纯虚函数, 而派生类只是继承基类的纯虚函数, 则这个派生类仍然还是一个抽象类。例如对于抽象类 shape, 若定义如下的派生类:
#include<iostream>
using namespace std;
class circlepublic shape
{
    int radius;
public :
    void rotateshape(int) ;
    void drawshape( ) ;
}

由于 shape∷hiliteshape ( ) 是一个纯虚函数, 缺省的 circle∷hiliteshape ( ) 也是一个纯虚函数,这时 circle 仍为抽象类。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值