章节目录

静态绑定和动态绑定

本节阅读量:

在这一课和下一课中,我们将更深入地了解虚函数是如何实现的。虽然这些信息对于有效使用虚函数并不是严格必要的,但它们很有趣。您可以将这两节视为可选阅读。

执行 C++ 程序时,它会从 main() 的顶部开始按顺序执行。当遇到函数调用时,执行点会跳到被调用函数的开头。CPU 如何知道该跳到哪里?

编译程序时,编译器会将 C++ 程序中的每个语句转换为一行或多行机器语言。机器语言的每一行都有自己唯一的顺序地址。函数也一样:函数会被转换为机器语言,并获得可用地址。因此,每个函数最终都有一个唯一地址。


绑定和分派

我们的程序包含许多名称(标识符、关键字等)。每个名称都有一组相关的属性:例如,如果名称表示变量,则该变量具有类型、值、内存地址等。

例如,当我们写 “int x” 时,就是告诉编译器将名称 x 与类型 int 关联起来。稍后,如果我们写 “x = 5”,编译器就可以使用这个关联对赋值进行类型检查,以确保它有效。

在一般编程中,绑定是将名称与此类属性相关联的过程。函数绑定(或方法绑定,bind)是确定与函数调用关联的函数定义的过程。实际调用绑定函数的过程称为分派(dispatch)。

在C++中,术语绑定的使用更随意(而分派通常被认为是绑定的一部分)。我们将探索下面这些术语的C++用法。

绑定是一个具有多重含义的术语。在其他上下文中,绑定可以指:

  1. 将引用与对象绑定
  2. std::bind
  3. 语言绑定

静态绑定

编译器遇到的大多数函数调用都是直接函数调用。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

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

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

int main()
{
    printValue(5);   // 直接函数调用 printValue(int)

    Foo f{};
    f.printValue(5); // 直接函数调用 Foo::printValue(int)
    return 0;
}

在C++中,当直接调用非成员函数或非虚成员函数时,编译器可以确定哪个函数定义应与调用匹配。这称为静态绑定或早期绑定,因为它可以在编译时执行。然后,编译器(或链接器)可以生成机器语言指令,告诉CPU直接跳到函数的地址。

对重载函数和函数模板的调用也可以在编译时解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

template <typename T>
void printValue(T value)
{
    std::cout << value << '\n';
}

void printValue(double value)
{
    std::cout << value << '\n';
}

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

int main()
{
    printValue(5);   // 直接函数调用 printValue(int)
    printValue<>(5); // 直接函数调用 printValue<int>(int)

    return 0;
}

让我们来看一个使用静态绑定的简单计算器程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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{};
    std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
    std::cin >> op;

    int result {};
    switch (op)
    {
        // 使用静态绑定,调用对应的函数
        case 0: result = add(x, y); break;
        case 1: result = subtract(x, y); break;
        case 2: result = multiply(x, y); break;
        default:
            std::cout << "Invalid operator\n";
            return 1;
    }

    std::cout << "The answer is: " << result << '\n';

    return 0;
}

由于 add()、subtract() 和 multiply() 都是对非成员函数的直接函数调用,因此编译器会在编译时将这些函数调用与各自的函数定义相匹配。

注意,由于 switch 语句的存在,实际调用哪个函数要到运行时才确定。然而,这是执行路径问题,而不是绑定问题。

动态绑定

在某些情况下,函数调用要到运行时才能解析。在 C++ 中,这称为动态绑定(或者延迟绑定)。

在一般编程术语中,术语“延迟绑定”通常意味着被调用的函数不能仅基于静态类型信息来确定,而必须使用动态类型信息来解析。

在 C++ 中,该术语的使用更宽泛,通常表示编译器或链接器在实际进行函数调用的位置,不知道最终会调用哪个具体函数。

在 C++ 中,获得动态绑定的一种方法是使用函数指针。简单回顾一下:函数指针是一种指向函数而不是变量的指针。可以通过函数指针加「运算符()」来调用对应函数。

例如,下面的代码通过函数指针调用printValue()函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>

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

int main()
{
    auto fcn { printValue }; // 创建函数指针,并指向 printValue
    fcn(5);                  // 通过函数指针,间接的调用 printValue

    return 0;
}

通过函数指针调用函数也称为间接函数调用。在实际调用 fcn(5) 的位置,编译器在编译时不知道正在调用哪个函数。相反,程序会在运行时读取函数指针保存的地址,并对该地址处的函数进行间接调用。

下面的计算器程序在功能上与上面的计算器示例相同,只是它使用函数指针而不是直接函数调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#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{};
    std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
    std::cin >> op;

    using FcnPtr = int (*)(int, int); // 给丑陋的类型一个别名
    FcnPtr fcn { nullptr }; // 创建函数指针, 设置为 nullptr

    // 按照用户选择,将 fcn 设置为对应的函数
    switch (op)
    {
        case 0: fcn = add; break;
        case 1: fcn = subtract; break;
        case 2: fcn = multiply; break;
        default:
            std::cout << "Invalid operator\n";
            return 1;
    }

    // 调用 fcn 对应的函数
    std::cout << "The answer is: " << fcn(x, y) << '\n';

    return 0;
}

在这个例子中,我们没有直接调用 add()、subtract() 或 multiply() 函数,而是让 fcn 指向希望调用的函数,然后通过指针调用函数。

编译器无法使用静态绑定来解析函数调用 fcn(x, y),因为它无法在编译时确定 fcn 将指向哪个函数!

动态绑定的效率稍低,因为它涉及额外的间接访问。通过静态绑定,CPU 可以直接跳到函数地址。对于动态绑定,程序必须读取指针中保存的地址,然后跳到该地址。这多了一个步骤,因此会稍微慢一些。然而,动态绑定的优点是比静态绑定更灵活,因为它可以在运行时才决定调用哪个函数。

在下一课中,我们将了解如何使用动态绑定来实现虚函数。


25.3 虚析构函数、虚赋值函数以及虚函数重写

上一节

25.5 虚函数表

下一节