C++继承、多态与虚表

拟墨画扇 提交于 2019-11-26 23:54:56

继承

继承的一般形式

787854ba-8e5b-4bac-a43a-86a63387ad2d

子类继承父类,是全盘继承,将父类所有的东西都继承给子类,除了父类的生死,就是父类的构造和析构是不能继承的。

继承的访问权限从两方面看:

1.对象:对象只能直接访问类中公有方法和成员。

2.继承的子类

9f5e5c82-a834-4d8a-9aa9-40e4a708422c

私有继承就终止了父类再往下继承的能力

c++默认继承为私有继承

像以下程序

class D :public B1 ,public B2,public B3

公有继承B1,B2,B3

class D :public B1,B2,B3;

公有继承B1,私有继承B2,B3

继承是按照继承的顺序,和构造函数的初始化顺序无关,看以下程序

image

如果,子类中有对象成员,构造顺序是:1.父类,2.对象成员,3.自己

image

如果父类中有虚基类,应该先构造虚基类

image

虚基类

虚基类主要解决菱形继承问题,有以下程序

image

继承模型为:

40a072fa-e845-483a-92ab-4a526991b96e

内存模型:

9a6c638d-e400-4ce0-a814-b17531d86bbe

如果对父类的x进行赋值,如下程序,会引发错误,编译器会报错,因为继承了两份,会产生二义性

9216355d-2a52-416f-b675-789b6d471653

如果指定访问A1还是A2就不会报错

image

内存中只为A1的成员x赋值了

2f73ba47-4920-489d-b234-c2745051dab6

如果希望来自父类的x在子类中只有1份

那么就要用虚拟继承

image

对于虚继承来的基类,又叫做虚基类

现在对cc.c进行赋值

84f35c11-4bb7-4ede-b6fe-0dcecb051652

7f641451-63d3-4bb3-80fe-9908711512b5

A1和A2对象中的x成员都变成100

如果不是虚拟机成A1和A2继承来的x各自是各自的空间

c43eac19-8000-46be-bbc4-7292f31704a5

虚继承让子类只保持父类的一份成员拷贝,A1和A2的继承的成员的空间是一个

803575cc-85e9-431d-b4e1-b4401d4c5f2c

如果是普通继承求C类型的大小为20字节

A1和A2各占8字节,C占4字节,加起来20字节

虚继承时,字节为24个,理论应该是16字节,但是多了两个虚表指针,空间就会增加

继承中的同名隐藏

子类继承父类,子类中有父类的同名方法,访问的是子类的方法,子类会隐藏父类所有的同名方法,即使父类有一个同名的参数不同的方法也是如此。如下程序:

image

如果子类对象访问父类的fun(int a)方法,编译会报错

09c3eb2f-d894-408c-bf7c-ac321a0292eb

但是通过作用域访问父类方法是可以访问的

多态

关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技 术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

如果通过父类的对象调用fun,当然是调用父类的方法,因为编译时期就决定了

image

赋值兼容规则

1.可以将子类对象赋值给父类,其实只是把子类中的父类的部分,赋值给了父类。(称为对象的切片)

2.子类的对象赋值给父类的指针

3.子类的对象初始化父类的引用

2ae21f30-7eb8-486e-b117-1d49b96f4a44

达到多态

想要实现多态,需要动态绑定,需要父类的指针或父类的引用

父类方法为虚方法,子类覆盖父类的虚方法,才能达到多态

上述程序中的指针和引用调用

pb->fun();
rb.fun();

就会访问到子类的方法

子类中父类没有的方法,父类指针也无法访问到,父类指针只能访问到父类自己有的范围

注意:

virtual与函数重载无关,基类没有virtual,子类的同名方法就是隐藏而不是覆盖

只要没有指针,父类调用子类的,全部是调用父类作用范围内的,就算子类覆盖了父类的virtual方法也是如此。

子类覆盖了父类的虚方法,子类的方法也是虚方法,即使前面不写不写virtual也是如此

子类要覆盖父类的方法,就是要函数名参数都必须一样才叫覆盖

抽象类

如果基类是一个抽象的概念,就可以为其定义纯虚函数构成一个抽象类

以下的抽象类就定义了三个纯虚函数

image

注意:

如果一个类具有一个或者多个纯虚函数,那么它就是一个抽象类,必须让其他类继承它。

抽象类位于类层次的上层,不能定义抽象类的对象。

基类中的纯虚函数没有代码,而且必须在子类中覆盖基类中的纯虚函数。

子类中如果没有实现其抽象父类的所有纯虚函数,其也是一个抽象类,也不能实例化对象。

抽象类和虚函数的类有区别:

抽象类中的纯虚函数不实现,并且继承他的子类必须实现它

虚函数基类中可以有实现,并且子类中如果重新覆盖了它,还可以实现多态机制

多态的实现

先看一个程序

class Base
{
public:
    virtual void fun()
    {
        cout<<"This is Base::fun()"<<endl;
    }
    void fun(int a)
    {
        cout<<"This is Base::fun(int)"<<endl;
    }
};

class D : public Base
{
public:
    void fun()
    {
        cout<<"This is D::fun()"<<endl;
    }
};

void main()
{
    D d;
    d.fun();
    d.fun(0);
}

当父类没有virtual时,d对象的内存模型

image

当父类有了virtual时,d对象的内存模型

3f74c113-9c30-4e43-aead-c1fed2c5be92

可以看到,父类会多出来一个虚表指针,保存一个子类的方法

再来看一个程序

class Base
{
public:
    Base():y(0)
    {
        cout<<"Create Base Object."<<endl;
    }
public:
    virtual void fun()
    {
        cout<<"This is Base::fun()"<<endl;
    }
    virtual void list()
    {
        cout<<"This is Base::list()"<<endl;
    }

    void print()
    {
        cout<<"This is Base::print()"<<endl;
    }
public:
    int y;
};

class D : public Base
{
public:
    D():x(0)
    {
        cout<<"Create D Object."<<endl;
    }
public:
    void fun()
    {
        cout<<"This is D::fun()"<<endl;
    }
    void list()
    {
        cout<<"This is D::list()"<<endl;
    }

    void print()
    {
        cout<<"This is D::print()"<<endl;
    }
    
private:
    int x;
};

void main()
{
    D d;
    Base *pb = &d;
    pb->fun();
    pb->list();
    pb->show();
}

因为创建子类对象前,要先构造父类对象,其内存模型为

3ce9950d-a1e8-47bd-972b-88eae9fa2a1b

虚表中存储的都是父类的虚函数指针

只要有虚方法,创建对象就会自动建立一个虚表,表的最后一个位置为NULL

26c4fa66-7357-431c-83b7-bbd4317eab65

后面的八个0就是表的最后一个位置NULL (不同平台可能不一样,这是VC平台下的结果)

虚表中存储父类的方法的地址,虚表指针指向这块地址,这就是为什么加了virtual后多了4个指针的原因

3598f77a-d261-4951-8837-7da32031c7f8

子类对象地址为什么能赋值给父类对象指针?

因为,子类对象地址赋值给父类对象指针,父类对象指针就指向了子类的对象空间,父类操作子类的范围是有限制的,只能操作到子类中父类的范围。

上面谈到构造子类对象前先构造一个父类对象

当构造父类对象完成时候,再构造子类时,可以看到,虚表中的方法已经被更改为子类的了

3107da1c-b65f-40d5-8d00-87ea6ebdd0a1

现在我们在父类中加一个虚的show()方法,而子类中不去覆盖父类的show方法,可以看到子类中虚表的成员show还是父类的函数指针

5e6e118b-8504-4a89-b116-ba44de106773

用父类的指针来调用,是在虚表中调用,但是经过子类的覆盖,虚表中的函数地址已经变成了子类的函数的地址了,所以会调用子类的方法

如下图

59c1e61b-ce45-498f-a4cc-739ba40a441c

通过指针访问虚表的函数成员

有以下程序

class Base 
{ 
public: 
    virtual void f()
    { cout << "Base::f"<<endl;}
    virtual void g()
    { cout << "Base::g"<<endl;}
    virtual void h()
    { cout << "Base::h"<<endl;}
private:
    int a;
    int x;
}; 

typedef void(*pFun)();

void main()
{
    Base b;
    cout<<"&b = "<<&b<<endl;
    cout<<"vfptr = "<<hex<<*(int*)(&b)<<endl;    //虚表的地址,前四个字节

    pFun pfun = (pFun)*(((int*)*(int*)(&b))+0);    //取虚表中第一个虚函数的地址
    pfun();
    pfun = (pFun)*(((int*)*(int*)(&b))+1);
    pfun();
    pfun = (pFun)*(((int*)*(int*)(&b))+2);
    pfun();
}

注意:虚表是在一个对象空间的开始位置

通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。

画个图解释一下。如下所示:

0.92727677412201

注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。

在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。(上面内容提到过)

下面,我将分别说明“无覆盖”和“有覆盖”时的子类虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。

一般继承(无虚函数覆盖)

下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

0.12154239708264525

请注意,在这个继承关系中,子类没有重写任何父类的函数。那么,在派生类的实例的虚函数表如下所示:

对于实例:Derive d; 的虚函数表如下:(overload(重载) 和 override(重写),重载就是所谓的名同而签名不同,重写就是对子类对虚函数的重新实现。)

0.8101163589599465

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

0.2945619967221582

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例的虚函数表会是下面的样子:

0.0692499251192753

我们从表中可以看到下面几点,

1)覆盖的f()函数被放到了子类虚函数表中原来父类虚函数的位置。

2)没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();

b->f();

由b所指的内存中的虚函数表(子类的虚函数表)的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

多重继承(无虚函数覆盖)

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数

0.8060241828430086

对于子类实例中的虚函数表,是下面这个样子:

0.8257025783841201

我们可以看到:

1) 每个父类都有自己的虚表。

2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数覆盖)

下面我们再来看看,如果发生虚函数覆盖的情况。

下图中,我们在子类中覆盖了父类的f()函数。

0.5885229699955636

下面是对于子类实例中的虚函数表的图:

0.006813835416641378

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以用任一个父类指针来指向子类,并调用子类的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

安全性

一、尝试:通过父类型的指针(指向子类对象)访问子类自己的虚函数

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到子类的虚表中有Derive自己的虚函数,但我们根本不可能使用基类的指针来调用子类的自有虚函数:

Base1 *b1 = new Derive();
b1->f1(); //编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。

但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。

二、尝试:通过父类型的指针(指向子类对象)访问父类的non-public虚函数

另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于子类虚函数表中,所以我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

如下程序:

class Base {
private:
    virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};

typedef void(*Fun)(void);

void main() {
    Derive d;
    Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
    pFun();
}

 

[参考:https://blog.csdn.net/sanfengshou/article/details/4574604]

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!