Skip to content

18.5 预绑定和后期绑定

By Alex on February 7th, 2008 | last modified by Alex on December 21st, 2020

翻译于 dashjay 2020-12-24 | 最后修改于 2020-12-24

在这一节和下一节课中,我们将会进一步看看虚函数怎么被实现的,当然这些知识并不是严格必须的,仅仅只是作为兴趣开拓。既然如此,你可以认为这两节是选读。

当一个 C++ 程序被执行,它按顺序执行,开始于 main()。当遇到函数调用的时候,执行点跳到了被调用函数开始的地方。CPU是如何知道这个的呢?

当一个程序被编译的时候,编译器转化每一个 C++ 程序中的语句成为一行或者多行机器语言。每一行机器语言给了有一个他自己的地址。每个函数没什么不同 —— 当遇到一个函数的时候,他会转化成机器语言并且提供下一个可用的地址,因此,每个函数结束于一个独特的地址。

绑定是指进程用于转换标识符(例如变量和函数名称)到地址的过程。尽管绑定同时作用于变量和函数,在这节课中我我们主要关注函数的绑定。

早期绑定(Early binding)

编译器遇到的大多数函数调用将会直接指向函数调用。一个直接调用是一个语句直接调用一个函数,例如:

#include <iostream>

void printValue(int value)
{
    std::cout << value;
}

int main()
{
    printValue(5); // This is a direct function call
    return 0;
}

直接调用可以使用一个称为早期绑定的过程来解决。早期绑定(也成为 static binding【静态绑定】)意味着编译器(或者链接器)能够直接关联标识符名称(例如函数或者变量)和一个机器地址。注意所有的函数调用有独一无二的地址。因此当一个编译器(或者链接器)遇到一个函数调用,他就用一个机器指令替换函数调用,告诉 CPU 跳到函数调用的地方。

让我们看一下使用早期绑定的简单计算器程序:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int subtract(int x, int y)
{
    return x - y;
}

int multiply(int x, int y)
{
    return x * y;
}

int main()
{
    int x;
    std::cout << "Enter a number: ";
    std::cin >> x;

    int y;
    std::cout << "Enter another number: ";
    std::cin >> y;

    int op;
    do
    {
        std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
        std::cin >> op;
    } while (op < 0 || op > 2);

    int result = 0;
    switch (op)
    {
        // call the target function directly using early binding
        case 0: result = add(x, y); break;
        case 1: result = subtract(x, y); break;
        case 2: result = multiply(x, y); break;
    }

    std::cout << "The answer is: " << result << std::endl;

    return 0;
}

因为 add(), substract() and multiply() 都是直接调用,编译器将会早期绑定来解析 add(), abstract(), multiply() 函数调用。编译器将会用一个指令来替换 add() 函数调用,那会告诉 CPU 跳到 add() 函数的执行。同样的道理也适用于 substract()multiply()

延迟绑定(Late Binding)

在同样的程序中,不到运行时(当程序运行的时候)不知道调用了哪个函数。这被称为后期绑定(或者动态绑定)。在 C++ 中,后期绑定的唯一方式是使用函数指针。来简单的回忆一下函数指针,函数指针是一种指针指向函数,而不是变量。函数指针指向的函数可以通过使用函数调用操作符来调用,通过那个指针。

例如,下面的函数调用 add() 函数:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int main()
{
    // Create a function pointer and make it point to the add function
    int (*pFcn)(int, int) = add;
    std::cout << pFcn(5, 3) << std::endl; // add 5 + 3

    return 0;
}

通过函数指针调用也被看做一种间接调用。下面的计算机程序就是和之前功能相同的。就是使用了函数指针而不是直接调用:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int subtract(int x, int y)
{
    return x - y;
}

int multiply(int x, int y)
{
    return x * y;
}

int main()
{
    int x;
    std::cout << "Enter a number: ";
    std::cin >> x;

    int y;
    std::cout << "Enter another number: ";
    std::cin >> y;

    int op;
    do
    {
        std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
        std::cin >> op;
    } while (op < 0 || op > 2);

    // Create a function pointer named pFcn (yes, the syntax is ugly)
    int (*pFcn)(int, int) = nullptr;

    // Set pFcn to point to the function the user chose
    switch (op)
    {
        case 0: pFcn = add; break;
        case 1: pFcn = subtract; break;
        case 2: pFcn = multiply; break;
    }

    // Call the function that pFcn is pointing to with x and y as parameters
    // This uses late binding
    std::cout << "The answer is: " << pFcn(x, y) << std::endl;

    return 0;
}

在这个例子当中,我们直接将 pFcn 指针的指向了我们希望调用的函数,而不是直接调用 add(), substract(), multiply() 等函数。紧接着我们调用函数通过指针,编译器不能通过早期绑定来决定函数的解析 pFcn(x, y) 因为不能在编译期间告诉函数指针 pFcn 指向哪个函数!

后期绑定轻微的低效,因为它引入了一层而外的重定向。使用早绑定,CPU可以直接跳到函数的地址。使用晚期绑定,程序必须读取指针指向的地址才能跳过去。这引入了而外的一部,使得程序更慢了。然而晚期绑定的优点就是能隔年灵活,相比于早期绑定,因为决定要调用哪个函数指导运行时才需要确定。

在下节课中,我们会看一眼后期绑定如何实现虚函数。