范文一:动态联编实现原理分析
动态联编实现原理分析
所谓动态联编,是指被调函数入口地址是在运行时、而不是在编译时决定的。 C++语言 利用动态联编来完成虚函数调用。 C++标准并没有规定如何实现动态联编,但大多数的 C++编译器都是通过虚指针(vptr )和虚函数表(vtable )来实现动态联编。
基本的思路是:
(1)为每一个包含虚函数的类建立一个虚函数表,虚函数表的每一个表项存放的是个 虚函数在内存中的入口地址;
(2)在该类的每个对象中设置一个指向虚函数表的指针,在调用虚函数时,先采用虚 指针找到虚函数表,确定虚函数的入口地址在表中的位置,获取入口地址完成调用。 我们将从以下几个方面来考察动态联编的实现细节。
1. 虚指针(vptr )的存放位置
虚指针是作为对象的一部分存放在对象的空间中。 一个类只有一个虚函数表, 因此类的 所有对象中的虚指针都指向同一个地方。 在不同的编译器中, 虚指针在对象中的位置时不同 的。两种典型的做法是:
(1)在 Visual C++中,虚指针位于对象的起始位置;
(2)在 GNU C++中,虚指针位于对象的尾部而不是头部。
可通过下面的程序考察在 Visual C++中,虚指针在对象中的位置。
#include usingnamespace std; int globalv; class NoVirtual { int i; public : void func(){ cout } NoVirtual(){ i=++globalv; } }; class HaveVirtual :public NoVirtual { public : virtualvoid func(){ cout } }; int main(){ NoVirtual n1, n2; unsignedlong * p; cout cout p=reinterpret_cast cout p=reinterpret_cast cout p=reinterpret_cast cout p=reinterpret_cast cout } 程序运行结果: 从程序的输出结果中,可以得出以下两个结论。 (1)可以清楚地的看到虚指针对类对象大小的影响。类 NoVirtual 不包含虚函数,因此 类 NoVirtual 的对象中只包含数据成员 i ,所以 sizeof (NoVirtual )为 4。类 HaveVirtual 包含 虚函数,因此类 HaveVirtual 的对象不近要包含数据成员 i ,还要包含一个指向虚函数表的指 针(大小为 4B ) ,所以 sizeof(HaveVirtual)为 8。 (2)虚指针如果不在对象的头部,那么对象 h1和对象 h2的头 4个字节(代表整型成 员变量 i )的值应该是 3和 4。而程序结果显示,类 HaveVirtual 的两个对象 h1和 h2的头 4个字节的内容相同,这个值就是类 HaveVirtual 的虚函数表所在地址。 2. 虚函数表(vtable )的内部结构 虚函数表是为拥有虚函数的类准备的。 虚函数表中存放的是类的各个虚函数的入口地址。 那么,可以思考以下几个问题: (1)虚函数的入口地址是按照什么顺序存放在虚函数表中的呢? (2)不同的类(比如说父类和子类)是否可以共享同一张虚函数表的呢? (3)虚函数表是一个类的对象共享,还是一个对象就拥有一个虚函数表? (4)多重继承的情况下,派生类有多少个虚函数表呢? 考察如下程序: #include usingnamespace std; #defineShowFuncAddress (function) _asm{\ _asm{mov p,eax}\ cout void showVtableContent(char * className , void * pObj , int index ){ unsignedlong * pAddr=NULL ; pAddr=reinterpret_cast pAddr=(unsignedlong *)*pAddr; //获取虚函数表指针 cout cout } class Base { int i; public : virtualvoid f1(){ cout } virtualvoid f2(){ cout } virtualvoid f3(){ cout } }; class Derived :public Base { int i; public : virtualvoid f4(){ cout } void f3(){ cout } void f1(){ cout } }; void func(){ cout } int main(){ Derived d; void *p; unsignedlong *pAddr; pAddr=reinterpret_cast cout *)*pAddr<> pAddr=reinterpret_cast cout ShowFuncAddress (Base ::f1); showVtableContent( ShowFuncAddress (Base ::f2); showVtableContent( ShowFuncAddress (Base ::f3); showVtableContent( ShowFuncAddress (Derived ::f1); showVtableContent( ShowFuncAddress (Derived ::f2); showVtableContent( ShowFuncAddress (Derived ::f3); showVtableContent( ShowFuncAddress (Derived ::f4); showVtableContent( } 程序运行结果: 代码相关说明: C++规定,类的静态成员函数和全局函数可以直接通过函数名或类名 ::函数名来获取函 数的入口地址。 但是,对于类的非静态成员函数, 不可以直接获取类成员函数的地址, 需要 利用内联汇编来获取成员函数的入口地址或者用 union 类型来逃避 C++的类型转换检测。两 种方法都是利用了某种机制逃避 C++的类型转换检测,为什么 C++编译器干脆不直接放开这 个限制, 一切让程序员自己作主呢?当然是有原因的, 因为类成员函数和普通函数还是有区 别的,允许转换后,很容易出错。 因此,在程序中使用了宏 ShowFuncAddress ,利用内联汇编来获取类的非静态成员函数 的入口地址。 这是一个带参数的宏, 并且对宏的参数做了一些特殊处理, 如字符串化的处理。 程序结果说明: (1)基类 Base 虚函数表的地址与派生类 Derived 的虚函数表的地址是不同的,尽管类 Base 是类 Derived 的父类,但它们却各自使用不同的虚函数表。可见,所有的类都不会和其他的 类共享同一张虚函数表。 (2)对任意包含虚函数的类,将虚函数的入口地址写入虚函数表,按照如下的步骤进行: a. 确定当前类所包含的虚函数个数。一个类的虚函数有两个来源,一是继承自父类(在当前 类中可能被改写) ,其他的是在当前类中新申明的虚函数。 b. 为所有虚函数排序。继承自父类的所有虚函数,排在当前类新生命的虚函数之前。心声明 的虚函数,按照在当前类中申明的顺序排列。 c. 确定虚函数的入口地址。继承自父类的虚函数,如果在当前类中被改写,则虚函数的入口 地址是改写之后的函数的地址, 否则保留父类中的虚函数的入口地址。 新声明的虚函数, 其 入口地址就是在当前类中的函数的入口地址。 d. 将所有虚函数的入口地址按照排定的次序写入虚函数表中。 (3)虚函数表是一个类的所有对象共享,而不是一个对象就拥有一个虚函数表,读者可自 行验证。 以上代码描述的是单继承情况下父类和子类的虚函数表在内存中的结构, 直观的图示描 述如下: 注意:在上面这个图中, 我在虚函数表的最后多加了一个结点, 这是虚函数表的结束结 点,就像字符串的结束符“ \0”一样,其标志了虚函数表的结束。这个结束标志的值在不同 的编译器下是不同的。 在 Visual C++下, 这个值是 NULL 。 而在 GNU C++下, 这个值是如果 1, 表示还有下一个虚函数表,如果值是 0,表示是最后一个虚函数表。 (4)多重继承的情况下,派生类有多少个虚函数表呢? 子类如果继承了多个父类, 并重写了继承而来的虚函数, 下面是对于子类实例中的虚函 数表的图: 其 它 成 员 &b 我们可以看见,子类有多少个父类,就有多少个虚函数表。三个父类虚函数表中的 f()的位置被替换成了子类的函数。 这样, 我们就可以以任一静态类型的父类来指向子类, 并调 用子类的 f()了。如: Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g() 3. 虚函数表(vtable )的放在哪里 虚函数表放在应用程序的常量区。 将上面的代码编译之后生成汇编代码文件, 查看 .asm 文件可以发现这样两端内容: CONST SEGMENT ??_7Base@@6B@ DD FLAT:??_R4Base@@6B@ ; Base::`vftable’ DD FLAT:?f1@Base@@UAEXXZ DD FLAT:?f2@Base@@UAEXXZ DD FLAT:?f3@Base@@UAEXXZ CONST ENDS CONST SEGMENT ??_7Derived@@6B@ DD FLAT:??_R4Derived@@6B@ ; Derived::`vftable’ DD FLAT:?f1@Derived@@UAEXXZ DD FLAT:?f2@Base@@UAEXXZ DD FLAT:?f3@Derived@@UAEXXZ DD FLAT:?f4@Derived@@UAEXXZ CONST ENDS 这里说明一下如何在 VS2012中生成汇编代码文件。需要进行如下设置: 项目 ---》 属性 ---》 配置属性 ---》 c/c++ ---》 输出文件 ---》 右边内容项:汇编输出 ---》 带源代码的程序集(/Fas ) 。 这样在项目里面生成后缀为 *.asm 的文件。里面还有注释,有利于分析。 从汇编代码可以看出, 这是两个常量段, 其中分别存放了 Base 类的虚函数表和 Derived 类的虚函数表。从中可以发现,虚函数表中的每一项代表了一个函数的入口地址,类型是 Double Word 。类中每个虚函数的入口地址在虚函数表中的排放顺序,也可以从相应的标识 符看出。 4. 通过访问虚函数表手动调用虚函数 既然知道了虚函数表的位置和结构,那么就可以通过访问虚函数表,手动调用虚函数。 虽然在利用 C++编写程序时没有必要这样做,但如果想了解动态联编的实现机理,请参考如 下代码: #include usingnamespace std; typedefvoid (*pFunc )(); void executeVirtualFunc(void * pObj , int index ){ pFunc p; unsignedlong * pAddr; pAddr=reinterpret_cast pAddr=(unsignedlong *)*pAddr; //获取虚函数表地址 p=(pFunc )pAddr[index ]; //获取虚函数入口地址 _asm mov ecx, pObj p(); //实施函数调用 } class Base { int i; public : Base(){i=0;} virtualvoid f1(){ cout } virtualvoid f2(){ cout } virtualvoid f3(){ cout } }; class Derived :public Base { int j; public : Derived(){j=1;} virtualvoid f4(){ cout } void f3(){ cout } void f1(){ cout } }; int main(){ Base b; Derived d; executeVirtualFunc(&b,1); executeVirtualFunc(&d,3); } 执行 executeVirtualFunc(&b,1);就是调用基类对象 b 的第二个虚函数 (b.f2()),执行 executeVirtualFunc(&d,3);就是调用子类 d 的第四个虚函数 (d.f4())。程序的输出结果是: Base's f2() Derived's f4(),j=1 结果表明,成功的对不同对象上的不同虚函数实现了调用。这些调用是通过访问每个对象 虚函数表来实现的。 由于在调用类对象的非静态成员函数时, 必须同时给出对象的首地址, 所以 在程序中使用了内联汇编代码 _asm mov ecx,pObj; 来达到这个目的。在 Visual C++中,在调用类 的费静态成员函数之前,对象的首地址都是送往寄存器 ecx 的。 联 编 是 指 一 个 程 序 自 身 彼 此 关 联 的 过 程 。 按 照 联 编 所 进行的阶段不同,可分为静态联编和动态联编。 静 态 联 编 又 称 静 态 绑 定 , 指 在 调 用 同 名 函 数 (即 重 载 函 数 ) 时 编 译 器 将 根 据 调 用 时 所 使 用 的 实 参 在 编 译 时 就 确 定 下 来 应 该 调 用 的 函 数 实 现 。 它 是 在 程 序 编 译 连 接 阶 段 进 行 联 编 的 , 这 种 联 编 又 称 为 早 期 联 编 , 这 是 因 为 这 种 联 编 工 作 是 在 程 序 运 行 之 前 完 成 的 。 它 的 优 点 是 速 度 快 , 效 率 高,但灵活性不够。 编 译 时 所 进 行 的 联 编 又 称 为 静 态 束 定 。 束 定 是 指 确 定 所调用的函数与执行该函数代码之间的关系。 动 态 联 编 也 称 动 态 绑 定 , 是 指 在 程 序 运 行 时 , 根 据 当 时 的 情 况 来 确 定 调 用 的 同 名 函 数 的 实 现 , 实 际 上 就 是 在 运 行时选择虚函数的实现。这种联编又称为晚期联编 或 动 态 ( 束定 。实现条件:①要有继承性且要求创建子类型关系; ) ② 要 有 虚 函 数 ; ③ 通 过 基 类 的 对 象 指 针 或 引 用 访 问 虚 函 数 。 继 承 是 动 态 联 编 的 基 础 , 虚 函 数 是 动 态 联 编 的 关 键 , 虚 函 数 经 过 派 生 之 后 , 在 类 族 中 就 可 以 实 现 运 行 过 程 中 的 多态。 动 态 联 编 要 求 在 运 行 时 解 决 程 序 中 的 函 数 调 用 与 执 行 该 函 数 代 码 间 的 关 系 , 调 用 虚 函 数 的 对 象 是 在 运 行 时 确 定 的 。 对 于 同 一 个 对 象 的 引 用 , 采 用 不 同 的 联 编 方 式 将 会 被 联 编 到 不 同 类 的 对 象 上 。 即 不 同 联 编 可 以 选 择 不 同 的 实 现,这便是多态性。它的优点是灵活性强,但效率较低。 动态、静态联编举例 1 ()静态联编 1程序段如下: #include class Point { public: Point(double i,double j) { x=i;y=j;} double Area() const {return 0.0;} private: double x,y; }; class Rectangle:public Point { public: Rectangle(double i,double j,double k,double l); double Area() const {return w*h;} private: double w,h; }; Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j) { w=k; h=l; } void fun(Point &s) {cout<><> void main() {Rectangle rec(3.5,15.2,5.0,28.0); fun(rec);} 运行结果为 。 ()动态联编 2程序段如下: 先来看一个上面的例子,在 前 面 加 double Area() const 一个 ,看看运行结果有什么不同: virtual #include class Point { public: Point(double i,double j) {x=i;y=j;} virtual double Area() const {return 0.0;} private: double x,y; }; class Rectangle:public Point 浅谈静态联编和动态联编的区别和实现动态联编的条件 杨晓燕 1,董 文 2 中国地质大学研究生院,湖北 武 汉 ; 四川南充西华师范大学计算机科学系,四川 南充 ) (1. 4300742. 637002摘 要:多态性是 ++面向对象语言的特征之一,多态性是要在调用函数时使用对象的指针或引用。另外,多态 C 性仅用于类层次结构,所以能以一具类中派生另一个类不是多态性的基本条件。虚函数是动态联编的基础,它经 过派生之后,在类族中就可以实现运行过程中的多态。动态联编恰是反映 ++语言中的多态性的具体体现。本文 C 先阐述了静态联编和动态联编的概念和区别,而后以几个实例说明实现动态联编的条件。 关键词:++;静态联编;动态联编;多态性;虚函数;条件 C 中图分类号:文献标识码:文章编号: TP311 A 1008-8814(2004)04-0040-03 { public: Rectangle(double i,double j,double k,double l); virtual double Area() const {return w*h;} private: double w,h; } ; Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j) { w=k; h=l;} void fun(Point &s) {cout<><> void main() {Rectangle rec(3.5,15.2,5.0,28.0); fun(rec);} 运行结果为:140 此 例 与 上 例 的 不 同 仅 在 于 该 程 序 中 两 个 类 中 的 Area()函 数 被 说 明 为 虚 函 数 , 而 使 得 程 序 采 用 了 动 态 联 编 , 在 函 数 调用时,由于实参 是 类 的 对 象 , 因 此 在 fun()rec rectangle 运 行 时 确 定 函 数 函 数 体 内 的 函数是 类 fun()Area()Rectangle 的,故输出上述结果。可见,是虚函数改变了联编方式。 动态联编分析 2 ()个函数的形参要求是父类的引用或指针,且派生 1 类是其子类型才行。 例: #include class B { public: virtual void fn() {cout }; class D:public B { public: virtual void fn() {cout }; void text(B &x) { x.fn();} void main() { B b; text(b); D d; text(d);} 其运行结果为: In B. In D. ()多态的虚函数要求函数说明相同,即形参个数、 2 形参类型、返回值类型要求相同,否则不实行动态联编。 例 :#include class B { public: virtual void fun(int i) { cout }; class D:public B { public: virtual void fun(float j) { cout }; void text(B &b) { int a=22; b.fun(a);} void main() { B b; D d; cout text(b); cout text(d);} 其运行结果为: Calling text(b). In B.i=22 Calling text(d). In B.i=22 函数 的形参要求是类 的 引 用 或 指 针 , 同 时 要 求 text()B 类 是类 的子类型才可能进行动态联编。派生类的虚函数 D B 与 基 类 中 对 应 的 虚 函 数 的 参 数 不 同 , 派 生 类 的 虚 函 数 将 丢 失虚特性,而变为重载函数。 ()虚函数不受保护类型的限制,即不管它是什么类 3 的访问级别。 例: #include class B { friend void text(B &); protected: virtual void fun(int i) { cout }; class D:public B { friend void text(B &); private: virtual void fun(int j) { cout }; void text(B &b) { int a=22; b.fun(a);} void main() {B b; D d; cout text(b); cout text(d);} 运行结果为: Calling text(b). In B.i=22 Calling text(d). In D.j=22 说 明 :虚 函 数 的 重 写 与 它 的 具 体 存 储 权 限 没 有 任 何 关 系 , 即 使 声 明 为 的 虚 函 数 , 在 子 类 中 也 同 样 可 以 重 private 写它。 ()使用基类类型的指针时,它指向哪个派生类的对 4 象,就可以通过它访问那个对象的同名虚成员函数。 例:#include class Base { public: virtual void hello(){cout void tryit(){cout }; class Derive:public Base {public: void hello() {cout virtual void tryit() {cout }; class Last:public Derive {public: void tryit() {cout }; void main() Last d; Base*pb=&d; Derive *pd=&d; Base b; b.hello(); b.tryit(); d.hello(); d.tryit(); pb->hello(); pb->tryit(); pd->hello(); pd->tryit();} 运行结果如下: Base::hello() Base::tryit() Derive::hello() Last::tryit() Derive::hello() ① Derive::hello() Last::tryit() 注意:①易出错,此时的 Derive::hello() Base*pb = , &d的对象 的地址赋给了 指针, 指针所指的内容则是 Last d pb pb 类 的 内 容 , 那 么 的 执 行 解 释 为 :是 的 Base pb->hello()d Last 对象, 中无 函 数 , 则 函 数 执 行 的 是 其 直 接 Last hello()hello() 基类 的,即 , Derive hello(){cout Last Base 因为类 定义的 不 是 虚 函 数 , 所 以 Base::tryit()(Base tryit 不实行动态联编,则执行父类。 在 遇 到 此 类 问 题 时 , 头 脑 保 持 清 醒 , 分 清 什 么 函 数 是 虚函数,类之间继承的关系等要具体分析透彻。 实现动态联编的关键是存在虚函数,虚函数应具有的 特性: ()求派生类中的虚函数与基类中的虚函数具有相同 1 的参数个数、对应的参数类型相同和返回值类型相同。 ()基类中说明的虚函数具有自动向下传给它的派生 2 类的性质。 ()只有非静态的成员函数才可以说明为虚函数。 3 () 构造函数不能说明为虚函数,因为对象在创建时 4 它还没有确定内存空间,只有在构造后才是一个类的实例。 析构函数可以是虚函数,并且最好在动态联编时被说明 为虚函数。 参 考 文 献: 吕凤翥 语言程序设计 北京 电子工业出版社 [1] .C++[M].:,2002. 侯俊杰 深入浅出同 武汉 华中科技大学出版社 [2] .MFC[M].:,2004. 吕凤翥 语言程序设计 北京 清华大学出版社 北方交通大 [3] .C++[M].:, 学出版社 ,2003. 杨 庚 面 向 对 象 程 序 设 计 与 语言 西安 西安电子科技大学 [4] .C++[M].: 出版社 ,2002. 北京 机械工业出版社 [5] [Eckel] Bruce Eckel.Thinking in C++[M].:, 2000. (美)著,李予敏等译 入门经典 北京 清 华 [6] Ivor Horton.C++[M].:大学出版社 ,2004. Discussing to AccompLishing Condition of Static Binding and Dynamic Binding YANG Xiao-Yan1, DONG Wen2 (1.China University of Geosciences in wuhan, Wuhan 430074, China; 2. China West normal University in Nanchong of Sichuan, Nanchong 637002, China) Abstract: Polymorphism is one of characters of C++ Object-Oriented Programming Language. Inheritance is basement of Virtual Functions, which is basic of dynamic binding. After deriving, Polymorphism is accomplished on the course of programme running. Dynamic binding fully reflects polymorphism. This thesis discusses concept and difference of static binding and dynamic binding. It describes condition of realizing dynamic binding. Key words: Static binding; Dynamic binding; Realizing condition; Virtual functions; C++ 程序所实现的功能: 定义一个抽象类 CShape ,包含纯虚函数 Area(用来计算面积 ) 和 SetData(用来重设形状大 小 ) 。然后派生出三角形 CTriangle 类、矩形 CRect 类、圆 CCircle 类,分别求其面积。最 后通过友元的方式计算这几个形状对象的面积之和。 源程序: #include using namespace std; class CRect; class CCircle; class CShape { public : virtual float area() = 0; virtual void setdata() = 0; }; class CTriangle:public CShape { private : float m_boot, m_high; public : void setdata() { cout cin>>m_boot>>m_high; } float area() { return (float )(m_boot * m_high / 2); } friend void areaplus(CTriangle tiangle, CRect rect, CCircle circle); }; class CRect:public CShape { float m_long, m_wide; public : void setdata() { cout cin>>m_long>>m_wide; } float area() { return (float )(m_long * m_wide); } }; class CCircle:public CShape { float m_re; public : void setdata() { cout cin>>m_re; } float area() { return (float )(3.14 * m_re * m_re); } friend void areaplus(CShape * shape[], int len); }; void areaplus(CShape * shape[], int len) { cout } void main(void ) { CShape *shape[3] = {new CTriangle, new CRect, new CCircle}; int choose, flag1 = 1, flag[3] = {0, 0, 0}; cout cout ê? while (flag1) { cout<> cin>>choose; switch (choose) { case 1: shape[0]->setdata(); flag[0] = 1; break ; case 2: shape[1]->setdata(); break ; case 3: shape[2]->setdata(); flag[2] = 1; break ; case 4: if (flag[0] == 0) { cout<> else { cout<> if (flag[1] == 0) { cout } else { cout } if (flag[2] == 0) { cout } else { cout } break ; case 5: areaplus(shape, 3); break ; case 6: flag1 = 0; default : cout } } delete []shape; } 一 从多态性谈动态联编的必要性 在进入主题之前先介绍一下联编的概念。联编就是将模块或者函数合并在一起生成可 执行 代码的处理过程, 同时对每个模块或者函数调用分配内存地址, 并且对外部访问也分配正确 的内存地址。 按照联编所进行的阶段不同, 可分为两种不同的联编方法:静态联编和动态联 编。 在编译阶段就将函数实现和函数调用关联起来称之为静态联编, 静态联编在编译阶段就 必须了解所有的函数或模块执行所需要检测的信息, 它对函数的选择是基于指向对象的指针 (或者引用) 的类型。 反之在程序执行的时候才进行这种关联称之为动态联编, 动态联编对 成员函数的选择不是基于指针或者引用, 而是基于对象类型, 不同的对象类型将做出不同的 编译结果。 C 语言中,所有的联编都是静态联编。 C++中一般情况下联编也是静态联编,但 是一旦涉及到多态性和虚函数就必须使用动态联编。 多态性是面向对象的核心, 它的最主要的思想就是可以采用多种形式的能力, 通过一个用户 名字或者用户接口完成不同的实现。通常多态性被简单的描述为 下面我们看一个静态联编的例子,这种静态联编导致了我们不希望的结果。 //cpp1. #include class shape { public: void draw(){cout void fun(){draw();} }; class circle:public shape { public: void draw(){cout }; main() { class circle oneshape; oneshape.fun(); } 程序的输出结果我们希望是 //2.cpp1. #include class shape {public: virtual void draw(){cout void fun(){draw();} }; class circle:public shape {public: void draw(){cout }; main() {class circle oneshape; fun(&oneshape); } 程序执行得到了正确的结果 到目前为止我们不清楚动态联编的执行机制, 但我们可以做个猜测。 正如上面所说, 对于函 数的实际的对象类型不同, 联编结果也应该不同。 在静态联编中, 执行的困难在于无法通过 基类知道需要联编的子对象的确切类型。 在 1.cpp 中 shape 的派生类既可能是 circle, 也可 能是其余的 rectangle 或者 square 等等,到底应该静态联编哪一个呢。迷惑正在于此。动 态联编在编译的时候应该也是不知道联编的确切对象类型的, (如果知道的话就成了静态联 编了) , 因此它只能通过一定的机制, 使得在执行时候能够找到和调用正确的函数体。 可以 想象, 为了达到这个目的, 一些相关信息应该封装在对象自身中。 这些信息有点象身份证明, 标识自己, 这样在动态联编的时候, 编译器可以根据这些标记找到相应的函数体, 实际上的动态联编过程是什么样的呢。 二 对象类型信息 为了证明我们的猜想, 我们用下面的一个程序进行测试, 下面的程序将获取普通的类和包含 虚函数的类的字节大小。程序代码如下。 //3.cpp1. #include class shape_novirtual { int a; public: void draw(){cout }; class shape_virtual1 {int a; public: virtual void draw(){cout }; class shape_virtual2 {int a; public: virtual void draw(){cout virtual void draw1(){cout }; main() {cout cout cout cout cout 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。如果有自定义的变量,变量将取代这个缺省成员。 静态联编和动态联编 联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。 静态联编是指在编译阶段就将函数实现和函数调用关联起来,因此静态联编也叫早绑定,在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型,C语言中,所有的联编都是静态联编,据我所知道的,任何一种编译器都支持静态联编(废话)。 动态联编是指在程序执行的时候才将函数实现和函数调用关联,因此也叫运行时绑定或者晚绑定,动态联编对函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。C++中一般情况下联编也是静态联编,但是一旦涉及到多态和虚拟函数就必须要使用动态联编了。下面将介绍一下多态。 多态:字面的含义是具有多种形式或形态。C++多态有两种形式,动态多态和静态多态;动态多态是指一般的多态,是通过类继承和虚函数机制实现的多态;静态多态是通过模板来实现,因为这种多态实在编译时而非运行时,所以称为静态多态。 动态多态例子: #include #include /** *Shape */ classCShape { public: CShape(){} virtual ~CShape(){} virtual void Draw() = 0; }; /** *Point */ classCPoint : public CShape { public: CPoint(){} ~CPoint(){} void Draw() { printf("Hello! I am Point!/n"); } }; /** *Line */ classCLine : public CShape { public: CLine(){} ~CLine(){} void Draw() { printf("Hello! I am Line!/n"); } }; void main() { CShape* shape = new CPoint(); //draw point shape->Draw();//在这里shape将会调用CPoint的Draw()函数 delete shape; shape = new CLine(); //draw Line shape->Draw();//而在这里shape将会调用CLIne的Draw()函数 delete shape; return ; } 大家可以考虑一下它的输出结果,如果实在不知道,那就麻烦你运行一下它吧~ 由上面的例子,大家应该能理解什么是多态了:也就是一个Draw()可以有两种实现,并且是在运行时决定的,在编译阶段不知道,也不可能知道~只有在运行的时候才能知道我们生成的shape是那种图形,当然要实现这种效果就需要动态联编了,在基类我们会把想要多态的函数声明为虚函数,而虚函数的实现原理就使用了动态联编。如果你想了解的更透彻,那就上网查查资料吧~呵呵~在这里我就不再啰嗦虚函数的实现原理了。 顺便在这里也提供一个静态多态的例子: 在上面例子的基础之上添加模板函数: template voidDrawShape(T* t) { t->Draw(); } 修改main函数为如下: void main() { CShape* shape = new CPoint(); //draw point shape->Draw(); DrawShape shape = new CLine(); //draw Line shape->Draw(); DrawShape delete shape; return ; } 在程序编译main函数的时候,编译器就已经指定了DrawShape函数里面的Draw要调用那个实现了,这就是静态多态,在编译时就已经知道了要调用的函数。 转载请注明出处范文大全网 » 动态联编实现原理分析范文二:浅谈静态联编和动态联编的区别和实现动态联编的条件
范文三:动态联编
范文四:动态联编
范文五:静态联编和动态联编