第20章:Derived Classes

20.1: introduction

对于类继承来说,可以分为实现继承(implementation inheritance)和接口继承(interface inheritance)这两种

(1)接口继承:派生类与基类具有相同的接口,在设计良好的类层次中,public派生类的对象可以用在任何需要基类对象的地方;
(2)实现继承:基类为其派生类提供成员函数或数据成员以简化派生类的实现。

20.2 Derived classes

1:基类对象的指针能够指向派生类,基类对象能够引用派生类,反过来则不可以。如下面代码所示:

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

B x1;
A* p1=&x1;  //okay! 基类的指针能够指向派生类;
A& r1=x1;   //okay! 基类能够引用派生类对象;

A x2;
B* p2=&x2;  //error! 派生类的指针不能指向基类;
B& r2=x2;   //error! 派生类不能引用基类的对象;

其实这很好理解,因为派生类包括了基类,所以基类的指针就能够指向派生类,可以认为基类的指针指向了派生类中的基类部分,但是基类不包括派生类,所以不能用派生类的指针去指向基类。引用也是如此。

2:一个类在被作为基类之前,一定要先被定义,只有声明是不可以的。比如:

class A;  //仅仅声明类A,没有定义;
class B: public A{   //error!  因为A在作为基类之前只有声明没有定义。
...
};

(3):构造函数和复制控制不能继承,每个类需要定义自己的构造函数和复制控制。派生类的构造函数和复制控制不仅需要初始化或赋值自己的数据成员,也要初始化或赋值其直接基类的成员。派生类整个构造或复制过程中,是先初始化或赋值基类的数据成员然后再初始化或赋值自己类的数据成员。

(4):无论是派生类还是基类都应该定义自己的析构函数,并在基类中声明其为虚函数。与构造函数和复制控制不同的是,派生类中的析构函数并不负责撤销基类对象的成员。编译器总是显示调用派生类对象基类部分的析构函数,每个析构函数只负责清除自己的成员。在整个衍生类的析构过程中,是先调用衍生类的析构函数然后再调用其直接基类的析构函数。

(5):从(3)和(4),我们不难发现构造和析构过程中,对象的类型一直都在变化,因此最好不要在构造函数和析构函数中调用虚函数。

20.3:class hierarchies

1:如果我们在基类中声明一个函数为虚函数,我们可以在其派生类中定义一个有着相同的名字,相同的参数类型,相同的返回类型(除非返回的是各自所在类的指针或引用)的函数,去覆盖(override)基类中的那个函数。这有一点好处就是当我们用基类的指针(或引用)去调用这个虚函数时,它不一定调用的就是基类的虚函数,它调用的是指针指向的实际类的虚函数。例子如下:

class A{
public:
    virtual int f(int a) { return a; } //virtual 声明这个函数为虚函数;
    int g(int a) { return a;} //注意这不是虚函数;
...
};

class B:public A{
public:
    int f(int a) override { return a*a; } //重写类A中的虚函数f().
    int g(int a) { return a*a; }
...
};

A* p=new B(); 
p->f(3); //调用的是B中的的f()函数,虽然指针p是用A*声明的,但p实际是指向类B的。
p->g(3); //调用的是A::g(int);虽然p实际指向的是类B不是基类A,但由于类B是包括基类A的,所以p可以调用基类成员g(int)。
delete p;

指针p调用f函数时,由于指针p是用A*声明的,因此编译器会在基类类A中查找名字为f的函数,找到了名字再进行函数匹配,最终找到了f(int)这个函数,发现其是虚函数,那么编译器就会生成代码以确定根据指针指向的实际类型(在运行时间确定)来调用哪个版本的函数f(int),在运行时间发现指针p实际指向的派生类B,因此会调用派生类B中的B::f(int)函数。其实这就是所谓的接口继承,类A和类B有着共同的接口f(int)函数。当我们用基类的指针(或引用)来调用虚函数时,它调用的是其指向的实际类型中的虚函数,这也称之为多态性。

用指针p调用g(int)函数时情况有点不一样,编译器在基类A中找到了函数g(int),由于g(int)不是虚函数,只是普通的成员函数,因此编译器在编译阶段直接生成代码调用A::g(int)函数。

(2):在类B中对函数f()声明的最后,有一个overirde 关键字,其意思是指现在声明的这个 函数是打算重写(覆盖)基类中对应的虚函数。如果发现在基类中没有对应的虚函数或这两个函数的返回类型或参数类型不一致,编译器将会报错。如下面代码所示:

struct A{
    void f(int) const;
    virtual void g(double);
    virtual int f(int);
}

struct B:public A{
    void f(int) const override; //error!因为在基类类A中对应的f(int)函数并不是虚函数;
    void g() override; //error! 因为在基类类A中g()有着double参数。
    int f(int) override; //okay!在基类A中有一个对应的虚函数。
};

(3):除了有override这个关键字外,还有一个是final关键字,和overide关键字一样,其也放在函数声明的最后,去表明正在声明的函数不打算再次被重写(覆盖)。代码例子如下:

struct A{
    virtual int f(int);
};

struct B:public A{
    int f(int) override final; //重写了基类A中的虚函数f,但不打算再次被重写
};

struct C:public B{
    int f(int) override; //error! 本意是想重写其基类B中的虚函数f(int),但无奈B类已经声明f(int)为final。
};  

(4):在基类和派生类中使用同一名字的函数时,在派生类作用域中派生类成员将屏蔽基类成员,即使函数原型不同,基类成员成员也会被屏蔽;如下面代码例子所示:

struct A{
    int f();
    int g();
};
struct B:public A{
    int f(int);
};

A a; B b;
a.f(); //calls A::f();
b.f(10); //calls B::f(int);
b.f();// error! A::f()is hidden;
b.A::f(); //okay!
b.g(); //okay! calls A::g();

为什么b.f()调用会出错呢?由于b是类B的对象,所以当b调用函数f()时,其会首先在类B寻找是否有着相同函数名f的函数,发现在类B中存在着函数名为f的函数,这时候编译器就不会继续查找了,这时候开始函数匹配,最后发现对象b调用的f()函数与B::f(int)函数不匹配,编译器报错。

那为什么b.g()正确呢?因为编译器在类B中没有找到函数名为g的函数,因此这时候编译器会在类B的基类类A中寻找,然后发现在类A中存在着g()函数,因此这时候b.g()实际调用的是A::g();

通过(1)和(4),我们知道编译器确定函数调用有如下四个步骤:

1. 首先确定函数调用的对象,引用或指针的静态类型;
2. 在该类中查找函数,如果找不到,就在直接基类中查找,如此循环着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或相关基类中找到名字,则调用时错误的;
3. 一旦找到了该名字,就进行常规类型检查,查看给定找到的定义,该函数调用是否合法;
4. 假定函数调用合法,编译器就生成代码。如果函数是虚函数并且是通过指针或引用调用,则编译器生成代码以确定根据对象的动态类型确定运行哪个函数的版本,否则,编译器生成代码直接调用函数。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值