C++ 第七章 面向对象编程(二)

一、代码重用
    1.1 结构化程序设计中的代码重用
    1.2 面向对象程序设计中的代码重用
二、类的组合
    2.1 组合类的定义方法
    1、组合类的定义和访问:
    2、多级访问的语法形式:
    3、如何设计组合类中对象成员的访问权限
    4、组合类对象的构造和析构
    5、类的聚合
    6、总结
三、类的继承与派生
    3.1继承与派生的编程原理:
    3.2 派生类对基类成员的二次封装
    3.3 派生类对象的定义与访问
    3.4 如何设计派生类的继承方式
    3.5 保护权限与保护继承
    3.6 派生类对象的构造和析构
        1、派生类的构造函数
    3.7 派生类的析构函数
四、多态性
    4.1 运算符多态与重载
        1、复数类的加法运算
        2、为复数类重载单目运算符“++”
        3、复数类重载关系运算符“==”
        4、赋值运算符”=“
        5、运算符重载的语法细则
    4.2 对象的替换与多态
        1、liskov替换准则
        2、对象多态性
        3、抽象类
五、关于多继承的讨论
    5.1多个基类之间的重名
    5.2 重复继承
    5.3 关于多继承的讨论

面向对象程序设计之所以能够有效提高程序开发效率是因为:

  1. 分类管理程序代码,即类与对象编程
  2. 重用类代码
    • 使用类定义对象
    • 使用已有类定义新类
      • 组合
      • 继承

本章将讲解使用已有的类定义新的类,即类的组合和继承。

一、代码重用

程序 = 数据 + 算法,程序中的数据,包括原始数据,中间结果和最终结果,如何根据所处理的数据来合理的使用和管理内存,是编写程序的第一项工作内容,c++语言通过定义变量语句来申请内存空间,定义变量就是与数据相关的代码;将数据的处理过程,细分成一组严格的操作步骤,这组操作步骤就被称为算法,如何设计数据处理算法,是编写程序的第二项工作内容,c++语言通过定义函数来描述算法模块,函数是与算法相关的代码。这一节,首先比较结构化程序设计与面向对象程序设计在代码重用方面的不同,然后再介绍3种面向对象程序中3种代码重用的不同方法。

1.1 结构化程序设计中的代码重用

结构化程序设计重用的是函数代码,换句话说,结构化程序设计重用的是算法代码,没有重用数据代码。

1.2 面向对象程序设计中的代码重用

面向对象程序设计既重用数据代码,也重用函数代码,因此开发效率更高。

重用类代码有3种形式:

  1. 用类定义对象
  2. 通过组合定义新的类(称为组合类)
  3. 通过继承定义新的类(称为派生类)

类的5大要素:

本章主要代码,以圆形类Circle进行讲解,这个段代码分为两个文件,分别如下:

类头文件:类声明部分,Circle.h

class Circle
{
    private:
        double r;
    public:
        void Input();//输入半径
        double CRadius();//读取半径
        double CArea();//求面积
        double CLen();//求周长
        
        Circle();//无参构造函数
        Circle(doublex);//有参构造函数
        Circle(Circle&);//拷贝构造函数
};

类源程序文件:类实现部分,Circle.cpp

#include <iostream>
using namespace std;
#include "Circle.h"

void Circle::Input()
{
    cin>>r;
    while (r<0)//检查数据合法性
    {
        cin>>r;//如果r<0,则重新输入
    }
}

double Circle::CRadius()//读取半劲
{ return r; }

double Circle::CArea()//求面积
{ return(3.14*r*r); }

double Circle::CLen()//求周长
{ return(3.14*r*2); }

Circle::Circle()//无参构造函数
{ r = 0; }

Circle::Circle(double x)//有参构造函数
{
    if(x<0) r=0;//如果r<0则,置0
    else r=x;
}

Circle::Circle(Circle&x)//拷贝构造函数
{ r = x.r;}

使用类Circle定义对象的典型流程:

Circle obj;
obj.Input();
cout<<obj.CRadius()<<endl;
cout<<obj.CArea()<<endl;
cout<<obj.CLen()<<endl;

在使用圆形类Circle定义对象的时候,也可以对对象进行初始化

Circle obj1;//调用无参构造函数
Circle obj2(5);//调用有参构造函数
Circle obj3(obj2);//调用拷贝构造函数

因为我们没有为Circle定义析构函数,c++语言编译器将自动为该类添加一个空的析构函数,因为一个类从语法上讲,必须有析构函数,即:

~Circle(){}

如何使用Circle类定义更复杂的累?就要用到类的组合方法和继承方法。

二、类的组合

类不是c++语言预定义的基本数据类型,而是由多个类型的数据成员组合在一起,形成的自定义数据类型,用简单的零件组装复杂的整体,是人们常用的一种方法,程序员可以将别人编写的类,当做零件,就是零件类,在此基础上定义自己的新类,我们称为整体类,这就是类的组合。

2.1 组合类的定义方法

假设有一个几何图形是由3个圆组成的,我们要编写一个类TriCircle来描述这样的几何图形,编写TriCircle类可以从零开始编写,也可以基于前文中的Circle类编写组合类。类TriCircle可以认为是由3个Circle类对象组合而成的。我们使用组合类的方法来定义TriCircle,代码如下:

头文件:TriCircle.h

#include "Circle.h" //声明类Circle

class TriCircle //类声明部分
{
    //二次封装
    public:
        Circle c0,c1,c2;//3个公有Circle类对象成员
        double TArea();//求面积
        double TLen();//求周长
};

类源程序文件:TriCircle.cpp

#include "TriCircle.h"//声明类TriCircle
//类实现部分,具体的函数代码
double TriCircle::TArea()//求面积
{
    double totalArea;
    totalArea = c0.CArea()+c1.CArea()+c2.CArea();//访问对象成员的下级成员受权限控制
    return totalArea;
}

double TriCircle::TLen()//求周长
{
    double totalLen;
    totalLen = c0.totalLen()+c1.totalLen()+c2.totalLen();
    return totalLen;
}

1、组合类的定义和访问:

与普通类一样,可以使用组合类来定义对象和访问对象实例,计算机执行对象定义语句时,会自动为对象分配内存空间,一个组合类对象所占用的内存空间等于类中全部数据成员,其中包括对象成员,它们所需内存空间的总和。

TriCircle obj;//定义对象
obj.c0;obj.c1;obj.c2;obj.TArea();Obj.TLen();//访问对象

2、多级访问的语法形式:

组合类对象名对象成员名.对象成员的下级成员名

综上所述,主函数调用TriCircle进行计算时,代码如下:

#include <iostream>
using namespace std;
#include "TriCircle.h"
int main()
{
    TriCircle obj;//定义一个组合类TriCircle的对象obj
    //调用组合类对象obj中对象成员c0的下级函数成员Input,输入c0半径
    obj.c0.Input();
    //再调用c0的下级函数成员CArea和CLen计算c0的面积和周长。
    cout<<obj.c0.CArea()<<","<<obj.c0.CLen()<<endl;

    //同理可以计算出c1和c2的面积和周长
    obj.c1.Input();cout<<obj.c1.CArea()<<","<<obj.c1.CLen()<<endl;
    obj.c2.Input();cout<<obj.c2.CArea()<<","<<obj.c2.CLen()<<endl;

    //调用组合类obj中的非对象成员TArea和TLen,计算显示总面积和总周长。
    cout<<obj.TArea()<<","<<obj.TLen()<<endl;
    return 0;
}

通过对象指针,也可以间接访问组合类对象及其下级成员,实例如下:

TriCircle obj;//定义一个组合类TriCircle的对象obj
TriCircle *p = &obj;//定义一个组合类TriCircle的对象指针p,让其指向对象obj。
//通过对象指针,间接访问组合类对象obj中对象成员的下架函数成员
p->c0.Input();
cout<< p->c0.CArea() << "," << p->c0.CLen() << endl;

3、如何设计组合类中对象成员的访问权限

组合类将零件类的对象作为自己的数据成员,及对象成员,相当于是用零件组装产品,用零件组装产品时,要考虑,是将零件直接暴露给用户还是将零件隐藏起来。通常这个问题是要根据产品及零件的功能来决定的。

组合类编程中有2种角色,分别是定义组合类的程序员使用组合类的程序员

4、组合类对象的构造和析构

按照数据类型的不同,组合类中的数据成员可以分为2种,即类类型的对象成员基本类型的非对象成员,构造组合类类型时,将首先构造对象成员,然后在构造非对象成员,在构造过程中,计算机将自动调用组合类的构造函数,先初始化对象成员,再初始化非对象成员。

在组合类的析构过程中,对象成员与非对象成员的析构顺序和构造时的顺序正好相反。及先析构非对象成员再析构非对象成员。析构时会自动调用组合类的析构函数。

组合类的构造函数:

构造函数通过形成传递初始值,实现对新建成员的初始化,组合类构造函数不能直接初始化类中的对象成员,因为对象成员的下级数据成员可能是私有的,不能访问赋值,要想初始化这些对象成员,必须要通过其所属类的构造函数才能完成,调用对象成员所属类的构造函数,其语法形式是在组合类构造函数的函数头后面添加初始化列表,语法如下:

组合类构造函数名(形参列表):对象成员名1(形参1),对象成员名2(形参2),......
{
    ... ...//在函数体中初始化其他非对象成员。
}
//有参构造函数
TriCircle::TriCircle(double p0,double p1, double p2):c0(p0),c1(p1),c2(p2)
{
     ......//非对象成员在函数体中初始化
}
//无参构造函数
TriCircle::TriCircle(){}
//拷贝构造函数
TriCircle::TriCircle(TriCircle&rObj):c0(rObj.c0),c1(rObj.c1),c2(rObj.c2){}

组合类中的析构函数:

5、类的聚合

聚合类:数据成员中包含对象指针的类

聚合类pTriCircle定义代码:

类头文件:pTriCircle.h

#include "Circle.h"//声明类Circle
class pTriCircle //声明部分,即声明成员
{
    public:
        Circle *p0,*p1,*p2;//公有Circle类的对象指针
        double TArea();//求总面积
        double TLen();//求总周长
};

c++语言将数据成员中包含对象成员的类,称为组合类,而将数据成员中包含对象指针的类,称为聚合类。聚合类是一种特殊形式的组合类。

类源程序文件:pTriCircle.cpp

#include "pTriCircle.h"//声明类pTriCricle
//pTriCricle类实现部分
double pTriCircle::TArea()
{
    double totalArea;
    //用对象指针,间接访问对象成员的下级成员
    totalArea = p0->CArea() + p1->CArea() + p2->CArea();
    return totalArea;
}
double pTriCircle::TLen()
{
    double totalLen;
    //用对象指针,间接访问对象成员的下级成员
    totalLen = p0->CLen() + p1->CLen() + p2->CLen();
    return totalLen;
}

主函数代码:

#include <iostream>
using namespace std;
#include "pTriCircle.h"//类pTriCircle的声明头文件
int main()
{
    Circle c0,c1,c2;//先定义3个类Circle的对象c0,c1,c2
    c0.Input(); c1.Input(); c2.Input();//输入3个圆的半径

    pTriCircle obj1;//定义1个聚合类pTriCircle的对象obj1
    //将obj1中的3个对象指针分别指向已经创建的Circle类对象c0,c1,c2
    obj1.p0 = &c0; obj1.p1 = &c1; obj1.p2 = &c2;
    //调用obj1中的函数成员TArea和TLen,计算并显示总面积和总周长
    cout<<obj1.TArea()<<","<<obj1.Tlen()<<endl;
        return 0;
  }

6、总结

类的组合和聚合:

区别:

组合类总结:

三、类的继承与派生

设计新类时可继承已有类,这个已有的类呗称为基类或父类。

基类是为解决以前的老问题设计的,在面对新问题时其功能可能会显得不够完善,程序员需要在继承的基础上对基类进行派生,例如添加新功能,或者对从基类继承来的功能进行某些修改,派生的目的时为了解决新问题。

通过继承与派生所得到的新类被称为派生类子类

3.1继承与派生的编程原理:

定义派生类的语法:

class 派生类名:继承方式1 基类1,继承方式2 基类2,... ... //派生类声明部分
{
    public:
        新增公用成员
    protected:
        新增保护成员
    private:
        新增私有成员
};
//派生类实现部分,各函数成员的完整定义代码

语法说明:

代码实例:假设要定义一个圆环类BorderCircle类

基于已有的基类Circle类来编写派生类,BorderCircle可以继承Circle类中的半径r、求面积和周长的函数CArea、CLen;在此基础上新增圆环宽度w,求内院面积和边框面积的函数InnerArea、BorderArea;因为Circle中的input函数,只能输入半径,为此BorderCircle从新定义了1个Input函数,这相当于是修改了原Input函数。新的Input函数能够同时输入半径和圆环的宽度。

类文件头:BorderCircle.h

#include "Circle.h"//声明基类Circle
class BorderCircle:public Circle //公有继承
{
    public:
        double w; //宽度
        double InnerArea();//求内圆面积
        double BorderArea();//圆环面积
        void Input();//输入半径和圆环宽度,与Circle中的Input重名,将覆盖基类中的Input
};

类程序文件:BorderCircle.cpp

#include <iostream>
using namespace std;
#include "BorderCircle.h
"double BorderCircle::InnerArea()
{
    double x = CRadius();//读取半径
    return(3.14*(x-w)*(x-w));
}

double BorderCircle::BorderArea()
{
    return(CArea()-InnerArea());
}

void BorderCircle::Input()
{
    Circle::Input();//调取输入半径函数,访问被同名覆盖后的基类成员Input()
    cin>>w;//输入边框宽度}

派生类中,新增函数成员可以和基类函数成员重名,但不是重载函数,例如上述例子中的Input函数,这属于同名覆盖

同名覆盖:派生类中定义与基类成员重名的新增成员,新增成员讲覆盖基类成员。通过成员名访问时,所访问到的是新增成员,这就是新增成员对基类成员的同名覆盖。同名覆盖后,被覆盖的基类成员任然存在,只是被隐藏了,可以访问被覆盖的基类成员,其访问形式是:“基类名::基类成员名”。

同名覆盖的目的是修改基类中的功能。

3.2 派生类对基类成员的二次封装

派生类通过继承方式,对向基类继承来的成员进行二次封装,如果采用公有继承的方式,则是将基类的成员中的公有成员全部开放,如果是私有或保护继承,者将基类的成员隐藏起来,在派生类外部是无法访问的,如果基类的成员本身就是私有或保护成员,派生类继承过后该成员的权限不变,只能通过基类的函数进行间接访问,不能再派生类中直接调用。

3.3 派生类对象的定义与访问

一个派生类所占用的数据空间,等于该类中全部数据成员所占空间,其中包含基类的数据成员和新增的数据成员。派生类对象的定义方法和普通类一样。

BorderCircle obj;//定义一个派生类对象obj,包含数据成员r(基类)和w(新增)

访问公有的基类成员:

obj.Circle::Input();//Input被同名覆盖,访问基类中的Input,需指定类名。
//基类公有成员的方法方法。
obj.CRadius();
obj.CArea();
obj.CLen();

r是基类Circle中的私有成员,不能访问。

访问公有的新增成员:

//与普通对象方法一样
obj.w;
obj.InnerArea();
obj.BorderArea();
obj.Input;

派生类中的所有成员,不管是继承于基类还是新增的成员,只要是公有权限,就都可以访问。私有权限和保护权限就不能访问。可以看出,访问派生类对象中的成员和普通类是一样的,只有在同名覆盖的情况下有例外。

主函数:main.cpp

#include <iostream>
using namespace std;
#include "BorderCircle.h"
int main()
{
    BorderCircle obj;//定义一个派生类对象BorderCircle的对象obj
    //调用新增成员(同名覆盖)Input()函数,输入半径和边框宽度
    obj.Input();

    //调用基类中的CArea和CLen计算圆的面积和周长
    cout<<obj.CArea()<<","<<obj.CLen()<<endl;
    //调用新增成员InnerArea和BorderArea计算内圆面积和圆环面积
    cout<<obj.InnerArea()<<","<<obj.BorderArea()<<endl;
    return 0;
}

3.4 如何设计派生类的继承方式

通过继承基类,重用基类代码,可以降低派生类的工作量,提供工作效率

3.5 保护权限与保护继承

保护权限protected是半开放的,那么保护权限是在什么情况下是开放的,什么情况下是封闭的。先看一个例子:

程序员甲,定义类A(头文件A.h)

//数据成员x,y,z分别是公有权限,私有权限和保护权限
class A
{
    public:
        A(int p1=0, int p2=0, int p3=0)//构造函数
        {
            x = p1; y = p2; z = p3;
        }
        int x;
    private:
        int y;
    protected:
        int z;
};

程序员乙,使用类A定义对象(1.cpp)

#include <iostream>
using namespace std;
#include "A.h"
int main()
{
    A obj(10,20,30);
     cout<<obj.x<<endl;//正确,公有权限
    cout<<obj.y<<endl; //错误,私有权限
    cout<<obj.z<<endl;//错误,保护权限
    return 0;
}  

程序员丙,使用类A定义派生类B(头文件B.h)

#include <iostream>
using namespace std;
#include "A.h"
class B:public A
{
    public:
        void funB()//新增成员访问基类成员
        {
            cout<<x<<endl;//正确,公有权限
            cout<<y<<endl; //错误,私有权限
            cout<<z<<endl;//正确,保护权限
        }
};   

通过上述例子,可以看出,通过对象访问成员,只能访问公有对象的成员,而保护权限和私有权限都不能访问;在派生类新增函数成员中访问基类成员时,此时公有成员和保护成员可以被访问,但是私有成员同样不能被访问。故类的保护权限是向其派生类定向开放的一种权限。

保护继承:基类中的public成员在保护继承后,访问权限被降级为protected,而基类中的保护成员和私有成员的访问权限不变。

派生类通过继承的方式,对基类继承来的成员进行二次封装,公有继承和私有继承,对派生类外部函数一视同仁,要么不封装,要么全部封装,而保护继承protected则不同,派生类保护继承基类,对下级新增函数成员来说,派生类的基类成员没有被封装,但是对派生类所有其他外部函数来说,这些成员是被封装起来的。这就是所谓的半封装,也就是说,保护继承,对下级派生类保持开放,而对其他函数来实,这些基类成员被隐藏了。

总结:派生类的保护继承是向其下级派生类定向开放的一种半封装。

3.6 派生类对象的构造和析构

派生类有2类成员,基类成员和新增成员,这两个成员的构造和析构是不一样的。在构造派生类对象过程中,计算机将自动调用派生类的构造函数,先初始化基类成员,在初始化新增成语,在派生类的析构过程中,顺序与构造时的顺序相反,即先析构新增成员,再析构基类成员,

1、派生类的构造函数

构造函数通过形参传递初始值,实现对新建对象数据成员的初始化,派生类中的构造函数不能直接初始化基类中的数据成员,因为它们再基类中可能是私有的,不能访问赋值,要想初始化这些基类成员,必须通过基类的构造函数才能完成,调用基类构造函数,其语法形式,是在派生类构造函数的函数名后面添加初始化列表,

派生类构造函数名(形参列表):基类名1(形参1),基类名2(形参2),... ...
{
    ... ...//在函数体中初始化新增成员。
}

例如:为派生类BorderCircle可添加如下3个重载构造函数

定义一个对象并初始化,c++语言会根据形实结合,自动调用对应得构造函数。

BorderCircle obj(5,2);// 调用有参构造函数
BorderCircle obj1;//调用无参构造函数
BorderCircle obj2(obj);//调用拷贝构造函数

3.7 派生类的析构函数

组合派生类c++示意代码:

首先构造4个类,A1,A2,B1,B2。这4个类无实际意义仅用于代码演示。

//类A1
class A1
{
    public:
        int a1;
        A1(int x = 0)//构造函数
        { a1=x; }
};
//类A2
class A2
{
    public:
        int a2;
        A2(int x=0)//构造函数
        { a2 = x ;}
};
//类B1
class B1
{    public:
        int b1;
        B1(int x=0)
        { b1 = x ;}
};
//类B2
class B2
{
    public:
        int b2;
        B2(int x=0)
        { b2 = x;}
};

定义组合派生类C:

class C:public A1, public A2 //继承基类A1和A2
{
    public:
        B1 Bobj1;//类B1的对象成员Bobj1
        B2 Bobj2;//类B2的对象成员Bobj2
        int c;
        //组合派生类的构造函数;初始化基类成员、新增成员、新增非对象成员
        C(int p1=0,int p2=0,int p3=0,int p4=0,int p5=0):A1(p1),A2(p2),Bobj1(p3),Bobj2(p4)
        { c = p5; }};

四、多态性

源程序中相同的程序元素可能会具有不同的语法解释,C++称这些程序元素具有多态性。常见的有:

对于源程序中具有多态性的程序元素,什么时候对他们做出最终明确的语法解释呢?任何下达给计算机的指令,必须在具有明确的语法解释后,才能被计算机执行,否则不能执行。对具有多态性的程序元素作出最终明确的语法解释,这称为多态的实现。

实现多态有2个时间点,分别是在程序编译的时候,或是在程序执行的时候。不同的多态形式具有不同的实现时间点,编译时实现的多态称为编译时多态,执行时实现的多态称为执行时多态

C++中某些关键字是多义词,具有多态性,例如static、const、void、public等等,关键字多态,是由编译器在编译源程序文件时进行解释的,是一种编译时多态。

之前学习的重载函数,如果两个函数的形参个数不同,或者类型不同,那么这两个函数就可以重名,被称为重载函数。编译时,由编译器根据实参的个数和类型的不同,自动调用形参最匹配的那个重载函数,相同的重载函数名,调用时会调用不同的函数,这就是重载函数多态。这也是由编译器在编译时实现的,这是一种编译时多态。所谓实现重载函数多态,就是在编译时,将调用语句中的函数名,转换成某个重载函数的存储地址。将源程序中具有多态的函数名,转换成某个具体的特定的函数储存地址,这种函数名到存储地址的转换,被称为对函数的绑定

本章中重点讲解运算符多态和对象多态

4.1 运算符多态与重载

C++语言中的运算符,具有多态性,例如,2+3和2.0+3.0,分别是整数加法和浮点数加法,它们使用的是同一个运算符,加号+。对计算机中CPU运算器做进一步细分,通常说的运算器是定点运算器,只能进行整数运算,而浮点运算,则是通过浮点运算器,或称为协处理器来完成的。相同的运算符,计算机会根据数据类型来选择执行不同的运算,这就是运算符的多态性,运算符多态是编译器在编译时进行语法解释的,是一种编译时多态。

C++预定义了40多种运算符,但只能对基本数据类的数据进行运算。那么对类这样的自定义数据类型,应该如何运算呢,我们看一个例子,复数类Complex:

class Complex
{
    private:
        double real,image;//实数和虚数部分
    public:
        Complex(double x=0,double y=0){ real=x; image=y; }//构造函数
        Complex(Complex&c){ real = C.real; image=c.image; }//拷贝构造函数
        void Show(){ cout<< real<<"+"<<image<<"i"<<endl; }//显示复数
};

定义对象:c1,c2,c3

Complex c1(1,3),c2(2,4),c3;
c3 = c1+c2; //是否能被执行?,答案是可以的。需要程序员自定义运算规则。

上述列子中,要想实现复数类的加减法,就需要程序员自定义复数类型的运算符的运算规则,因为这是程序员自定义的数据类型。

重新定义C++语言已有运算符的运算规则,使同一运算符作用于不同类型数据时执行不同的运算,这就是运算符重载。正是因为C++语言支持运算符多态,程序员才能重载运算符,实现类运算。

程序员定义重载运算符的常见语法形式为:

函数类型 operator 运算符(形式参数)
{ 函数体 }

这两种方法在实现的功能是相同的,但在定义时,形参和函数体实现部分会有一些差别,另外针对不同运算符,其运算符函数的具体实现方法也有所不同,例如单目运算符和双目运算符、前置和后置运算符等等。

1、复数类的加法运算

代码1:重载运算符为复数类Complex的函数成员

class Complex
{
    private:
        double real,image;//实数和虚数部分
    public:
        Complex(double x=0,double y=0){ real=x; image=y; }//构造函数
        Complex(Complex&c){ real = C.real; image=c.image; }//拷贝构造函数
        void Show(){ cout<< real<<"+"<<image<<"i"<<endl; }//显示复数
        Complex operator+(Complex c)
        {
            Complex result;
            result.real = real + c.real;
            result.image = image + c.image;
            return result;
        }
};

代码2:重载函数为复数类的友元函数

class Complex
{
    private:
        double real,image;//实数和虚数部分
    public:
        Complex(double x=0,double y=0){ real=x; image=y; }//构造函数
        Complex(Complex&c){ real = C.real; image=c.image; }//拷贝构造函数
        void Show(){ cout<< real<<"+"<<image<<"i"<<endl; }//显示复数
    friend Complex operator+(Complex c1,Complex c2);
};

Complex operator+(Complex c1,Complex c2);
{
    Complex result;
    result.real = c1.real + c2.real;
    result.image = c1.image + c2.image;
    return result;
}

从上述例子可以看出,如果不将运算符函数定义成类中的函数成员,那么他就是类外的普通函数,为了让类外的函数能够访问类中的非公有成员,就必须将他们定义成类的友元函数。

在对复数类进行加法的重载后,就可以使用复数类进行加法运算了。例如:

Complex c1(1,3),c2(2,4),c3;
c3 =c1+c2;
c3.show();//显示结果为3+7i

计算机执行“c1+c2”的加法运算,相当于是执行了一次函数调用,其调用形式如下:

2、为复数类重载单目运算符“++”

class Complex
{
    private:
        double real,image;//实数和虚数部分
    public:
        Complex(double x=0,double y=0){ real=x; image=y; }//构造函数
        Complex(Complex&c){ real = c.real; image=c.image; }//拷贝构造函数
        void Show(){ cout<< real<<"+"<<image<<"i"<<endl; }//显示复数
        Complex & operator ++()//实现前置++
        {
            real++;image++;//规则,实部与虚部均加1
            return *this;//返回前置“++”表达式结果:加1后对象的引用
        }

        Complex operator ++(int)//后置++
        {
            Complex temp(*this);
            real++;image++;
            return temp;//返回后置++表达式的结果:加1之前的对象
        }
};

c++语言规定,前置单目运算符重载时没有形参,后置单目运算符重载时需要有一个int形参,这个int型形参没有参数名,这时语法规定,在函数体中并不使用这个形参,其目的是使两个重名函数拥有不同的形式参数,才能实现重载。

#include <iostream>
using namespace std;
#include "complex.cpp"
int main()
{
    Complex c1(1,3),c2(2,4),c3,c4;
    c3 = ++c1;//前置++,返回实部虚部加一后的结果
    c1.Show();//返回结果为2+4i,也就是自增1后的结果
    c3.Show();//返回结果为2+4i,也就是c1自增1后的结果
    //经过前置++运算,此时c1保存的值是2+4i
    c4 = c1++;//后置++,返回实部虚部自增一前的结果
    c1.Show();//显示结果为:3+5i,即自增1后的结果。
    c4.Show();//显示结果是2+4i,即c1自增1之前的结果
    return 0;
}

3、复数类重载关系运算符“==”

class Complex
{
    private:
        double real,image;//实数和虚数部分
    public:
        Complex(double x=0,double y=0){ real=x; image=y; }//构造函数
        Complex(Complex&c){ real = c.real; image=c.image; }//拷贝构造函数
        void Show(){ cout<< real<<"+"<<image<<"i"<<endl; }//显示复数
        bool Complex::operator ==(Complex c)
        {
            return(real==c.real&&image==c.image);
        }
};

此处教学代码有误,待修正

4、赋值运算符”=“

Complex& Complex::operator=(Complex &c)
{
    real = c.real; image = c.image;
    return *this;
}

代码有误。

总结:为方便程序员,C++语言已经默认为所有类重载了赋值运算符,如果某个类在构造函数中动态分配了内存,那么就需要为这个类编写析构函数来释放这些内存,此时,拷贝构造函数和重载运算符”=“的函数都需要程序员自己来重新编写,其目的是进行深拷贝,为新建对象或被赋值对象,动态在分配同样多的内存,

5、运算符重载的语法细则

  1. 除却下面5个运算符,C++语言中的其他运算符都可以重载,5个不能重载的运算符是:
    • 条件运算符”?:“
    • sizeof运算符
    • 成员运算符”.“
    • 指针运算符"*"
    • 和作用域运算符"::"
  2. 重载后,运算符的优先级和结合性不会改变。改变的只是运算规则。
  3. 重载后,运算符的操作数个数不能改变,同时至少要有一个操作数是自定义数据类型。
  4. 重载后,运算符的含义应与原运算符相似,否则会给使用类的程序员造成困扰。

4.2 对象的替换与多态

在类的继承与派生过程中,除构造函数和析构函数之外,派生类将继承所有基类中的数据成员和函数成员,派生类和基类之间存在着这样一种特殊的关系,就是:派生类是一种基类,具有基类的所有功能

面向对象程序设计,利用派生类和基类之间的这种特殊关系,常常将派生类对象当做基类对象来使用,或者用基类来代表派生类,其目的是为提高程序代码的可重用性。

程序代码的可重用性:C++语言对数据一致性的要求比较严格,属于强类型检查的计算机语言,C语言、java语言也属于强类型检查的语言,因为数据不一致,不能重用不同类型的函数去处理不同类型的数据。例如:

void fun(int x){... ...}
fun(5);//正确,形参和实参相匹配
fun(5.0);//错误,形参是int型,而实参是double型
//要想处理double数据,就必须定义个重载函数。
void fun(double x){... ...}
fun(5.0);//编译器根据形实结合,自动调用fun(double x)

那么在类中,情况是怎么样的呢?

class A{... ...};//定义一个类A
void afun(A x){... ...}//处理A类数据的函数afun,x是A类的对象
A aObj;//定义一个A类对象aObj
afun(aObj);//正确,形实一致。
class B{... ...};//定义一个类B
B bObj;//定义一个B类对象bObj
afun(bObj);//错误,bObj的类型与afun中形参的类型不一致

结论:不能重用函数afun的代码来处理B类的对象数据。

但是在面向对象程序设计中,有一种特殊情况是可以的,这就是类B公有继承类A,即B是A的派生类,这时就能重用A类的代码来处理B类的数据

class B:public A
{... ...};

在面向对象程序设计中,重用基类对象的程序代码来处理派生类对象,这是非常普遍的需求。如果派生类对象能够与基类对象一起共用程序代码,它将极大的提高程序开发和维护的效率。面向对象程序设计方法,利用派生类和基类之间存在的特殊关系,提出了对象的替换与多态。

其目的还是提高程序代码的可重用性,本节用一个钟表的例子来讲解。

class Clock
{
    private:
        int hour,minute,second;//时分秒
    public:
        void Set(int h, int m, int s)//设置时间
        {
            hour = h; minute=m; second=s;
        }
        void Show()//显示时间
        {
            cout<<hour<<":"<<":"<<second;
        }
};

派生类:手表类

#include "Clock.h"
class Watch:public Clock
{
    public:
        int band;
    void Show()
    {
        cout<<"Watch";
        Clock::Show();
    }
};

派生类:挂钟类

#include "Clock.h"
class WallClock:public Clock
{
    public:
        int size;
        void Show()
        {
            cout<<"WallClock";
            Clock::Show();
        }
};

派生类:潜水表类

#include "Watch.h"
class DivingWatch:public Watch
{
    int depth;
    void Show()
    {
        cout<<"DivingWatch";
        Clock::Show();
    }
};

类的继承和派生可以任意多级,基类即下面的各级派生类共同组成了一个具有继承关系和共同特性的类的家族,我们称之为类族。类祖中的子类具有共同的祖先,都继承了基类中的成员。

1、liskov替换准则

全球根据地理精度,被换分为24个市区,不同市区的时间与格林威治时间(GMT)它们是存在时差的,例如北京时间比格林威治时间晚8个小时,可以定义一个函数GMT,将格林威治时间转换为北京时间,

void SetGMT(Clock &rObj,int hGMT,int mGMT, int sGMT)
{
    rObj.Set(hGMT+8,mGMT,sGMT);//小时加8,即晚8个小时。
 }
Clock obj;//定义1个基类Clock的对象obj
SetGMT(obj,8,30,0);//将基类对象obj的时间设为8:30分(GMT时间)
obj.Show();//显示基类对象obj中的时间,结果为:16:30:0

问题:可否使用SetGMT去设置Clock的派生类?

为了让基类对象及派生类对象之间可以重用代码,c++语言指定了如下的类型兼容语法规则:

应用类型兼容语法规则有1个前提条件和1个使用限制:

简单来说,就是将派生类对象,当做基类来使用。

Watch obj1;//定义1个派生类Watch的对象obj1
obj1.Set(8,30,0);//将Watch对象obj1的时间设为8点30分(北京时间)
obj1.Show();//显示Watch对象obj1的时间,显示结果为Watch 8:30:0
obj1.band =1;//设置Watch对象obj1的表带类型,假设1代表皮革

//如何将派生类Watch,当做基类使用?
//演示1:将派生类对象obj1赋值给基类对象obj
Clock obj;
obj=obj1;//派生类对象赋值给基类对象
obj.Show();//访问赋值后基类对象obj的成员Show,显示时间:8:30:0
cout<<obj.band;//错误。赋值后基类对象obj不包含派生类对象obj1中的新增成员。

//演示2:通过基类引用rObj访问派生类obj1
Clock &rObj = obj1;
rObj.Show();//访问赋值后基类对象obj的成员Show,显示时间:8:30:0
cout<<rObj.band;//错误。通过引用访问派生类对象obj1,不能访问新增成员。

//通过基类对象指针pObj访问派生类obj1
Clock *pObj = &obj1;
pObj->Show();//访问赋值后基类对象obj的成员Show,显示时间:8:30:0
cout<<pObj->band;//错误。通过指针访问派生类对象obj1,不能访问新增成员。

应用liskov替换准则,可以将派生类对象当做基类来处理,即用基类对象替换派生类对象,将派生类对象当做基类对象处理的好处是,使某些处理基类对象的代码可以被派生类对象重用。

void SetGMT(Clock &rObj,int hGMT,int mGMT, int sGMT)
{
    rObj.Set(hGMT+8,mGMT,sGMT);//小时加8,即晚8个小时。
 }

Clock obj;//定义1个基类Clock的对象obj
SetGMT(obj,8,30,0);//将基类对象obj的时间设为8:30分(GMT时间)
obj.Show();//显示基类对象obj中的时间,结果为:16:30:0
Watch obj1;//定义Watch对象obj1
SetGMT(obj1,8,30,0);//使用SetGMT函数设置派生类的对象。SetGMT的形参是基类的引用。
obj.Show();//能够显示派生类的时间:16:30:0,因为基类只能访问派生类继承的基类成员。

2、对象多态性

显示基类Clock对象的时间

Clock obj; SetGMT(OBJ,8,30,0);
OBJ.Show();//显示基类对象obj的时间,显示结果:16:30:0
void ShowBeijing(Clock &rObj)
{
    rObj.Show();cout<<"(北京时间)";
}
ShowBeijing(obj);//显示:16:30:0(北京时间)

显示派生类Watch对象时间

void ShowBeijing_Watch(Watch &rObj)
{
    rObj.Show();cout<<"(北京时间)";
}
Watch obj1;SetGMT(obj1,8,30,0);
ShowBeijing_Watch(obj);//显示:Watch 16:30:0(北京时间)

如果我们使用基类的方法去显示Watch的时间,显示时间和基类的时间相同,没有Watch标签。

ShowBeijng(obj1);//显示时间为:16:30:0(北京时间)

其原因在liskov替换原则中已经讲过,通过基类对象去访问派生类对象时,只能访问到派生类对象继承的基类成员,实际上通过基类Clock的方法ShowBeijng去访问派生类Watch对象时,调用的是基类的Show方法。

那么如果通过重用函数ShowBeijng的代码,并能区分基类和派生类对象,再分别调用其对应的显示时间函数Show呢?

那么什么是对象的多态性呢?例如米老鼠和唐老鸭是老鼠类和鸭子类的对象,

在面向对象程序设计中,不同的对象,可能有相同的名称的函数成员,例如使用类A、类B分别定义对象aObj和bObj,假设它们都有1个名为Fun的函数成员,但是其算法和功能都各不相同,将调用对象函数成员Fun的操作做如下类比:

对象的多态:

从程序角度,对象多态性就是,调用不同对象的同名函数成员,但执行的函数不同,完成的程序功能不同,导致对象多态的同名函数成员有3种不同的形式:

  1. 不同类之间的同名函数成员:类成员具有类作用域,不同类之间的函数成员可以重名,互不干扰。
  2. 类中的重载函数:类中的函数成员可以重名,只要他们的形参个数不同或类型不同,重载函数成员导致的多态本质上属于重载函数多态。
  3. 派生类中的同名覆盖:派生类中新增成员可以与从基类继承而来的函数成员重名,但它们不是重载函数。

对象的多态,重点是研究同名覆盖,为了扩展和修改基类的功能,类族中的派生类可能会定义新的函数成员,来覆盖同名的基类成员,这样同一个类族中的基类及各个派生类都具有各自的同名函数成员,这些函数成员虽然名字相同,但实现算法有所不同。

应用liskov替换准则,将派生类对象当作基类对象来处理,即用基类对象替换派生类对象,其目的是让某些处理基类对象的代码可以被派生类对象重用,这里“某些”代码的含义是:这些代码在通过基类的引用或对象指针访问派生类成员时,只能访问其基类成员。例如前文中的北京时间设置函数:

void SetGMT(Clock &rObj,int hGMT,int mGMT, int sGMT)
{
    rObj.Set(hGMT+8,mGMT,sGMT);//不管是基类或派生类对象,都调用基类成员Set
}

还有另外一些代码,这些代码在通过基类的引用或对象指针访问同一类族的对象时,需要根据实际的引用或指向的对象类型,自动调用该类同名函数成员中的新增成员(而不是基类成员),例如:

void ShowBeijing(Clock &rObj)
{
    rObj.Show();cout<<"(北京时间)";//希望能够区分基类和派生类对象,自动调用对应的成员
}

应用对象多态性,相当于是用基类来代表派生类,通过基类引用或对象指针调用派生类对象的函数成员,应能够根据实际引用或指向的对象类型,自动调用该类同名函数成员中的新增成员。C++语言使用虚函数的语法形式来实现类族中对象的多态性

实现对象的多态性:

虚函数的声明与调用:

定义基类:A

class A
{
    public:
        virtual void fun1();//声明fun1为虚函数
        void fun2();//fun2为普通函数,即非虚函数
};
void A::fun1(){ cout<<"Base class A: virtual fun1() called."<<endl;}
void A::fun2(){ cout<<"Base class A: non-virtual fun2() called."<<endl; }

定义派生类:B

class B:public A
{
    public:
        virtual void fun1();//重新基类的虚函数成员fun1
        void fun2();//重新基类的非虚函数成员fun2
};
void B::fun1(){ cout<<"Derived class B:virtual fun1() called."<<endl; }
void B::fun2(){ cout<<"Derived class B:non-virtual fun2() called."<<endl; }

声明虚函数的语法细则:

调用实例:

A aobj;//定义1个基类对象aobj
B bobj;//定义1个派生类对象bobj

aobj.fun1();//调用结果:调用了基类对象的虚函数成员fun1
aobj.fun2();//调用结果:调用了基类对象的虚函数成员fun2
bobj.fun1();//调用结果:调用了派生类对象bobj的新增函数成员fun1
bobj.fun2();//调用结果:调用了派生类对象bobj的新增函数成员fun2
//结论1:通过对象名访问派生类对象的成员,将访问器新增成员(同名覆盖)

调用虚函数:通过基类引用分别调用虚函数成员和非虚函数成员,对比结果

A &raobj = aobj;//定义1个基类引用raobj,引用基类对象aobj
raobj.fun1();//调用结果:调用了基类对象aobj的虚函数成员fun1
raobj.fun2();//调用结果:调用了基类对象aobj的非虚函数成员fun2
A &raobj = bobj;//定义1个基类引用raobj,引用派生类对象bobj
rbobj.fun1();//调用结果:调用了派生类对象bobj的新增函数成员fun1
rbobj.fun2();//调用结果:调用了派生类对象bobj的基类函数成员fun2
//结论2:通过基类引用访问派生类对象的虚函数成员将访问其新增成员(多态)
//通过基类引用范围派生类对象的非虚函数成员将访问其基类成员

通过对象指针访问:

A *paobj = &aobj;//定义1个基类对象指针paobj,指向基类对象aobj
paobj->fun1();//调用结果:调用了基类对象aobj的虚函数成员fun1
p->fun2();//调用结果:调用了基类对象aobj的非虚函数成员fun2
A *paobj = &bobj;//定义1个基类对象指针paobj,指向派生类对象bobj
rbobj.fun1();//调用结果:调用了派生类对象bobj的新增函数成员fun1
rbobj.fun2();//调用结果:调用了派生类对象bobj的基类函数成员fun2
//结论2:通过基类对象指针访问派生类对象的虚函数成员将访问其新增成员(多态)
//通过基类对象指针访问派生类对象的非虚函数成员将访问其基类成员

实现基类对象与派生类对象之间的多态性要满足以下3个条件:

  1. 在基类中声明虚函数成员
  2. 派生类需公有继承基类,并重写虚函数成员(属于新增成员)
  3. 通过基类的引用或对象指针调用虚函数成员

只有满足这3个条件,基类对象和派生类对象才会分别调用各自的虚函数成员,呈现出多态性。

为了让某天类族共用程序代码:

3、抽象类

面向对象程序设计方法,来设计解决某个实际问题的计算机程序,先从实际问题中提取出一个个具体的对象,并将具有共性的对象划分为类,可以继续将多个不同类中的共性抽象出来,形成基类,编码时再从基类进行派生,还原出各个不同的类,抽象出基类可以凝练类代码,有效减小程序中的重复代码,从对象抽象出类,从类继续抽象出基类,这是一个自底向上,逐步抽象的过程,越往上,类就越宽泛,越抽象.

定义一个圆形类:Circle

class Circle
{
public:
    double r;
    double CArea();
    double CLen();
};
//类实现部分,代码省略

定义一个矩形类:Rectangle

class Rectangle
{
 public:
    double w,h;
    double RArea();
    double RLen();
};
//类实现部分,代码省略

在圆和矩形的基础上进行抽象,定义一个形状:Shape

class Shape
{
    public:
        double Area();
        double Len();
};

形状类有2个函数成员,仔细分析会发现这两个函数成员无法定义,因为形状是一个纯抽象的概念,无法计算其面积和周长,形状类就是一种抽象类。其中有2个只声明未定义的函数成员,他们就是一种纯虚函数。这一节将结合形状类Shape,来具体介绍纯虚函数,抽象类已经它们在面向对象程序设计中的作用。

纯虚函数:类定义中,只声明,未定义的函数被称为纯虚函数。纯虚函数的声明语法形式是,用关键字virtual声明,并在其声明部分加上等于0(”=0“),例如:

class Shape
{
    public:
        virtual double Area()=0;
        virtual double Len()=0;
};

纯虚函数是一种虚函数,据有虚函数的特性,最重要的就是纯虚函数成员在调用时具有多态性。

抽象类:含有纯虚成员的类就是抽象类,例如形状类Shape,抽象类有以下特点:

  1. 抽象类不能实例化

    不能用抽象类定义对象(即不能实例化),因为抽象类中含有未定义的纯虚函数,其类型定义还不完整。但可以定义抽象类引用、对象指针,所定义的引用、对象指针可以引用或指向其他派生类的实例化对象。

  2. 抽象类可以作为基类定义派生类

    • 抽象类可以作为基类定义派生类,派生类继承抽象类中除构造函数、析构函数之外的所有成员,包括纯虚函数成员
    • 纯虚函数成员只声明了函数原型,没有定义函数体代码,因此派生类继承纯虚函数成员时,只是继承了其函数原型,即函数接口,派生类需要为纯虚函数成员编写函数体代码,称为实现纯虚函数成员。
    • 派生类如果实现了所有的纯虚函数成员,那么它就变成了一个普通的类,可以实例化。

例如:利用类Shape定义派生类Circle和派生类Rectangle

//派生类Circle
class Circle:public Shape
{
    public:
        double r;//新增数据成员,半径r
        Circle(double x=0)//构造函数
        { r = x; }
        double Area()//同名覆盖,实现纯虚函数Area
        { return(3.14*r*r);}
        double Len()//同名覆盖,实现纯虚函数Len
        { return(3.14*2*r);}
};

//派生类Rectangle
class Rectangle:public shape
{
    public:
        double a,b;//新增数据成员,长宽:a,b
        Rectangle(double x=o,double y=0)//构造函数
        { a= x; b=y; }
        double Area()//同名覆盖,实现纯虚函数Area
        { return(a*b); }
        double Len()//同名覆盖,实现纯虚函数Len
        { return((a+b)*2);}
};

以上两个派生类,都实现了纯虚函数,因此可以实例化。

抽象类的应用:

  1. 统一类族接口

    通常,派生类继承基类是为了重用基类代码,如果基类是抽象类,其中的纯虚函数成员并没有定义函数体代码,只是声明了函数原型,基类声明纯虚函数成员的目的不是为了重用其代码,而是为了统一类族对外的接口。在基类中声明纯虚函数成员,各派生类按照各自的功能要求实现这些纯虚函数,这样类族中所有的派生类都具有相同的接口。统一类族可以方便类族的使用,程序员只需呀记忆一套函数名即可。

    Circle cObj;
    Rectangle rObj;
    cObj.Area(); cObj.Len(); //求圆的面积和周长
    rObj.Area(); rObj.Len();//求矩形的面积和周长
    //在求圆和矩形的周长和面积时,调用的是一样的函数,这就是接口的统一。
    
  2. 重用代码

    抽象类定义的纯虚函数具有虚函数的特性,调用时具有多态性,在基类中声明纯虚函数成员的另一个目的是利用虚函数调用时的多态性,让类族中的所有派生类对象可以重用相同的代码。

    //为Shape定义一个显示信息的函数ShapeInfo
    void ShapeInfo(Shape *pObj)//显示面积和周长信息
    { cout<< pObj->Area()<<","<<PObj->Len()<<endl; }
    
     //调用主函数代码
     int main()
     {
         Circle cObj(10);//定义1个圆形类对象cObj
         Rectangle rObj(5,10);//定义1个矩形类对象rObj
         ShapeInfo(&cObj);//显示圆形对象cObj的周长和面积信息
         ShapeInfo(&rObj);//显示矩形对象rObj的周长和面积信息
         return 0;
     }
    

五、关于多继承的讨论

派生类可以从多个基类继承,这就是多继承,多继承派生类存在比较复杂的成员重名问题,其表现形式有3种:

第一种已经在第4节中详细讲解,本小节重点讲解后面两种。

5.1多个基类之间的重名

基类A1:

class A1
{
    public:
        int a1;
        int a;
        void fun()
        { cout<<a1<<","<<a<<endl; }
};

基类A2:

class A2
{
    public:
        int a2;
        int a;
        void fun()
        { cout<<a2<<","<<a<<endl; }
};

双继承派生类B,继承A1:a1,a,fun;继承A2:a2,a,fun

class B:public A1,public A2
{
    public:
        //...不新增任何成员,因此派生B只包含从基类A1、A2继承的基类成员
};

使用多继承派生类B定义对象:

B bObj;//定义派生类对象bObj
cin>>bObj.a1>>bObj.a2;//访问不重名的基类成员,直接使用成员名
cin>>bObj.A1::a>>bObj.A2::a;//访问重名的基类成员,需在成员名前加“基类名::”
bObj.A1::fun();//调用从A1类继承来的基类函数成员fun,显示a1和A1::a的值
bObj.A2::fun();//调用从A2类继承来的基类函数成员fun,显示a2和A2::a的值

5.2 重复继承

基类A:

class A
{
    public:
        int a;
        void fun(){ cout<<a<<endl; }
};

派生类A1:

class A1:public A //公有继承基类A
{
    public:
    //不新增任何新成员
    //派生类A1继承了1份基类A的成员
};

派生类A2:

class A2:public A //公有继承基类A
{
    public:
    //不新增任何新成员
    //派生类A2继承了1份基类A的成员
};

二级派生类B:

class A:public A1,public A2 //公有继承基类A
{
    public:
    //不新增任何新成员
    //派生类B同时继承类A1,A2,继承后将包含了2份完全相同的基类A的成员
}

多分基类被重复继承,不仅造成内存的浪费,也会造成使用上的混乱,c++语言引入了虚基类的概念,对重复继承时,不希望需保存多份拷贝的基类,在第一层继承时,就使用virtual关键字,将其声明虚基类。例如,上述代码中的,A1、和A2的代码可以修改如下:

//A1代码
class A1:virtual public A //继承虚基类A
{
    public:
        //不新增任何新成员
        //派生类A1继承了1份基类A的成员
};

//A2代码
class A2:virtual public A //继承虚基类A
{
    public:
        //不新增任何新成员
        //派生类A2继承了1份基类A的成员
};

这样class B在同时继承A1、A2时只会拷贝一份基类A的成员。

5.3 关于多继承的讨论

类的继承与派生主要有2个作用:

  1. 重用类代码:派生类继承基类,就是重用基类的代码,试用其功能,从而提高程序的开发效率
  2. 统一类族接口:抽象类中包含纯虚函数成员,纯虚函数只声明函数原型,没有定义代码,没有实现任何功能。如果基类是抽象类,在基类中声明纯虚函数成员,其派生类按照各自的功能要求实现这些纯虚函数。这样以该类为根的类族中的所有派生类都将具有相同的对外接口,更便于类族的使用。

多继承会造成重复继承,为解决重复继承中的多拷贝问题导致的内存浪费,c++语言又引入了虚基类的概念。虚基类又会引出更复杂的语法,例如虚基类只定义了一个带形参的构造函数,那么整个继承关系中的所有直接或间接继承虚基类的派生类,都必须在构造函数的初始化列表中对虚基类的成员进行初始化,c++因为使用多继承,引发了一系列非常复杂的语法形式,并且难以掌握,后来的其他面向对象设计语言都放弃了多继承,例如:java和c#语言,只允许单继承,派生类只能继承一个基类,即只能重用一个基类代码,但是为了统一类族接口,它们引入了一个新的概念:接口。接口类似于抽象类,但接口只包含纯虚函数,不能包含数据成员,派生类可以继承多个接口,但不会造成重复继承中的多拷贝问题,引入接口,取消多继承,这有效简化了java语言在继承和派生方面的语法学习时也便于掌握