`
russelltao
  • 浏览: 151828 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

C++的多态如何在编译和运行期实现

 
阅读更多

多态是什么?简单来说,就是某段程序调用了一个API接口,但是这个API有许多种实现,根据上下文的不同,调用这段API的程序,会调用该API的不同实现。今天我们只关注继承关系下的多态。


还是得通过一个例子来看看C++是怎样在编译期和运行期来实现多态的。很简单,定义了一个Father类,它有一个testVFunc虚函数哟。再定义了一个继承Father的Child类,它重新实现了testVFunc函数,当然,它也学习Father定义了普通的成员函数testFunc。大家猜猜程序的输出是什么?

#include <iostream>
using namespace std;

class Father
{
public:
	int m_fMember;

	void testFunc(){
		cout<<"Father testFunc "<<m_fMember<<endl;
	}
	virtual void testVFunc(){
		cout<<"Father testVFunc "<<m_fMember<<endl;
	}
	Father(){m_fMember=1;}
};

class Child : public Father{
public:
	int m_cMember;
	Child(){m_cMember=2;}
	
	virtual void testVFunc(){cout<<"Child testVFunc "<<m_cMember<<":"<<m_fMember<<endl;}
	void testFunc(){cout<<"Child testFunc "<<m_cMember<<":"<<m_fMember<<endl;}
	void testNFunc(){cout<<"Child testNFunc "<<m_cMember<<":"<<m_fMember<<endl;}
};


int main()
{
	Father* pRealFather = new Father();
	Child* pFalseChild = (Child*)pRealFather;
	Father* pFalseFather = new Child();
	
	pFalseFather->testFunc();
	pFalseFather->testVFunc();

	pFalseChild->testFunc();
	pFalseChild->testVFunc();	
	pFalseChild->testNFunc();	

	return 0;
}

同样调用了testFunc和testVfunc,输出截然不同,这就是多态了。它的g++编译器输出结果是:

Father testFunc 1
Child testVFunc 2:1
Child testFunc 0:1
Father testVFunc 1
Child testNFunc 0:1

看看main函数里调用的五个test*Func方法吧,这里有静态的多态,也有动态的多态。编译是静态的,运行是动态的。以下解释C++编译器是怎么形成上述结果的。


首先让我们用gcc -S来生成汇编代码,看看main函数里是怎么调用这五个test*Func方法的。

        movl    $16, %edi
        call    _Znwm 
        movq    %rax, %rbx
        movq    %rbx, %rdi
        call    _ZN6FatherC1Ev
        movq    %rbx, -32(%rbp)
        movq    -32(%rbp), %rax
        movq    %rax, -24(%rbp)
        movl    $16, %edi
        call    _Znwm 
        movq    %rax, %rbx
        movq    %rbx, %rdi
        call    _ZN5ChildC1Ev
        movq    %rbx, -16(%rbp)
        movq    -16(%rbp), %rdi
        call    _ZN6Father8testFuncEv    本行对应pFalseFather->testFunc();
        movq    -16(%rbp), %rax
        movq    (%rax), %rax
        movq    (%rax), %rax
        movq    -16(%rbp), %rdi
        call    *%rax										本行对应pFalseFather->testVFunc();
        movq    -24(%rbp), %rdi
        call    _ZN5Child8testFuncEv		本行对应pFalseChild->testFunc();
        movq    -24(%rbp), %rax
        movq    (%rax), %rax
        movq    (%rax), %rax
        movq    -24(%rbp), %rdi
        call    *%rax										本行对应pFalseChild->testVFunc();	
        movq    -24(%rbp), %rdi
        call    _ZN5Child9testNFuncEv		本行对应pFalseChild->testNFunc();	
        movl    $0, %eax
        addq    $40, %rsp
        popq    %rbx
        leave

红色的代码,就是在依次调用上面5个test*Func。可以看到,第1、3次testFunc调用,其结果已经在编译出来的汇编语言中定死了,C++代码都是调用某个对象指针指向的testFunc()函数,输出结果却不同,第1次是:Father testFunc 1,第3次是:Child testFunc 0:1,原因何在?在编译出的汇编语言很明显,第一次调用的是_ZN6Father8testFuncEv代码段,第三次调用的是_ZN5Child8testFuncEv代码段,两个不同的代码段!编译完就已经决定出同一个API用哪种实现,这就是编译期的多态。


第2、4次testVFunc调用则不然,编译完以后也不知道以后究竟是调用Father还是Child的testVFunc实现,直到运行时,拿到CPU寄存器里的指针了,才知道这个指针究竟指向Father还是Child的testVFunc实现。这就是运行期的多态了。


现在我们看看,C++的对象模型是怎么实现这一点的,以及为什么最后打印的是如此结果。还以上面的代码做例子,生成的pFalseFather指向的对象是一个Child对象,它的内存布局是:


再来看看调用代码:

	Father* pFalseFather = new Child();

	pFalseFather->testFunc();
	pFalseFather->testVFunc();

当我们调用pFaseFather->testFunc()代码时,这不是个virtual函数,所以,汇编代码里直接调用了Father::testFunc()实现,这是C++的规则。C++中,如果不是virtual字段的成员函数,调用它的程序将在编译时就直接调用到函数实现。所以,这行代码将执行以下C++代码:

	void testFunc(){
		cout<<"Father testFunc "<<m_fMember<<endl;
	}

注意到,pFaseFather指向的是个Child对象,所以Child对象在生成时同时执行了自己和Father父类的构造函数,所以,m_fMember被初始化为1,打印的结果就是Father testFunc 1。


而pFalseFather->testVFunc();调用了vptl指向的函数,上面说了,pFaseFather指向的是个Child对象,而Child对象实现了自己的testVFunc方法,在你new一个Child对象时,编译器会将vptl指向它自己的testVFunc的。所以,将会执行下面的C++代码:

virtual void testVFunc(){cout<<"Child testVFunc "<<m_cMember<<":"<<m_fMember<<endl;}

m_cMemeber被Child的构造函数初始化为2,m_fMember被Father的构造函数初始化为1,所以打印出的结果是:Child testVFunc 2:1。


下面我们看看最后三个调用:

	pFalseChild->testFunc();
	pFalseChild->testVFunc();	
	pFalseChild->testNFunc();	

我们生成了一个pRealFather指向Father对象,它的内存空间是这样的:


而后我们通过:

Child* pFalseChild = (Child*)pRealFather;

指针pFalseChild是个Child类型,但它实际指向的是个Father对象。首先它调用testFunc函数,到底执行Father还是Child的实现呢?上面说过,非virtual函数一律编译期根据类型决定,所以,它调用的是Child实现:

void testFunc(){cout<<"Child testFunc "<<m_cMember<<":"<<m_fMember<<endl;}

这里,m_fMember被Father的构造函数初始化为1,而m_cMember已经内存越界了!没错,在32位机器上,Father对象只有8个字节,而Child对象有12个字节,访问的m_cMember就是第9-12个字节转换成的int类型。通常情况,这段内存都是全0的,所以,m_cMember是0。看看结果:Child testFunc 0:1。


然后它调用testVFunc了,这次执行父类还是子类的?是父类的,因为这个对象是Father对象,在new出来的时候,Father的构造函数会把vptl指针指向自己的testVFunc实现哟。所以将会执行C++代码:

	virtual void testVFunc(){
		cout<<"Father testVFunc "<<m_fMember<<endl;
	}

执行结果自然是:Father testVFunc 1。


最后一个调用testNFunc,真实的Father对象对应的Father类中可没有这个函数,但是实际编译执行都没问题,why?同上理,在main函数中,因为指针pFalseChild是个Child类型,编译完的汇编语言在pFalseChild->testNFunc();这里就直接调用Child的testNFunc实现了,虽然m_cMember越界了,可是并不影响程序的执行哦。



分享到:
评论

相关推荐

    C++编译期多态与运行期多态

    在面向对象C++编程中,多态是OO三大特性之一,这种多态称为运行期多态,也称为动态多态;在泛型编程中,多态基于template(模板)的具现化与函数的重载解析,这种多态在编译期进行,因此称为编译期多态或静态多态。在...

    C++面向对象之多态的实现和应用详解

    前言 本文主要给大家介绍的是关于C++面向对象之多态的实现和应用的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看... 静态多态:静态多态就是重载,因为是在编译期决议确定,所以称为静态多态。 动态

    C++笔试题(很全的)

    很辛苦的找到的,比较全 表示已有答案 表示没有处理 表示答案不确定 C++笔试题 1.多态类中的虚函数表是...而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键.

    多态性.ppt

    C++中的多态(虽然多态不是C++所特有的,但是C++中的多态确实是很特殊的)分为静多态和动多态(也就是静态绑定和动态绑定两种现象),静动的区别主要在于这种绑定发生在编译期还是运行期

    C++笔试面试要点

    C++笔试题 1.多态类中的虚函数表是Compile-Time,还是Run-Time时建立的? 答案:虚拟函数表是在编译期就建立了...而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键.

    C++入门到精通

    读者亲自编译并执行这些程序 第 2 章介绍了 C++是如何通过类机制 为基于对象和面向对 象的程序设计提供支持的 同时通过数组抽象的演化过程来说明这些设计思想 另外 它简 要介绍了模板 名字空间 异常处理 以及标准...

    程序员面试经典题

    答案:虚拟函数表是在编译期就建立了 各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组 而对象的隐藏成员 虚拟函数表指针是在运行期 也就是构造函数被调用时进行初始化的 这是实现多态的关键 2 将一个 1M ...

    朗讯、华为C++笔试题、答案

    华为朗讯C++笔试题C++笔试题 1.多态类中的虚函数表是Compile-Time,还是Run-Time时建立的?...而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键.

    最新名企标准通用C++面试题,

    这样声明之后,相当于告诉C, 函数const void f(void)是在C++语言的文件中声明或者实现的,c程序可以使用这个C++中的函数了,从而实现C++和c的混合编程。 13、编写一个函数,作用是把一个char组成的字符串...

    详解C++虚函数的工作原理

    静态绑定与动态绑定 讨论静态绑定与动态绑定,首先需要理解的是绑定,何为绑定?...在C++中动态绑定是通过虚函数实现的,是多态实现的具体形式。而虚函数是通过虚函数表实现的。这个表中记录了虚函数的地址,

    C/C++面试题目及解答.doc

    而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键. &lt;br&gt;2.将一个 1M -10M 的文件,逆序存储到另一个文件,就是前一个文件的最后一个 字符存到新文件的第...

    探讨C++中不能声明为虚函数的有哪些函数

    多态的运行期行为体现在虚函数上,虚函数通过继承方式来体现出多态作用,顶层 函数不属于成员函数,是不能被继承的 2.为什么C++不支持构造函数为虚函数? 这个原因很简单,主要是从语义上考虑,所以不支持。因为...

    c++ 面试题 总结

    1.是不是一个父类写了一个virtual 函数,如果子类覆盖它的函数不加virtual ,也能实现多态? virtual修饰符会被隐形继承的。 private 也被集成,只事派生类没有访问权限而已 virtual可加可不加 子类的空间里有父类...

    精通MFC (光盘) 源代码

    1.1.2 封装、多态和继承 1.1.3 消息 1.2 面向对象的建模技术UML 1.2.1 类图 1.2.2 交互图 1.2.3 用例图 1.3 面向对象的C++语言 1.3.1 C++对象的内存布局 1.3.2 C++编程技术要点 1.4 小结 第2章 窗口 2.1...

    C#微软培训资料

    18.2 在 C #代码中调用 C++和 VB 编写的组件 .240 18.3 版 本 控 制 .249 18.4 代 码 优 化 .252 18.5 小 结 .254 第五部分 附 录 .255 附录 A 关 键 字.255 附录 B 错 误 码.256 附录 C .Net 名字空间...

    语言程序设计课后习题答案

    由于图形用户界面的应用,程序运行由顺序运行演变为事件驱动,使得软件使用起来越来越方便,但开发起来却越来越困难,对这种软件的功能很难用过程来描述和实现,使用面向过程的方法来开发和维护都将非常困难。...

    深入浅出MFC-简体版(2)PDF

    第一篇 勿在浮砂筑高台 第1章 Win32程序基本概念 Win32程序开发流程 需要什么函数库(.LIB) 需要什么头文件(.H) 以消息为基础,以事件驱动之(message based,event driven) 一个具体而微的Win32程序 程序进入点...

    深入浅出MFC【侯捷】

    C++程序的生与死:兼谈构造函数与解构函数 四种不同的对象生存方式(in stack、in heap、global、local static) 执行期类型信息(RTTI) 动态生成(Dynamic Creation) 异常处理(Exception Handling) Template ...

    深入浅出MFC 2e

    对话框的运行 模块定义文件(.DEF) 资源描述档(.RC) Widnows程序的生与死 空闲时间的处理:OnIdle Console程序 Console程序与DOS程序的差别 Console程序的编译链接 JBACKUP:Win32 Console程序设计 MFCCON:MFC ...

    侯捷- -深入浅出MFC

    对话框的运行 模块定义文件(.DEF) 资源描述档(.RC) Widnows程序的生与死 空闲时间的处理:OnIdle Console程序 Console程序与DOS程序的差别 Console程序的编译链接 JBACKUP:Win32 Console程序设计 MFCCON:MFC ...

Global site tag (gtag.js) - Google Analytics