解析动态联编
- 格式:doc
- 大小:66.50 KB
- 文档页数:8
动态联编实现原理分析所谓动态联编,是指被调函数入口地址是在运行时、而不是在编译时决定的。
C++语言利用动态联编来完成虚函数调用。
C++标准并没有规定如何实现动态联编,但大多数的C++编译器都是通过虚指针(vptr)和虚函数表(vtable)来实现动态联编。
基本的思路是:(1)为每一个包含虚函数的类建立一个虚函数表,虚函数表的每一个表项存放的是个虚函数在内存中的入口地址;(2)在该类的每个对象中设置一个指向虚函数表的指针,在调用虚函数时,先采用虚指针找到虚函数表,确定虚函数的入口地址在表中的位置,获取入口地址完成调用。
我们将从以下几个方面来考察动态联编的实现细节。
1.虚指针(vptr)的存放位置虚指针是作为对象的一部分存放在对象的空间中。
一个类只有一个虚函数表,因此类的所有对象中的虚指针都指向同一个地方。
在不同的编译器中,虚指针在对象中的位置时不同的。
两种典型的做法是:(1)在Visual C++中,虚指针位于对象的起始位置;(2)在GNU C++中,虚指针位于对象的尾部而不是头部。
可通过下面的程序考察在Visual C++中,虚指针在对象中的位置。
#include<iostream>usingnamespace std;int globalv;class NoVirtual{int i;public:void func(){cout<<"no virtual function"<<endl;}NoVirtual(){i=++globalv;}};class HaveVirtual:public NoVirtual{public:virtualvoid func(){cout<<"Virtual Function"<<endl;}};int main(){NoVirtual n1, n2;HaveVirtual h1, h2;unsignedlong* p;cout<<"sizeof(NoVirtual):"<<sizeof(NoVirtual)<<endl;cout<<"sizeof(HaveVirtual):"<<sizeof(HaveVirtual)<<endl;p=reinterpret_cast<unsignedlong*>(&n1);cout<<"first 4 bytes of n1:"<<p[0]<<endl;p=reinterpret_cast<unsignedlong*>(&n2);cout<<"first 4 bytes of n2:"<<p[0]<<endl;p=reinterpret_cast<unsignedlong*>(&h1);cout<<"first 4 bytes of h1: 0x"<<hex<<p[0]<<endl;p=reinterpret_cast<unsignedlong*>(&h2);cout<<"first 4 bytes of h2: 0x"<<hex<<p[0]<<endl;}程序运行结果:从程序的输出结果中,可以得出以下两个结论。
二级C++笔试-280(总分98, 做题时间90分钟)一、选择题下列各题A) 、B) 、C) 、D) 四个选项中,只有一个选项是正确的。
1.结构化程序设计主要强调的是SSS_SINGLE_SELA 程序的规模B 程序的效率C 程序设计语言的先进性D 程序易读性分值: 2答案:D[解析] 结构化程序设计方法的主要原则可以概括为自顶向下、逐步求精、模块化及限制使用 goto语句,总的来说可使程序结构良好、易读、易理解、易维护。
2.一个函数功能不太复杂但要求被频繁调用,应选用SSS_SINGLE_SELA 内联函数B 重载函数C 递归函数D 嵌套函数分值: 2答案:A[解析] 本题考查的是内联函数的使用,它的引入是为了解决程序中函数调用的效率问题,在编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来替换。
3.下列不是描述类的成员函数的是SSS_SINGLE_SELA 构造函数B 析构函数C 友元函数D 复制构造函数分值: 2答案:C[解析] 友元函数是独立于当前类的外部函数,但它可以访问该类的所有对象的成员;拷贝构造函数是使用已存在的对象初始化正在生成的对象时调用的成员函数。
4.在数据管理技术的发展过程中,经历了人工管理阶段、文件系统阶段和数据库系统阶段。
其中数据独立性最高的阶段是SSS_SINGLE_SELA 数据库系统B 文件系统C 人工管D 数据项管理分值: 2答案:A[解析] 人工管理阶段是在20世纪50年代中期以前出现的,数据不独立,完全依赖于程序;文件系统是数据库系统发展的初级阶段,数据独立性差;数据库系统具有高度的物理独立性和一定的逻辑独立性。
5.在数据库设计中,将E-R图转换成关系数据模型的过程属于SSS_SINGLE_SELA 需求分析阶段B 逻辑设计阶段C 概念设计阶段D 物理设计阶段分值: 2答案:B[解析] E-R模型即实体-联系模型,是将现实世界的要求转化成实体、联系、属性等几个基本概念,以及它们之间的两种连接关系。
多态性是C++最主要的特征,多态性的实现得益于C++中的动态联编技术。
文章通过对动态联编的关键技术虚拟函数表进行深入的剖析,解析的动态联编的过程极其技术要领。
关键字多态性动态联编 VTABLE 虚函数文章正文一从多态性谈动态联编的必要性在进入主题之前先介绍一下联编的概念。
联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址。
按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。
在编译阶段就将函数实现和函数调用关联起来称之为静态联编,静态联编在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。
反之在程序执行的时候才进行这种关联称之为动态联编,动态联编对成员函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。
C语言中,所有的联编都是静态联编。
C++中一般情况下联编也是静态联编,但是一旦涉及到多态性和虚函数就必须使用动态联编。
多态性是面向对象的核心,它的最主要的思想就是可以采用多种形式的能力,通过一个用户名字或者用户接口完成不同的实现。
通常多态性被简单的描述为"一个接口,多个实现。
在C++里面具体的表现为通过基类指针访问派生类的函数和方法。
下面我们看一个静态联编的例子,这种静态联编导致了我们不希望的结果。
//1.cpp1. #include <iostream.h>2. class shape{3. public:4. void draw(){cout<<"I am shape"<<endl;}5. void fun(){draw();}6. };7. class circle:public shape{8. public:9. void draw(){cout<<"I am circle"<<endl;}10. };11. main(){12. class circle oneshape;13. oneshape.fun();14. }程序的输出结果我们希望是"I am circle",但事实上却输出了"I am shape"的结果,造成这个结果的原因是静态联编。
继承和派生一、选择题1. 在C++中的类有两种用法:一种是类的实例化,即生成类的对象,并参与系统的运行;另一种是通过(),派生出新的类。
A、复用B、继承C、单继承D、多继承答案:B2. 继承具有(),即当基类本身也是某一个类的派生类时,底层的派生类也会自动继承间接基类的成员。
A、规律性B、传递性C、重复性D、多样性答案:B3. 对于虚函数的调用()A、一定使用动态联编B、一定使用静态联编C、必须使用动态联编D、不一定使用动态联编解析:虚函数只是实现动态联编的必要条件之一,只有在同时满足下面条件时,对虚函数的调用才是动态联编:动态联编调用才是动态联编:(1)类之间为基类和派生类关系(2)要有虚函数(1)类之间为基类和派生类关系(2)要有虚函数(3)调用虚函数操作的是对象的指针或者对象引用,或者是由成员函数调用虚函数。
如果不能同时满足以上条件,对虚函数的调用将采取静态联编。
4.下列描述中,()是抽象类的特征。
A、可以说明虚函数B、可以定义友元函数C、可以进行构造函数重载D、不能说明其对象解析:带有纯虚函数的类称为抽象类。
抽象类中的纯虚函数没有具体的定义,所以不能说明抽象类的对象。
这也是抽象类的特性。
答案:D5. 在多继承中,公有派生和私有派生对于基类成员在派生类中的可访问性与单继承的规则()。
A、完全相同B、完全不同C、部分相同,部分不同D、以上都不对答案:A6. 实现运行时的多态性要使用()A、构造函数B、析构函数C、重载函数D、虚函数解析:动态联编一直要到程序运行时才能确定调用哪个函数。
虚函数是实现动态联编的必要条件之一,没有虚函数一定不能实现动态联编。
答案:D7. 派生类的对象对它的基类成员中()中可以访问的。
A、公有继承的公有成员B、公有继承的私有成员C、公有继承的保护成员D、私有继承的公有成员解析:派生类的对象只能访问公有成员,而基类中的公有成员在公有继承方式下成为派生类的公有成员。
8. 派生类的构造函数的成员初始化列表中,不能包含()。
C++动态联编和静态联编C++ 动态联编和静态联编本⽂较长,⾮常详细,主要关于动态联编、静态联编和虚函数。
建议前置阅读当你写了⼀个函数,程序运⾏时,编译器会如何执⾏你的函数呢?什么是联编?你会认为这个问题很弱智,代码怎么写的编译器就怎么执⾏呗?这对于C语⾔来说是成⽴的,因为每⼀个函数名都对应⼀个不同的函数。
但是C++由于引⼊了重载、重写,⼀个函数名可能对应多个不同的函数。
编译器必须查看函数参数以及函数名才能确定具体执⾏哪个函数。
将源代码中的函数调⽤解释为执⾏特定的函数代码块的过程称为函数名联编。
意思就是,同⼀个名称的函数有多种,联编就是把调⽤和具体的实现进⾏链接映射的操作。
⽽联编中,C++编译器在编译过程中完成的编译叫做静态联编。
静态联编静态联编⼯作是在程序编译连接阶段进⾏的,这种联编⼜称为早期联编,因为这种联编实在程序开始运⾏之前完成的。
在程序编译阶段进⾏的这种联编在编译时就解决了程序的操作调⽤与执⾏该操作代码间的关系。
但是重载、重写、虚函数使得这项⼯作变得困难。
因为编译器不知道⽤户将选择哪种类型的对象,执⾏具体哪⼀块代码。
所以,编译器必须⽣成能够在程序运⾏时选择正确的虚函数的代码,这个过程被称为动态联编,⼜称晚期联编。
动态联编编译程序在编译阶段并不能确切地指导将要调⽤的函数,只有在程序执⾏时才能确定将要调⽤的函数,为此要确切地指导将要调⽤的函数,要求联编⼯作在程序运⾏时进⾏,这种在程序运⾏时进⾏的联编⼯作被称为动态联编,或称动态束定,⼜叫晚期联编。
为了深⼊的探讨联编的内容,我们先从指针、引⽤和虚函数开始讲起。
指针和引⽤的兼容性C++中,动态联编与指针、引⽤调⽤⽅法息息相关。
从某个⾓度⽽⾔,动态联编的产⽣和继承如影随形。
继承是如何处理指向对象的指针、引⽤我们已经有所讨论。
通常,C++不允许将⼀种类型的地址赋给另⼀种类型的指针。
也不允许⼀种类型的引⽤指向另⼀种类型。
例如double x = 3.14;int* pi = &x; // 不可以,int指针不可以指向doublelong& ri = x; // 不可以,long引⽤不可以引⽤double但是,就像我们之前讨论的,指向基类的引⽤或指针可以引⽤派⽣类对象,⽽不需要进⾏显式转换。
多态性是C++最主要的特征,多态性的实现得益于C++中的动态联编技术。
文章通过对动态联编的关键技术虚拟函数表进行深入的剖析,解析的动态联编的过程极其技术要领。
关键字多态性动态联编 VTABLE 虚函数文章正文一从多态性谈动态联编的必要性在进入主题之前先介绍一下联编的概念。
联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址。
按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。
在编译阶段就将函数实现和函数调用关联起来称之为静态联编,静态联编在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。
反之在程序执行的时候才进行这种关联称之为动态联编,动态联编对成员函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。
C语言中,所有的联编都是静态联编。
C++中一般情况下联编也是静态联编,但是一旦涉及到多态性和虚函数就必须使用动态联编。
多态性是面向对象的核心,它的最主要的思想就是可以采用多种形式的能力,通过一个用户名字或者用户接口完成不同的实现。
通常多态性被简单的描述为"一个接口,多个实现。
在C++里面具体的表现为通过基类指针访问派生类的函数和方法。
下面我们看一个静态联编的例子,这种静态联编导致了我们不希望的结果。
//1.cpp1. #include <iostream.h>2. class shape{3. public:4. void draw(){cout<<"I am shape"<<endl;}5. void fun(){draw();}6. };7. class circle:public shape{8. public:9. void draw(){cout<<"I am circle"<<endl;}10. };11. main(){12. class circle oneshape;13. oneshape.fun();14. }程序的输出结果我们希望是"I am circle",但事实上却输出了"I am shape"的结果,造成这个结果的原因是静态联编。
静态联编需要在编译时候就确定函数的实现,但事实上编译器在仅仅知道shape的地址时候无法获取正确的调用函数,它所知道的仅是shape::draw(),最终结果只能是draw操作束缚到shape类上。
产生"I am shape"的结果就不足为奇了。
为了能够引起动态联编,我们只需要将需要动态联编的函数声明为虚函数即可。
动态联编只对虚函数起作用。
我们在通过基类而且只有通过基类访问派生类的时候,只要这个基类中直接的或者间接(从上上层继承)的包含虚函数,动态联编将自动唤醒。
下面我们将上面的程序稍微改一下。
//2.cpp1. #include <iostream.h>2. class shape{3. public:4. virtual void draw(){cout<<"I am shape"<<endl;}5. void fun(){draw();}6. };7. class circle:public shape{8. public:9. void draw(){cout<<"I am circle"<<endl;}10. };11. main(){12. class circle oneshape;13. fun(&oneshape);14. }程序执行得到了正确的结果"I am circle"。
代码在VC6.0中执行。
到目前为止我们不清楚动态联编的执行机制,但我们可以做个猜测。
正如上面所说,对于函数的实际的对象类型不同,联编结果也应该不同。
在静态联编中,执行的困难在于无法通过基类知道需要联编的子对象的确切类型。
在1.cpp中shape的派生类既可能是circle,也可能是其余的rectangle或者square等等,到底应该静态联编哪一个呢。
迷惑正在于此。
动态联编在编译的时候应该也是不知道联编的确切对象类型的,(如果知道的话就成了静态联编了),因此它只能通过一定的机制,使得在执行时候能够找到和调用正确的函数体。
可以想象,为了达到这个目的,一些相关信息应该封装在对象自身中。
这些信息有点象身份证明,标识自己,这样在动态联编的时候,编译器可以根据这些标记找到相应的函数体,"不要跑,就是你了"。
实际上的动态联编过程是什么样的呢。
二对象类型信息为了证明我们的猜想,我们用下面的一个程序进行测试,下面的程序将获取普通的类和包含虚函数的类的字节大小。
程序代码如下。
//3.cpp1. #include <iostream.h>2. class shape_novirtual{3. int a;4. public:5. void draw(){cout<<"shape_novirtual::draw()"<<endl;}6. };7. class shape_virtual1{8. int a;9. public:10. virtual void draw(){cout<<"shape_virtual::draw()"<<endl;}11. };12. class shape_virtual2{13. int a;14. public:15. virtual void draw(){cout<<"shape_virtual2::draw()"<<endl;}16. virtual void draw1(){cout<<"shape_virtual2::draw1()"<<endl;}17. };18. main(){19. cout<<"sizeof(int)"<<sizeof(int)<<endl;20. cout<<"sizeof(classshape_novirtual):"<<sizeof(shape_novirtual)<<endl;21. cout<<"sizeof(void*):"<<sizeof(void*)<<endl;22. cout<<"sizeof(classshape_virtual):"<<sizeof(shape_virtual)<<endl;23. cout<<"sizeof(classshape_virtual2):"<<sizeof(shape_virtual2)<<endl;24. }VC6.0中运行结果如下:sizeof(int)4sizeof(class shape_novirtual):4sizeof(void*):4sizeof(class shape_virtual1):8sizeof(class shape_virtual2):8Press any key to continue从上面可以看出,没有虚函数的类shape_novirtual的大小为4,正好为int a 的大小。
而带有虚函数的类shape_virtual1和shape_virtual2的大小除了int a的大小还多出了4格个字节的大小,这个大小正好是void*指针的大小。
到现在为止我们基本上可以说带有虚函数的对象自身确实插入了一些指针信息,而且这个指针信息并不随着虚函数的增加而增大。
如果我们将每个类的成员变量int a去掉,VC6.0运行结果就会变成下面的情况。
sizeof(int)4sizeof(class shape_novirtual):1sizeof(void*):4sizeof(class shape_virtual1):4sizeof(class shape_virtual2):4Press any key to continue上面的运行结果应该让人感到例外。
既然size(int)为4,现在没有了这个成员变量,类shape_novirtual应该字节大小为0,但事实上C++编译器不允许对象为零长度。
试想一个长度为0的对象在内存中怎么存放?怎么获取它的地址?为了避免这种情况,C++强制给这种类插入一个缺省成员,长度为1。
如果有自定义的变量,变量将取代这个缺省成员。
三虚函数表VTABLE动态联编过程跟我们猜测的大致相同。
编译器在执行过程中遇到virtual关键字的时候,将自动安装动态联编需要的机制,首先为这些包含virtual函数的类(注意不是类的实例)--即使是祖先类包含虚函数而本身没有--建立一张虚拟函数表VTABLE。
在这些虚拟函数表中,编译器将依次按照函数声明次序放置类的特定虚函数的地址。
同时在每个带有虚函数的类中放置一个称之为vpointer的指针,简称vptr,这个指针指向这个类的VTABLE。
关于虚拟函数表,有几点必须声明清楚:1. 每一个类别只能有一个虚拟函数表,如果该类没有虚拟函数,则不存在虚拟函数表。
2. C++编译时候编译器会在含有虚函数的类中加上一个指向虚拟函数表的指针vptr。
3. 从一个类别诞生的每一个对象,将获取该类别中的vptr指针,这个指针同样指向类的VTABLE。
因此类、对象、VTABLE的层次结构可以用下图表示。
其中X类和Y类的对象的指针都指向了X,Y的虚拟函数表,同时X,Y类自身也包含了指向虚拟函数的指针。
为了方便问题说明,我们将2.cpp例子进行扩展,扩展程序如下。
//4.cpp15. #include <iostream.h >16. class shape{17. public:18. virtual void draw(){cout<<"shape::draw()"<<endl;}19. virtual void area(){cout<<"shape::area()"<<endl;}20. void fun(){draw();area();}21. };22. class circle:public shape{23. public:24. void draw(){cout<<"circle::draw()"<<endl;}25. void adjust(){cout<<"circle::adjust()"<<endl;}26. };27. main(){28. shape oneshape;29. oneshape.fun();30.31. circle circleshape;32. shape& baseshape=circleshape;33. baseshape.fun();34. }编译器在编译上面这段代码的时候将为这shape和circle两个对象分别建立一个VTABLE表,这些表依次填充派生类对象和基类对象中声明的所有的虚函数地址。