C++是一种强类型语言,它是如何保持静态类型的同时,实现动态绑定的呢?

上一节基于一个简单的实例讨论了C++语言中的虚函数,我们提到了“动态绑定”这个词,大意就是动态绑定在很大程度上满足了虚函数的特性,从而支持了C++的多态性。不过我们知道,C++是一门强类型的语言,它是如何在保持静态类型的同时,实现动态绑定的呢?

“静态”VS“动态”

在C++语言中,对象指针一般能够提供两种类型信息:

  1. 指针本身的类型
  2. 指针指向类的类型

鉴于指针指向的类可能是继承某个基类派生而来的,所以这两个信息可能是不同的,请看下面这段C++语言代码示例:

class Animal
{
public:
    virtual void eat() {
         std::cout << "I'm eating generic food." ;
    }
};

class Cat : public Animal
{
public:
    void eat() { 
    std::cout << "I'm eating a rat.";
    }
};

Animal *a = new Animal;
Cat *c = new Cat;

a = c; 
a->eat();

上述代码执行完毕后,(静态)类型为Animal *的指针 a 指向的实际上是Cat对象 c,因为Animal::eat()是虚函数,所以a->eat()在运行时(动态)确定为Cat::eat()。到这里其实可以看出,所谓“静态”,其实就是在编译时确定的类型,而“动态”则是在运行时确定的类型,二者在C++语言中提供不同的功能。

静态类型通常在编译时用于成员函数调用的合法性检查,编译器会根据指针的静态类型来确定程序是否能够合法的调用某个成员函数,如果可以,那么它指向的对象也一定可以。例如,如果 Animal 对象有某个成员函数可供调用,那么Cat对象也一定可以调用该成员函数,因为CatAnimal的其中一种派生类。

动态绑定则在程序运行过程中,执行被调用函数时用于确定实际的函数地址,它被称作“动态”是因为其在程序运行时才最终被确定下来,C++语言中的虚函数的特性正是基于“动态绑定”机制实现的。

四个关键词

要进一步的理解C++语言中的“动态”和“静态”相关的概念,需要先理解下面这四个关键词:

  1. 对象的静态类型:声明对象使用的类型,编译时确定。
  2. 对象的动态类型:指针当前所指对象的类型,运行时确定。
  3. 静态绑定:绑定对象的静态类型,发生在编译时。
  4. 动态绑定:绑定对象的动态类型,发生在运行时。

关于前两个名词,可以看下面这几段C++语言代码示例:

Cat *c = new Cat;

此时对象指针 c 的静态类型是声明它时采用的Cat *,动态类型也是Cat *。对于下面这行C++语言代码:

Animal *a = c;

对象指针 a 的静态类型是Animal *,动态类型就不同了,而是Cat *。对象的静态类型一旦确定,就不能修改了,但是动态类型可以,例如:

a = new Animal;

现在来看看“静态绑定”和“动态绑定”,我们先为 Animal 和 Cat 类新增加两个非虚函数:

class Animal
{
public:
    virtual void eat() {
         std::cout << "I'm eating generic food." ;
    }
    void run() {
        std::cout << "some can run." ;
    }
};

class Cat : public Animal
{
public:
    void eat() { 
        std::cout << "I'm eating a rat.";
    }
    void run() {
        std::cout << "I can run." ;
    }
};

Cat *c = new Cat;
Animal *a = c;

此时 a->run() 和 c->run() 调用的是同一个函数吗?显然不是的,非虚函数 run() 是静态绑定的,这一过程发生在编译时,并且具体调用哪个函数由对象的静态类型确定,所以虽然 a 和 c 指向同一个对象,但是 a 的静态类型是Animal *,所以 a->run() 调用的是 Animal::run(),同理,c->run() 调用的是 Cat::run()。

那 a->eat() 和 c->eat() 调用的是同一个函数吗?是的,eat() 函数是虚函数,它是动态绑定的,这一过程发生在运行时,并且具体调用哪个函数由指针指向的对象类型确定,因为 a 和 c 指向的都是Cat对象,所以 a->eat() 和 c->eat() 调用的都是 Cat::eat() 函数。

看来,弄清C++语言中哪些是静态绑定,哪些是动态绑定是问题的关键,我查阅了很多资料,没有得到直接的答案,但是相当多的C++程序员都认为只有涉及到虚函数时才使用动态绑定,其他的都是静态绑定。当然了,这一结论是根据我有限的学识推测的,只不过我目前还没有发现例外,如果有错误,希望可以指点一二。

注意事项

C++语言程序开发中有一条“潜规则”:Never redefine function’s inherited default parameters value,意思是绝对不要重定义函数继承而来的默认参数,对于虚函数来说也一样需要遵守。请看下面这个例子:

class A {
    virtual void vfoo(int i=1);
};
class B :public A{
    virtual void vfoo(int i=2); 
};
B *b = new B;
A *a = b;
a->vfoo();
b->vfoo();

经过上面的讨论,相信读者一眼就能看出 a->vfoo() 和 b->vfoo() 调用的都是 B::vfoo(),但是它们的默认参数是多少呢?会不会都是 2 呢?按照前面的理论“除了虚函数,C++语言中的其他都是静态绑定的”,那么默认参数也应该是静态绑定的,这就是说默认参数主要取决于对象指针的静态类型,也即 a->vfoo() 的默认参数取决于 a 的静态类型A *,所以它的默认参数是 1,同理,b->vfoo() 的默认参数应该是 2,将示例C++语言代码写完整测试之:

#include <iostream>

using namespace std;

class A {
public:
    virtual void vfoo(int i=1){
        cout <<"A: "<< i << endl;
    }
};
class B :public A{
public:
    virtual void vfoo(int i=2){
        cout <<"B: "<< i << endl;
    } 
};

int main()
{
    B *b = new B;
    A *a = b;
    a->vfoo();
    b->vfoo();

    return 0;
}

编译并执行这段代码,得到如下输出:

$ g++ t.cpp
$ a.out
B: 1
B: 2

可见,默认参数的确是静态绑定的。但是这样的输出很怪异,动态绑定和静态绑定纠缠在一起,很容易弄混头脑,相信没有人喜欢这样,所以还是遵守行业“潜规则”吧。

小结

本文主要讨论了理解C++语言中虚函数的关键——“静态绑定”和“动态绑定”的概念,这两个过程分别发生编译时运行时,前者根据对象的指针类型确定调用函数,后者则根据指针指向的对象类型确定调用函数。文章在最后还引入了一条C++程序员间的潜规则,并通过一段简短的示例说明了遵守潜规则的必要性。

阅读更多:   C++虚函数
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif