当前位置:金沙国际 > 金沙唯一网址 > C++多态实现和virtual原理
C++多态实现和virtual原理
2020-02-25

金沙在线 1

问题:定义一个空的类型,里面没有任何的成员变量或者成员函数,对这个类型进行 sizeof 运算,结果是?

本文转载自:

《深度探索C++对象模型》是一本由斯坦利•B.李普曼 (Stanley B. Lippman)著作,电子工业出版社出版的平装图书,本书定价:69.00元,页数:320,特精心从网络上整理的一些读者的读后感,希望对大家能有帮助。

结果是1,因为空类型的实例不包含任何信息,按道理 sizeof 计算之后结果是0,但是在声明任何类型的实例的时候,必须在内存占有一定的空间,否则无法使用这些实例,至于占据多少内存大小,由编译器决定。

  1. 基本概念多态性是一个接口多种实现,分为类的多态性函数多态性函数的多态性是指一个函数被定义成多个不同参数的函数,它们一般被存在头文件中,当你调用这个函数,针对不同的参数,就会调用不同的同名函数。类的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

  2. 多态实现机制举个例子

《深度探索C++对象模型》读后感:吐槽

继续问:如果在这个类型里添加一个构造函数和析构函数,那么结果又是多少?

正在读此书,三点感悟:

还是1,因为我们调用构造函数和析构函数,只需要知道函数的地址即可,而这些函数的地址只和类型相关,和类型的实例无关,编译器不会为这两个函数在实例内添加任何额外的信息。

#include <iostream.h>class animal{ public: void sleep(){cout<<"animal sleep"<<endl;} void breathe(){cout<<"animal breathe"<<endl;}};class fish:public animal{ public: void breathe(){cout<<"fish bubble"<<endl;}};void main(){ fish fh; animal *pAn=&fh; pAn->breathe();}

1. 内容不错,不过有一些过时的内容,cfront作为大婶们第一个C++编译器早在1993年就被取消了,书上很多数据与现在的编译器已经不一致。

继续问:如果把析构函数变为虚函数呢?结果是多少?

答案是输出:animal breathe结果分析:

  1. 候大婶勘误了很多原著的错误,不过我还是想读一下原著。

  2. 请问中文版是用谷歌翻译的吗?各种生硬的翻译腔语句让我看的好痛苦。

金沙在线 ,c++编译器发现了类型里有虚函数,,就会为这个类型生成一个虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,在32位机器,指针类型大小是4字节,结果是4,64位机器中,指针大小是8字节,结果是8。

...

面向对象的多态的实现效果

要想得到我们想要的结果就要使用虚函数前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

继续痛苦研读..

多态:同样的调用语句有多种不同的表现形态

下面我们将上面一段代码进行部分修改

《深度探索C++对象模型》读后感:非常有深度

看下面的代码例子:

virtual void breathe(){cout<<"animal breathe"<<endl;}

很好很强大,也非常深奥,比Effective系列深太多了。只有前5章勉强看懂。

class animal
{
public:
    void sleep()
    {
        cout<<"animal sleep"<<endl;
    }

    void breathe()
    {
        cout<<"animal breathe"<<endl;
    }
};

class fish:public animal
{
public:
    void breathe()
    {
        cout<<"fish bubble"<<endl;
    }
};

int main(void)
{
    fish fh;
    animal *pAn=&fh;
    pAn->breathe();
    return 0;
}    

运行结果:fish bubble结果分析编译器为每个类的对象提供一个虚表指针,这个指针指向对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。

这本书偏重的是C++内部实现,而effective则是讲C++的常用技术。虽然在技术上没有太大帮助,但绝对大大提高内功,会给你剖析了C++ class的实现机制。

父类指针指向了子类对象,调用了 breathe 方法,那么结果是animal breathe,也就是说调用的是父类的breathe方法。 这没有实现多态性。因为C++编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定(early binding),当fish类的对象fh的地址赋给父类的pAn指针时,C++编译器进行了类型转换,它认为父类的指针变量pAn保存的就是animal对象的地址。当在main()函数中执行pAn->breathe()时,调用的就是animal对象的breathe函数。

由于pAn实际指向的对象类型是fish,因此vptr指向的fish类的vtable,当调用pAn->breathe()时,根据虚表中的函数地址找到的就是fish类的breathe()函数。

建议想深入C++的人都读一读,会有一种醍醐灌顶的作用(原来C++内部是这样实现的啊~)。

进一步说:

正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。

以后回头再看第二遍,现在功力不够。

金沙在线 3

那么虚表指针在什么时候,或者说在什么地方初始化呢?答案是在构造函数中进行虚表的创建和虚表指针的初始化。还记得构造函数的调用顺序吗,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。

《深度探索C++对象模型》读后感:太恐怖了

在我们构造fish类的对象时,首先要调用父类:animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。当将fish类的对象转换为animal类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中的“animal的对象所占内存”。

当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。在类型转换后,调用pAn->breathe(),由于pAn实际指向的是fish类的对象,该对象内部的虚表指针指向的是fish类的虚表,因此最终调用的是fish类的breathe()函数。

读的是2012重印的书。内容不错,也并没有因为时隔近二十年而过时多少,然而翻译时加的太多译注让人扫兴!

那么当利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此,输出animal breathe。这不是多态的表现形式。

在该文章中详细讲解了继承中类的内存对象模型:

错误和非错误都擅自修改,比如把==改成=,又重复多次“译注”;把virtual table中虚函数的引号去掉,原因是“怕读者以为是字符串”;各种提醒,请注意,唯恐读者不懂,真不懂的人恐怕连提醒都看不懂,懂的人又多余。

多态实现的三个条件

  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类不能被称为抽象类(abstract class)。
  2. 虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。
  3. 虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。
  4. 虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。
  5. 虚函数的定义形式:virtual {method body}纯虚函数的定义形式:virtual { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。
  6. 虚函数必须实现,如果不实现,编译器将报错,错误提示为:error LNK****: unresolved external symbol "public: virtual void __thiscallClassName::virtualFunctionName"
  7. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
  8. 实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
  9. 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
  10. 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。a.编译时多态性:通过重载函数实现b 运行时多态性:通过虚函数实现
  11. 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。

不过加的几幅图到是值得夸奖,然而我对侯捷的译作已经产生了阴影~

必要的前提是必须有继承关系、然后我们需要父类指针(引用)去调用子类的对象,且关键是:子类有对父类的虚函数的重写。virtual关键字,告诉编译器这个函数要支持多态,我们不要根据指针类型判断如何调用方法,而是要根据指针所指向的实际对象类型来判断如何调用。

.这本书有股奇怪的味道,不是平常书的味道…

多态的理论基础

《深度探索C++对象模型》读后感:C++编译器说明书 + virtual 关键字详解 !

前面的例子,输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

这本书非常适合用来装逼!人活着不为了装逼,那跟咸鱼又有什么区别!

所谓的动态联编:根据实际的对象类型来判断重写函数的调用。

整本书几乎没讲C++的任何语法,任何编程技巧,任何使用经验,说的内容就如标题所言:C++编译器说明书

C++中多态的实现原理