浅析静态绑定和动态绑定

来源:互联网 发布:硬妹风的淘宝店 编辑:程序博客网 时间:2024/05/29 16:27

静态绑定 & 动态绑定

  • 静态绑定(statically bound),又名前期绑定(early binding);
  • 动态绑定(dynamically bound),又名延期绑定(late binding)。

ps 英文名称摘自《Effective C++》 条款37。此条款中有关于“静态类型、动态类型”的描述。

在 C 语言中并没有“静态绑定”、“动态绑定”的概念(至少我没有查到)。

我理解的 C++ 类的内存模型其实就是 C 语言中的 struct 结构体。但成员函数又是归属于类所有的,所以就存在函数到类的绑定。

  • 静态绑定指的就是这种绑定关系(映射关系)是在编译期间确定的;
  • 动态绑定指的就是这种绑定关系(映射关系)在编译期间确定不了,得等到程序运行、执行期间才能最终确定

我们朴素地分析一下静态绑定、动态绑定。函数都有其地址,函数调用翻译成汇编代码其实就是直接用地址,很明显在汇编代码这个层次(甚至不用这么底层,C语言层次就行)不同的函数实现有不同的地址(使用C 语言的话,就有不同的函数名),但在 C++、Java 高级语言这个层次,不同的实现可能有相同的函数名(有很多种情况:重载、不同类里面相同名称、模板、继承体系中的重写),在 C++ 代码中出现一个函数调用(函数名),怎么正确地找到对应的实现(地址)呢?

  1. 重载:因为参数类型或者个数不同其实是有区分的,直接在高级语言下一个层次(比如C语言层面)使用不同的命名重新包装就可以了。调用时根据传参的情况,再映射就可以了。
  2. 不同的 class 里面相同名称:编译器实现这个完全可以和重载情形使用同样的方案。维护一个映射表就可以。
  3. 模板:暂时不了解
  4. 继承体系的重写:

我们都知道 override 函数时,两个函数的声明式肯定是一模一样的,如果不考虑 override 的概念(具体到代码中就是不使用 virtual 关键字),那么其场景和上述第 2 中就是一样的——如果是 base 类型(即便是指针)就调用 base class 的函数,如果是 derived 类型就调用 derived class 的函数。事实上都是编译期间根据维护的映射表“偷梁换柱”(映射是在声明的类型(函数声明式、类类型)-具体的函数实现之间),直接把对应的地址拿过来,ok,汇编代码完成了。

继承体系中允许 base 指针是可以指向 derived 对象,但编译器依旧是根据声明指针时的类型去映射具体的函数实现的,所以会出现一些变态的现象:

  • class Base 无 void func(),class Derived 有 void func(),我们执行 Base *p=new Derived(); p->func(); 会报错找不到
  • class Base 的 void func() 打印 base,class Derived 的 void func() 打印 derived,我们执行 Base *p=new Derived(); p->func(); 会打印出 base 纳尼
  • class myclass 有函数 simple(),函数实现中没有对 this 解引用的操作(不管是显式的还是隐在的),我们执行 myclass *p=NULL; p->simple(); 能够正确执行

so,正如我们看到的,这就是静态绑定。

随着 OO 越来与流行,为了获得多态性,我们想要打破这种规则——继承体系中允许基类指针指向派生类对象,在此基础上我们想让基类指针可以调用派生类的函数,我们要让例二 p->func() 打印 derive 怎么办?

好吧,增添新的语言特性,使用关键词 virtual,用来表明碰到这个类的指针(或引用)调用此函数时不要根据静态类型(声明指针的类型)映射具体实现,你们要根据这个指针指向的对象的实际类型来映射具体实现(即动态绑定)。编译器说,纳尼,我靠,我哪知道啊?我只解析 declaration,只分析了变量声明的类型,内存的初始化、赋值在运行期才发生呢……好吧,编译器感觉为难做不了这个事情,只能把这个找到(绑定)函数具体实现的步骤放到运行期间了,可是效率会低一些呢。如果你要多态性,只能接受了。

具体实现中通过 virtual 关键词标记延迟绑定,然后在运行过程中,根据对象内存中的虚函数表指针获得函数地址。

如果不是通过指针或引用调用虚函数,也是在编译期间就绑定的;而没有 virtual 修饰的函数根据调用者的静态类型在编译期间直接绑定。

// Base 类有虚函数 func()// Derived 继承自 Base,且 override 了 func()   Base *p=new Derived();    p->func();

我们觉得一目了然的事情,比如编译器汇编时直接把 p->func() 调用换成 derived::func(p) 不就好了吗,实现起来很难吗?事实上编译器是卡在它只知道 p 是 base * 类型,它并不知晓 p 被初始化(赋值)了什么,它只生成“得到一个地址,把这个地址赋给 base 指针 p;根据 p 指明的地址调用 func()” 的指令,至于前一条“分配内存,初始化 derived 对象”的指令,现在是前后相邻紧挨着,其他场景可能这两条指令相差十万八千里呢。事实上我们只会在测试时写 Base *p=new Derived(); p->func(); 这样的例子,在真实的业务场景中为了效率至少应该写成 Derived derived; derived.func();,在能够确定类型的时候使用静态绑定效率更高。实际上真实的业务场景多是