动态联编 和 静态联编
首先我们知道的是,动态联编 和 静态联编 都是多态性的一种体现。
关于面向对象的三个基本要素:封装(类型抽象), 继承 和 多态。
首先我们从概念性上面了解了 动态联编 和 静态联编 的功能:实现了多态性。
然后我们从最最基本的开始讲解。
1.什么是 联编?
我参考了下面这个博客:
http://bdxnote.blog.163.com/blog/static/8444235200911311348529/
联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的操作调用(函数调用)与执行该操作(函数)的代码段之间的映射关系;按照联编所进行的阶段不同,可分为静态联编和动态联编;
仔细读读红色字体的那部分句子。我们就能很清楚的明白什么是联编了。给大家举个最通俗易懂的例子好了:
A类中有fun这个函数, B类中也有fun这个函数,现在我在类外的main函数里面调用fun 函数。
那么main函数就是函数调用,调用fun函数,
而A类中的fun函数和B类中的fun函数就是执行该操作的代码段
所以现在联编就是实现两者的映射关系。
- class A
- {
- void func() {cout<<"It's A"<<endl;
- };
- class B
- {
- void func() {cout<<"It's B"<<endl;
- };
- int main()
- {
- func();
- }
联编就是决定将main函数中的func()的函数调用映射到A中的func函数还是B中的func函数的过程。
2.静态联编 和 动态联编 的定义
知道了什么事联编,那么再来理解动态联编 和静态联编也就不难了
静态联编:
是指联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编;因为这种联编是在程序开始运行之前完成的;
在程序编译阶段进行的这种联编又称静态束定;在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又被称为束定;编译时束定又称为静态束定;
拿上面的例子来说,静态联编就是在编译的时候就决定了main函数中调用的是A中的func还是B中的func。一旦编译完成,那么他们的映射关系就唯一确定了。
动态联编:
编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或动态束定,又叫晚期联编;C++规定:动态联编是在虚函数的支持下实现的;
动态联编在编译的时候还是不知道到底应该选择哪个func函数,只有在真正执行的时候,它才确定。
静态联编和动态联编都是属于多态性的,它们是在不同的阶段进对不同的实现进行不同的选择;
其实多态性的本质就是选择。因为存在着很多选择,所以就有了多态。
3.静态联编
首先还是拿个例子来说事吧。
- #include <iostream>
- using namespace std;
- class shape{
- public:
- void draw(){cout<<"I am shape"<<endl;}
- void fun(){draw();}
- };
- class circle:public shape{
- public:
- void draw(){cout<<"I am circle"<<endl;}
- };
- void main(){
- circle oneshape;
- oneshape.fun();
- }
现在我们详细具体定义了一开始的A类和B类以及func函数。让我们来分析一下:
调用oneshape.fun()的时候,进入类shape中的fun函数。
现在我们的问题就是:fun函数调用的draw到底是shape里面的draw还是circle中的draw??
答案是:它调用了cshape这个基类的draw函数。所以输出了 I am shape
那么一直困扰我的问题是:为什么调用基类的draw而不是派生类中得draw呢?
书上好像没有具体讲,上课的时候老师那也根本不会讲。
自己想了一下,应该可以从汇编的角度理解:
1.调用oneshape.fun(),这里是一个跳转指令,进入类shape中的fun函数所在的代码段
2.类shape的代码段是依次顺序放置的。进入fun函数后,现在我们要调用draw的地址。
由于没有另外的数据结构来保存draw的地址,所以程序所知道的,必然只有在shape类中的draw地址了,仅仅用一个跳转指令
在我的vs2010的反汇编调试窗口下是这样的一句代码:
013B1546 call shape::draw (13B10F5h)
很明确这里指出了shape::draw,也就确定了映射关系,完成了联编。
4.动态联编
从第3节中我们看到了静态联编的不足(大概教材中就是这样说的)。
刚才我讲了,
由于没有另外的数据结构来保存draw的地址,所以程序所知道的,必然之后在shape类中的draw地址了,仅仅用一个跳转指令
所以我们想实现动态编联,其实就是想弄出个数据结构来,这个数据结构用来存放 映射关系,也就是联编。
所以c++才搞了个虚函数。其实虚函数的本质就是搞了个数据结构。也就是虚函数表
关于虚函数表,自己不想多扯了,扯出来又是一大串,大家自己去下面的博客学习吧。
http://blog.csdn.NET/haoel/article/details/1948051/
4.1虚函数
大家都不喜欢理解概念。我也不喜欢。但是概念毕竟是要紧的。我已经跟大家说明了为什么要引入虚函数。所以接下来请大家耐心看看下面这段概念吧:
虚函数是动态联编的基础;虚函数是成员函数,而且是非静态的成员函数;虚函数在派生类中可能有不同的实现,当使用这个成员函数操作指针或引用所标识的对象时,对该成员函数的调用采用动态联编方式,即:在程序运行时进行关联或束定调用关系;
动态联编只能通过指针或引用标识对象来操作虚函数;如果采用一般的标识对象来操作虚函数,将采用静态联编的方式调用虚函数;
如果一个类具有虚函数,那么编译器就会为这个类的对象定义一个指针成员,并让这个指针成员指向一个表格,这个表格里面存放的是类的虚函数的入口地址;比如:一个基类里面有一些虚函数,那么这个基类就拥有这样一个表,它里面存放了自己的虚函数的入口地址,其派生类继承了这个虚函数表,如果在派生类中重写/覆盖/修改了基类中的虚函数,那么编译器就会把虚函数表中的函数入口地址修改成派生类中的对应虚函数的入口地址;这就为类的多态性的实现提供了基础;
虚函数按照其声明顺序存放于虚函数表中;
父类的虚函数存放在子类虚函数的前面;
多继承中,每个父类都有自己的虚函数表;
子类的成员函数被存放于第一个父类的虚函数表中;
4.2动态联编举例
最后给大家举个例子,在第三节的那个例子上,下面的代码仅仅是多了一个词:virtual
- #include <iostream>
- using namespace std;
- class shape{
- public:
- void virtual draw(){cout<<"I am shape"<<endl;}//这里设定了draw是虚函数
- void fun(){draw();}
- };
- class circle:public shape{
- public:
- void draw(){cout<<"I am circle"<<endl;}//虽然没有说明circle类中的draw是虚函数,但是circle其实继承了virtual性质
- };
- void main(){
- circle oneshape;
- oneshape.fun();
- }
现在我们再来运行一下程序,输出就变成了I am circle
这里的反汇编代码贴出来,自己有空看看:
- 00CC15A0 push ebp
- 00CC15A1 mov ebp,esp
- 00CC15A3 sub esp,0CCh
- 00CC15A9 push ebx
- 00CC15AA push esi
- 00CC15AB push edi
- 00CC15AC push ecx
- 00CC15AD lea edi,[ebp-0CCh]
- 00CC15B3 mov ecx,33h
- 00CC15B8 mov eax,0CCCCCCCCh
- 00CC15BD rep stos dword ptr es:[edi]
- 00CC15BF pop ecx
- 00CC15C0 mov dword ptr [ebp-8],ecx
- 00CC15C3 mov eax,dword ptr [this]
- 00CC15C6 mov edx,dword ptr [eax]
- 00CC15C8 mov esi,esp
- 00CC15CA mov ecx,dword ptr [this]
- 00CC15CD mov eax,dword ptr [edx]
- 00CC15CF call eax
- 00CC15D1 cmp esi,esp
- 00CC15D3 call @ILT+440(__RTC_CheckEsp) (0CC11BDh)
- 00CC15D8 pop edi
- 00CC15D9 pop esi
- 00CC15DA pop ebx
- 00CC15DB add esp,0CCh
- 00CC15E1 cmp ebp,esp
- 00CC15E3 call @ILT+440(__RTC_CheckEsp) (0CC11BDh)
- 00CC15E8 mov esp,ebp
- 00CC15EA pop ebp
- 00CC15EB ret
等自己看了虚函数表之后再来好好理解下。