多态是什么?简单来说,就是某段程序调用了一个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++编程中,多态是OO三大特性之一,这种多态称为运行期多态,也称为动态多态;在泛型编程中,多态基于template(模板)的具现化与函数的重载解析,这种多态在编译期进行,因此称为编译期多态或静态多态。在...
总之,C++的多态性提供了强大的设计工具,静态多态通过编译期的决策提供效率和灵活性,而动态多态则通过运行时的动态绑定实现更高级别的抽象和代码复用。理解并熟练运用这两种多态形式,对于编写高效、可维护的C++...
静态多态是指在编译期确定函数的调用,而动态多态是指在运行期确定函数的调用。虚函数是一种动态多态的实现机制,它允许在程序中根据对象的类型来选择调用不同的函数。 示例代码 下面是一个简单的示例代码,演示了...
前言 本文主要给大家介绍的是关于C++面向对象之多态的实现和应用的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看... 静态多态:静态多态就是重载,因为是在编译期决议确定,所以称为静态多态。 动态
在编译期和运行期绑定中,编译期绑定是在编译期就确定了函数调用的入口地址,而运行期绑定是在编译期并没有确定了函数调用的入口地址,而是在程序运行时。虚函数的入口地址是通过系统对各个虚函数建有入口地址表,在...
此外,C++中的多态包括编译时多态和运行时多态。编译时多态通过函数重载和模板实现,而运行时多态则通过虚函数实现。 ### 重载 C++允许运算符重载,这使得自定义类型的运算能够像内置类型一样直观。教材中还讨论了...
在C++中,编译时多态通常通过函数重载和运算符重载来实现,而运行时多态则主要通过虚函数来实现。虚函数允许派生类重写基类中的方法,从而在运行时根据对象的实际类型来调用相应的函数版本,这是实现运行时多态的...
书中深入讨论了类型转换、静态类型与动态类型、类型信息的获取等话题,帮助读者理解类型在编译期和运行期的作用。 4. **继承与多态**:C++的继承是实现面向对象编程的关键特性。书中详述了继承层次结构中对象的构造...
C++提供了两种形式的多态:静态多态(编译时多态)和动态多态(运行时多态)。静态多态主要通过函数重载和运算符重载实现;动态多态则依赖于虚函数和纯虚函数,通过基类指针或引用调用派生类的成员函数,实现运行时...
这一期可能会讲解如何在C++中使用try/catch语句来捕获和处理运行时错误,以及如何利用断言(assertions)来进行调试。此外,还可能介绍一些调试工具和最佳实践,帮助开发者更快速地定位和解决问题。 #### 6. 性能...
10. **编译与链接过程**:了解C++的编译和链接过程有助于理解对象模型的实现细节,如编译器如何生成机器代码以及链接器如何整合不同源文件中的符号。 《Inside C++ Object Model》对于想要深入理解C++的程序员来说...
C++支持三种多态形式:静态多态(通过函数重载和运算符重载实现)、编译期多态(通过虚函数和模板实现)和运行期多态(通过动态绑定和抽象基类实现)。这些内容对于理解C++的灵活性和可扩展性至关重要。 书中还会...
### C++中的多态性详解 #### 知识点概览 - **多态性**在面向对象编程中是一项核心特性,它允许我们通过基类的指针或引用调用...掌握了这些知识点,对于想要深入了解C++多态特性的开发者来说,无疑是一个宝贵的资源。
C++中的多态(虽然多态不是C++所特有的,但是C++中的多态确实是很特殊的)分为静多态和动多态(也就是静态绑定和动态绑定两种现象),静动的区别主要在于这种绑定发生在编译期还是运行期
通过在编译期生成代码,可以实现高度优化的解决方案,但同时也增加了编译复杂性。 2. **STL(标准模板库)**: STL是C++的核心部分,它提供了容器(如vector、list、map)、迭代器、算法和函数对象,极大地提高了...
本书详细讲解了C++对象模型的各个方面,包括类、对象、继承、多态等核心概念在内存中的表示以及编译器如何实现这些特性。 1. **C++对象模型基础**:C++对象模型是C++语言在内存中的抽象表示,包括对象的创建、生命...
在C++中,编译期和运行期的概念对于理解和使用多态性至关重要: - **编译期**:在此阶段,编译器解析源代码,检查语法错误,解析符号引用,进行类型检查等。静态联编在此阶段完成,编译器确定函数调用的具体实现。 ...
9. **编译期计算与元编程**:C++允许在编译期间进行计算,如常量折叠和模板元编程,这可以提高程序性能并减少运行时开销。 10. **构造函数与析构函数**:正确使用构造函数和析构函数是保证对象初始化和清理的关键,...