0%

C++面向对象笔记:继承与多态

0.概述

前文分析了C++类内成员的关系,本文讨论类和类之间的关系。
考虑用C++对现实世界的交通工具进行描述。

  • 汽车可能包含各种类型,小汽车,公交车,但他们能抽象出四个轮子,烧油这些基本属性
  • 飞机也有各种类型,但也能抽象出机翼,机身等基本属性
  • 轮船…

如果自顶向下设计,如何设计这些对象的类?

  • 提炼这些交通工具的共有属性,如材质,耗油量,价格,设计成一个交通工具基础类;然后设计一些操作方法,比如制造,启动,停止。
  • 分别设计汽车、飞机、轮船等更具体的类的属性,比如轮子、排水量等,注意,他们也包含基础类的材质,耗油量,价格等基本属性;然后也设计一些方法,比如制造汽车、开汽车和造飞机、开飞机等
  • 然后再设计更细节的类,作为汽车、飞机、轮船类的细化,比如A品牌的汽车,B品牌汽车,作为两个具体类。

仔细考虑以上步骤,有以下问题:

  • 这些类的属性(成员变量)是相互独立的吗?
  • 这些类的方法(成员函数)是相互独立的吗?

C++用类的“继承”描述层层细化的类及其成员变量的关系,用“多态”描述各层方法的实现关系。

类的继承

继承关系的概念

继承:在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么就可以把A作为一个基类(也叫父类),而把B作为基类的一个派生类(也叫子类)。

  • 派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数
  • 派生类一经定义后,可以独立使用,不依赖于基类。
  • 派生类拥有基类的全部成员函数和成员变量,不论是private、 protected、 public。但是派生类的成员函数不能访问基类中的private成员

一个管理学生的类继承:
image-20221208165130260
派生类语法:

class 派生类名: public 基类名
{
};

学生类的派生:

class CStudent {
    private:
    string sName;
    int nAge;
    public:
    bool IsThreeGood() { };
    void SetName( const string & name )
    { sName = name; }
        //......
};

class CUndergraduateStudent: public CStudent {
    private:
    int nDepartment;
    public:
    bool IsThreeGood() { ...... }; //覆盖
    bool CanBaoYan() { .... };
}; // 派生类的写法是:类名: public 基类名

类继承的存储空间

在类与对象一文讲过,类对象的存储空间,实际就是成员变量的空间,成员函数不在对象空间内(虚函数包含一个虚函数表指针)。那么基类和派生类的对象空间有什么相关性?
派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。 在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。
一个示例:

class CBase
{
    int v1, v2;
};
class CDerived:public CBase
{
    int v3;
};

image-20221208165140659

类继承的覆盖

类内的同名非同参的函数叫函数重载,那么基类与派生类的同名函数呢?
派生类可以定义和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,默认访问派生类中定义的成员,基类的成员函数或变量被“覆盖”掉了。如果要在派生类中访问基类定义的同名成员时,要使用作用域符号::
一个例子:

class base {    //基类
    int j;  //默认private
    public:
    int i;
    void func();
};
class derived : public base{    //派生类
    public:
    int i;  //覆盖基类i
    void access();
    void func(); //覆盖基类func()
};

void derived::access() { //访问派生类成员
    j = 5; //error
    i = 5; //引用的是派生类的 i
    base::i = 5; //引用的是基类的 i
    func(); //派生类的
    base::func(); //基类的
}

调用函数:

derived obj;
obj.i = 1;  //访问派生类成员i
obj.base::i = 1; //访问基类成员i

内存分布:
image-20221208165150851
以上只是示例,一般来说,基类和派生类不定义同名成员变量,但经常有同名成员函数,所以覆盖通常用于成员函数覆盖。

类继承的成员访问控制

  • 基类的private成员:可以被下列函数访问
    – 基类的成员函数
    – 基类的友元函数
  • 基类的public成员:可以被下列函数访问
    – 基类的成员函数
    – 基类的友元函数
    – 派生类的成员函数
    – 派生类的友元函数
    – 其他的函数
  • 基类的protected成员:可以被下列函数访问
    – 基类的成员函数
    – 基类的友元函数
    – 派生类的成员函数可以访问当前对象的基类的保护成员

一个示例:

class Father {
    private: int nPrivate; //私有成员
    public: int nPublic; //公有成员
    protected: int nProtected; // 保护成员
};
class Son :public Father{
    void AccessFather () {
        nPublic = 1; // ok;
        nPrivate = 1; // wrong
        nProtected = 1; // OK,访问从基类继承的protected成员
        Son f;
        f.nProtected = 1; //wrong , f不是当前对象
    }
};

int main()
{
    Father f;
    Son s;
    f.nPublic = 1; // Ok
    s.nPublic = 1; // Ok
    f.nProtected = 1; // error
    f.nPrivate = 1; // error
    s.nProtected = 1; //error
    s.nPrivate = 1; // error
    return 0;
}

类继承的构造函数

类似于嵌套类(封闭类)的构造函数,使用初始化列表来实现层层构造,基类和派生类只初始化他们能访问的成员

class Bug {
private :
    int nLegs; int nColor;
    public:
    int nType;
    Bug ( int legs, int color);
    void PrintBug (){ };
};

class FlyBug: public Bug // FlyBug是Bug的派生类
{
    int nWings;
    public:
    FlyBug( int legs,int color, int wings);
};

Bug::Bug( int legs, int color) //Bug类的构造函数
{
    nLegs = legs;
    nColor = color;
}

//错误的FlyBug构造函数!
FlyBug::FlyBug ( int legs,int color, int wings)
{
    nLegs = legs; // 不能访问
    nColor = color; // 不能访问
    nType = 1; // ok
    nWings = wings;
}

//正确的FlyBug构造函数:使用初始化列表
FlyBug::FlyBug ( int legs, int color, int wings):Bug( legs, color)
{
    nWings = wings;
}

int main() {
    FlyBug fb ( 2,3,4);
    fb.PrintBug();
    fb.nType = 1;
    fb.nLegs = 2 ; // error. nLegs is private
    return 0;
}

类继承的构造析构时序

在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
调用基类构造函数的两种方式:

  • 显式方式:在派生类的构造函数中,为基类的构造函数提供参数.

    derived::derived(arg_derived-list):base(arg_base-list)

  • 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数

析构函数执行时序:
派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。
一个例子:

class Base {
    public:
    int n;
    Base(int i):n(i)
    { cout << "Base " << n << " constructed" << endl;}
    ~Base()
    { cout << "Base " << n << " destructed" << endl; }
};
    
class Derived:public Base {
    public:
    Derived(int i):Base(i)
    { cout << "Derived constructed" << endl; }
    ~Derived()
    { cout << "Derived destructed" << endl;}
};
int main() { Derived Obj(3); return 0; }

输出结果:

Base 3 constructed
Derived constructed
Derived destructed
Base 3 destructed

##封闭派生类的构造函数
封闭类的构造用初始化列表,派生类也用初始化列表,那么封闭派生类呢?
还是初始化列表

class Bug {
    private :
    int nLegs; int nColor;
    public:
    int nType;
    Bug ( int legs, int color);
    void PrintBug (){ };
};

class Skill {
    public:
    Skill(int n) { }
};
class FlyBug: public Bug {
    int nWings;
    Skill sk1, sk2;
    public:
    FlyBug( int legs, int color, int wings);
};
FlyBug::FlyBug( int legs, int color, int wings):
    Bug(legs,color),sk1(5),sk2(color) ,nWings(wings) { //初始化列表,不能访问的通通交给下层构造函数
}

封闭派生类的构造析构时序

在创建派生类的对象时:

  1. 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员
  2. 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象
  3. 最后执行派生类自己的构造函数
    在派生类对象消亡时:
  4. 先执行派生类自己的析构函数
  5. 再依次执行各成员对象类的析构函数
  6. 最后执行基类的析构函数
    析构函数的调用顺序与构造函数的调用顺序相反

    类的复合

    在数学上,两个集合有无关、相交和包含的关系。对于多个类来说,也应该有以上三种关系。无关类=两个成员不相关的类;继承类=类成员间有继承关系的类;那么相交的类呢?

    复合关系的概念

    C++用“复合”表示类的相交关系。

1)继承:“是”的关系
基类是A, B是基类A的派生类,逻辑上要求:“一个B对象也是一个A对象”
2)复合:“有”的关系
类C中“有” 成员变量k,k是类D的对象,则C和D是复合关系,逻辑上要求:“D对象是C对象的固有属性或组成部分

下面比较一下继承和复合在具体设计的实例:
继承关系顶层设计例子:

  • 写了一个 CMan 类代表男人
  • 后来又发现需要一个CWoman类来代表女人
  • CWoman类和CMan类有共同之处,让CWoman类从CMan类派生而来,是否合适?
  • 错!从一开始就应该设计CHuman类,代表“人” ,然后CMan和CWoman都从
    CHuman派生

继承逻辑关系:
image-20221208165210195

复合关系顶层设计例子:

  • 几何形体程序中,需要写“点”类,也需要写“圆”类
  • 每个圆都有圆心,那么点类应该从圆类派生出来吗?
  • 错!”点“不仅在圆内有,在其他图形也有,不是圆独有,非继承关系
  • 实际上,圆和点是复合关系,每一个“圆”对象里都包含()一个“点”对象
  • 逻辑上,复合关系就是,我的一部分可以看成是你的,但是我的全部东西不都属于你

复合关系的类通常用友元实现:

class CPoint
{
    double x,y;
    friend class CCircle;
    //便于Ccirle类操作其圆心
};

class CCircle
{
    double r;
    CPoint center;
};

复合关系的典型示例

如果要写一个小区养狗管理程序,需要写一个“业主”类,还需要写一个“狗” 类
狗是归宿于业主的,一个业主可以有多条狗,狗也可以随时脱离业主
考虑以下设计方法:
设计人和狗两个类,相互包含对方类

class CDog;
class CMaster
{
    CDog dogs[10];
};
class CDog
{
    CMaster m;
};

这样有循环定义错误!且逻辑上,狗和人并非相互包含关系
这种关系上相互相关,对象本身又完全独立的情况,用对象指针表示

class CMaster; //CMaster必须提前声明,不能先写CMaster类后写Cdog类
class CDog {
    CMaster * pm;
};
class CMaster {
    CDog * dogs[10];
};

逻辑关系:
image-20221208165220338

继承的不足

继承方式的访问限制

基类和派生类是包含的关系,那么基类对象和派生类对象是什么关系?
对于类的public派生方式:

class base { };
class derived : public base { };
base b;
derived d;

1)派生类的对象可以赋值给基类对象
b = d;
2)派生类对象可以初始化基类引用
base & br = d;
3)派生类对象的地址可以赋值给基类指针
base * pb = & d;
如果派生方式是 private或protected,则上述三条不可行

对于类的protected和private派生方式:

class base {};
class derived : protected base {};
base b;
derived d;

• protected继承时,基类的public成员和protected成员成为派生类的protected成员。
• private继承时,基类的public成员成为派生类的private成员,基类的protected成员成为派生类的不可访问成员。
• protected和private继承不是“是”的关系

派生类的对象指针转换

public派生的情况下,派生类对象的指针可以直接赋值给基类指针

Base * ptrBase = &objDerived;
//ptrBase指向的是一个Derived类的对象;

*ptrBase可以看作一个Base类的对象,访问它的public成员直接通过ptrBase即可,但不能通过ptrBase访问objDerived对象中属于Derived类而不属于Base类的成员
过强制指针类型转换,可以把ptrBase转换成Derived类的指针

Base * ptrBase = &objDerived;
Derived *ptrDerived = (Derived * ) ptrBase;

程序员要保证ptrBase指向的是一个Derived类的对象,否则很容易会错

派生类的指针赋值给基类后,基类指针也不能访问派生类的特有成员

#include <iostream>
using namespace std;
class Base {
    protected:
    int n;
    public:
    Base(int i):n(i){cout << "Base " << n <<" constructed" << endl; }
    ~Base() {cout << "Base " << n <<" destructed" << endl;}
    void Print() { cout << "Base:n=" << n << endl;}
};

class Derived:public Base {
    public:
    int v;
    Derived(int i):Base(i),v(2 * i) {
    cout << "Derived constructed" << endl;
}

~Derived() {
    cout << "Derived destructed" << endl;
}

void Func() { } ;
    void Print() {
        cout << "Derived:v=" << v << endl;
        cout << "Derived:n=" << n << endl;
    }
};

int main() {
    Base objBase(5);
    Derived objDerived(3);
    Base * pBase = & objDerived ;
    //pBase->Func(); //err;Base类没有Func()成员函数
    //pBase->v = 5; //err; Base类没有v成员变量
    pBase->Print();
    //Derived * pDerived = & objBase; //error
    Derived * pDerived = (Derived *)(& objBase);
    pDerived->Print(); //慎用,可能出现不可预期的错误
    pDerived->v = 128; //往别人的空间里写入数据,会有问题
    objDerived.Print();
    return 0;
}

输出:

Base 5 constructed
Base 3 constructed
Derived constructed
Base:n=3
Derived:v=1245104 //pDerived->n 位于别人的空间里
Derived:n=5
Derived:v=6
Derived:n=3
Derived destructed
Base 3 destructed
Base 5 destructed

从逻辑上来说,派生类指针既然能被赋值给基类指针,那么通过基类指针,应该能调用派生类的成员函数,获取派生类的成员变量。在下一章,继承类的多态将实现这个目的。

多级继承

类A派生类B,类B派生类C,类C派生类D……
– 类A是类B的直接基类
– 类B是类C的直接基类,类A是类C的间接基类
– 类C是类D的直接基类,类A、 B是类D的间接基类
在声明派生类时, 只需要列出它的直接基类
– 派生类沿着类的层次自动向上继承它的间接基类
– 派生类的成员包括
• 派生类自己定义的成员
• 直接基类中的所有成员
• 所有间接基类的全部成员

多态:在继承上更进一步

前面派生类的对象指针转换一节,基类指针强转后也不能访问派生类私有对象。考虑一下本文开始讲的交通工具顶层设计思路,在顶层设计时就要设计类的成员函数,在派生类也要设计成员函数,这些函数会有重合的情况吗?如果有重合,基类指针也不能访问派生类成员,这样基类和派生类不就失去联系了吗?多级继承这种情况不是更加严重?
为了解决这种问题,本节引入继承类的“多态”
多态能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少

虚函数与多态

在类的定义中,前面有 virtual 关键字的成员函数就是虚函数

class base {
    virtual int get() ;
};
int base::get(){ }

virtual关键字只用在类定义里的函数声明中使用,定义函数体时不用。
使用虚函数,来实现“多态”效果。多态有通过指针和引用两种表现形式:

  • 能通过基类的指针调用派生类虚函数,访问其特有成员变量

派生类的指针可以赋给基类指针
通过基类指针调用基类和派生类中的同名虚函数时:
(1)若该指针指向一个基类的对象,那么被调用是
基类的虚函数;
(2)若该指针指向一个派生类的对象,那么被调用
的是派生类的虚函数

class CBase {
public:
    virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
    virtual void SomeVirtualFunction() { }
};
int main() {
    CDerived ODerived;
    CBase * p = & ODerived;
    p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
    return 0;
} 
  • 能通过基类的引用调用派生类虚函数

派生类的对象可以赋给基类引用
通过基类引用调用基类和派生类中的同名虚函数时:
(1)若该引用引用的是一个基类的对象,那么被调
用是基类的虚函数;
(2)若该引用引用的是一个派生类的对象,那么被
调用的是派生类的虚函数。

class CBase {
public:
    virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
    virtual void SomeVirtualFunction() { }
};
int main() {
    CDerived ODerived;
    CBase & r = ODerived;
    r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
    return 0;
} 

是不是所有成员函数加virtual都是多态?不是!

  • 在非构造或析构函数的成员函数中调用虚函数,是多态。在运行时才确定到底调用哪一层派生类函数
  • 在构造函数和析构函数中调用虚函数,不是多态。调用的函数是当前类的函数,编译时即确定

多层继承实现多态,每一层都要加virtual关键字吗?

  • 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数

多态与对象指针

一个变量有两方面属性:类型、值
那么多态把derived类的地址值,赋值给base类的指针,访问对象成员时是什么效果?
以下例子的this指针指向什么?

class Base {
public:
    void fun1() { this->fun2(); } //this是基类指针, fun2是虚函数,所以是多态
    virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derived:public Base {
public:
    virtual void fun2() { cout << "Derived:fun2()" << endl; }
};
int main() {
    Derived d;
    Base * pBase = & d;
    pBase->fun1();
    return 0;
}

pBase被Derived对象的地址赋值后,其值为Derived对象的地址,但类型还是Base的指针(多态指针赋值不会强转)。pBase->fun1()会先在Base类访问其fun1(),传入this指针(指向fun2),而this->fun2()会调用Derived类的fun2()
输出:

Derived:fun2()

虚函数也可以定义为private:

class Base {
private:
    virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derived:public Base {
public:
    virtual void fun2() { cout << "Derived:fun2()" << endl; }
};
Derived d;
Base * pBase = & d;
pBase -> fun2(); // 编译出错

pBase已经被赋值为指向derived d的指针,不能调用base类的private函数。

多态的实例:游戏开发

游戏中有很多种怪物,每种怪物都有一个类与之对应。某个玩家创建的具体怪物就是对象
怪物的主要动作(成员函数)有:

  • 攻击(Attack),针对不同的被攻击者有不同的函数
  • 反击(FightBack),被某个怪物攻击时做出的相应动作
  • 掉血(Hurted),被攻击时会掉血,血量值不同有不同处理,如死亡

现在的需求是:已经有CWolf、CGhost两种怪物,需要设计新的怪物CThunderBird,并能满足和其他怪物的交互
顶层设计:
设置基类 CCreature,并且使CDragon, CWolf等其他类都从CCreature派生而来
image-20221208165241029
非多态的派生类设计:
由于每个怪物对于其他怪物的攻击和反击都是不同的,每个怪物类都要设计一组Attack和FightBack:

class class CCreature {
    protected: int nPower ; //代表攻击力
    int nLifeValue ; //代表生命值
};
class CThunderBird : public CCreature {
    public:
    void Attack(CWolf * pWolf) {
        ...表现攻击动作的代码
        pWolf->Hurted( nPower);
        pWolf->FightBack( this);
    }
    void Attack( CDragon * pDragon) {
        ...表现攻击动作的代码
        pDragon->Hurted( nPower);
        pDragon->FightBack( this);
    }
    void FightBack( CWolf * pWolf) {
        ....表现反击动作的代码
        pWolf ->Hurted( nPower / 2);
    }
    void FightBack( CDragon * pDragon) {
        ....表现反击动作的代码
        pDragon->Hurted( nPower / 2 );
    }
    void Hurted ( int nPower) {
        ....表现受伤动作的代码
        nLifeValue -= nPower;
    }
}

现有n种怪物,CThunderBird类中就得有n个Attack 和n个FightBack成员函数,对于其他类也得新增针对CThunderBird的Attack和FightBack。这种设计工作量过于巨大。原因就在于要区分传入的对象指针。
那么能否传入基类的指针呢,这样就不存在为各种类型写几个函数。基类指针要访问派生类的成员,得用虚函数形成多态。多态实现如下:

//基类 CCreature:
class CCreature {
protected :
    int m_nLifeValue, m_nPower;
    public:
    virtual void Attack( CCreature * pCreature) {}
    virtual void Hurted( int nPower) { }
    virtual void FightBack( CCreature * pCreature) {}
};
//派生类 CDragon:
class CDragon : public CCreature {
public:
    virtual void Attack( CCreature * pCreature);
    virtual void Hurted( int nPower);
    virtual void FightBack( CCreature * pCreature);
};

//派生类的成员函数实现具体操作
void CDragon::Attack(CCreature * p) //传入基类指针
{ …表现攻击动作的代码
    p->Hurted(m_nPower); //多态
    p->FightBack(this); //多态
}
void CDragon::Hurted( int nPower)
{ …表现受伤动作的代码
    m_nLifeValue -= nPower;
}
void CDragon::FightBack(CCreature * p)
{ …表现反击动作的代码
    p->Hurted(m_nPower/2); //多态
}

//多态的调用
CDragon Dragon; CWolf Wolf; CGhost Ghost;
CThunderBird Bird;
Dragon.Attack( & Wolf); //调用CWolf::Hurted
Dragon.Attack( & Ghost); //调用CGhost::Hurted
Dragon.Attack( & Bird); //调用CBird::Hurted

使用多态,新增某个派生类时,已有的类可以原封不动,因为传入基类指针,会“自动”调用正确的派生类函数,开发者只需要设计新增的派生类和其成员函数即可

多态的原理:虚函数表指针

多态” 的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定,这叫“动态联编”
首先分析包含虚函数的类对象的内存分布:

class Base {
public:
int i;
    virtual void Print() { cout << "Base:Print" ; }
};

class Derived : public Base{
public:
int n;
    virtual void Print() { cout <<"Drived:Print" << endl; }
};

int main() {
    Derived d;
    cout << sizeof( Base) << ","<< sizeof( Derived ) ;
    return 0;
}

输出:8, 12
为什么类对象的size比成员变量int(4字节)还多4字节?
因为包含虚函数的基类,实例化的对象除了成员变量,还包含一个指针(一般4字节),指向虚函数的入口地址,如果有多个虚函数,这些地址连续排列形成虚函数表,指针指向首个虚函数地址。如果这个指针指向基类,就能找到基类的所有虚函数入口,如果指针指向派生类,就能找到派生类的的所有虚函数入口。基类和派生类对象的指针赋值,实际会导致虚函数表指针指向的虚函数入口地址不同,从而调用时不同。
如果当前指针指向基类,则调用基类自己的虚函数:

Base b;
pBase = &b;
pBase->Print();

image-20221208165259927
如果当前指针指向派生类,则调用派生类的虚函数:

Derived d;
pDerived = &d;
pBase = pDerived;
pBase->Print();

image-20221208165311145
动态联编的实现:
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令
而普通函数是编译过程中确定了成员函数的入口地址,不存在运行时根据对象来改变某个函数的入口地址。

虚函数与抽象类

可以想象得到,前文的游戏使用虚函数的例子是通用的,先设计基类,提炼对象属性,定义虚函数;再派生子类,在子类实现局函数的具体操作。那么问题来了,基类的虚函数有必要实现函数体吗?
很多情况,基类只是一个抽象,定义了函数的名称和参数,不需要在基类实现虚函数,全部交给派生类实现。

  • 纯虚函数:没有函数体的虚函数
  • 抽象类:包含纯虚函数的类

纯虚函数写法:没函数体{},直接=0

class A {
private: int a;
public:
    virtual void Print( ) = 0 ; //纯虚函数
    void fun() { cout << "fun"; }
};

抽象类特点:

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
  • 可以创建抽象类的指针和引用,它们可以指向派生类的对象

抽象类的指针:

A a ; // 错, A 是抽象类,不能创建对象
A * pa ; // ok,可以定义抽象类的指针和引用
pa = new A ; //错误, A 是抽象类,不能创建对象

如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类
抽象类的成员函数可以调用纯虚函数,但是构造函数或析构函数内不能调用纯虚函数

class A {
public:
    virtual void f() = 0; //纯虚函数
    void g( ) { 
        this->f( ) ; //ok
    }
    A( ){ 
        f( ); // 错误
    }
};
class B:public A{
public:
    void f(){cout<<"B:f()"<<endl; }
};

虚函数与构造析构函数

前面考虑了普通成员函数加virtual,可以形成虚函数达到继承类的多态效果。那么构造函数和析构函数呢?

  • 不允许以虚函数作为构造函数
  • 类继承需要把基类的析构函数设为虚函数

对于常规析构函数,通过基类指针删除派生类对象时,只能调用基类的析构函数。但是合理的做法是,应该先调用派生类的析构函数,然后调用基类的析构函数。解决的方法:把析构函数定义为virtual,由于基类析构函数是虚函数,派生类的同名析构函数自然也是虚函数。
什么时候定义虚析构函数

  • 一个类只要定义了虚函数,则应该将析构函数也定义成虚函数
  • 一个类打算作为基类使用,则应该将析构函数定义成虚函数

虚析构函数用法:通过基类的指针删除派生类对象,会首先调用派生类的析构函数,然后调用基类的析构函数

class son{
public:
    virtual ~son() {cout<<"bye from son"<<endl;};
};
class grandson:public son{
public:
    ~grandson(){cout<<"bye from grandson"<<endl;};
};
int main() {
    son *pson;
    pson= new grandson(); //pson指向派生类grandson
    delete pson;
    return 0;
}

输出:

bye from grandson
bye from son