章节目录

前向声明

本节阅读量:

如下示例程序,看似没有问题:

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

int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n';
    return 0;
}

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

该程序期望结果:

1
The sum of 3 and 4 is: 7

但事实上,它无法编译通过!Visual Studio生成以下编译错误:

1
add.cpp(5) : error C3861: 'add': identifier not found

编译失败的原因是编译器按顺序编译代码。函数add直到第9行才定义,因此编译器在main的第5行遇到add的函数调用时,不知道add是什么。

有两种常见方法来解决该问题。


选项1:重新排序函数定义

一种方法是重新排序函数定义,以便在main前定义add:

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

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

int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n';
    return 0;
}

当main调用add时,编译器已经知道了什么是add。因为此程序很简单,所以这种更改很容易做。然而,在较大的程序中,试图弄清楚函数之间的调用关系(及顺序)并按顺序声明它们,会非常繁琐。

此外,这种方法并不总是可行的。假设正在编写一个包含两个函数A和B的程序。如果函数A调用函数B,函数B也调用函数A,那么就没有办法对函数进行排序来满足编译器的要求。如果先定义A,编译器会报错不知道B是什么。如果先定义B,编译器会报错不知道A是什么。


选项2:使用前向声明

通过使用前向声明可修复此问题。

前向声明允许在实际定义标识符之前告诉编译器标识符的存在。

对于函数,前向声明允许在定义函数体之前告诉编译器该函数的存在。这样,当编译器遇到函数调用时,就能理解这是一个函数调用,并检查是否正确地调用了该函数,即使编译器还不知道函数的实际定义。

为了编写函数的前向声明,需要使用函数声明语句(也称为函数原型)。函数声明由函数的返回类型、名称和参数类型组成,以分号结尾。参数名称是可选的,且声明中不包括函数体。

下面是add函数的函数声明:

1
int add(int x, int y); // 函数声明包含返回值,函数名,参数,分号,没有函数体

下面是修改后能通过编译的程序,使用了函数add的前向声明:

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

int add(int x, int y); // 函数add()的前向声明

int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n'; // 可以通过编译
    return 0;
}

int add(int x, int y) // 函数add()在这里定义
{
    return x + y;
}

现在,当编译器到达main中对add的调用时,它已经知道add函数是什么样子的(接受两个整数参数并返回一个整数),因此不会编译报错。

注意,函数声明不需要指定参数的名称(因为参数名称不是函数声明的组成部分)。在上面的代码中,也可以这样写前向声明:

1
int add(int, int); // 有效的前向声明语句

当然,带参数名的声明更好(建议使用与实际函数相同的名称),因为通过查看声明就可以理解函数参数的含义。


为什么需要前向声明?

如果可以重新排序函数的顺序使程序工作,为什么要使用前向声明?

大多数情况下,前向声明用于告诉编译器在其他代码文件中定义的函数。在这种情况下,无法通过重新排序来解决,因为调用者和被调用者位于完全不同的文件中!下一节中将更详细地讨论这一点。

前向声明也可以让我们以不受顺序限制的方式定义函数,以最容易阅读和理解的顺序来组织函数。

在不太常见的情况下,两个函数会相互调用。这时也不可能通过重新排序来解决,因为无论怎么排列都无法让两个函数都在对方之前定义。前向声明提供了一种解决这种循环依赖的方法。


缺少函数定义的情形

新手常常想了解:如果只前向声明了函数但不定义,会发生什么。

答案是:视情况而定。如果进行了前向声明但从未调用该函数,则程序将正常编译和运行。然而,如果进行了前向声明并调用了该函数,但程序中从未定义该函数,则程序可以编译通过,但链接器将报错,提示无法找到对应的函数定义。

考虑以下程序:

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

int add(int x, int y); // 前向声明 add()

int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n';
    return 0;
}

// 未定义函数 add

在此程序中,前向声明了add并调用了add,但从未在任何地方定义add。尝试编译此程序时,Visual Studio会生成以下消息:

1
2
3
4
5
Compiling...
add.cpp
Linking...
add.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z)
add.exe : fatal error LNK1120: 1 unresolved externals

正如所看到的,程序编译正常,但在链接阶段失败,因为从未定义int add(int, int)。


其他类型的前向声明

前向声明最常用于函数。然而,前向声明也可以与C++中的其他标识符一起使用,例如变量和类型。变量和类型具有不同的前向声明语法,将在以后课程中介绍这些。


声明与定义

在C++中,经常听到使用“声明”和“定义”两个词,并且通常可以互换。它们是什么意思?现在有足够的基础知识来理解两者之间的区别。

声明告诉编译器标识符的存在及其关联的类型信息。下面是一些声明的示例:

1
2
int add(int x, int y); // 告诉编译器函数add的存在,参数是2个int,返回值是int,无函数体
int x;                 // 告诉编译器变量x的存在,类型是int

定义是一个声明,它实际实现(对于函数和类型)或实例化(对于变量)标识符。

下面是一些定义示例:

1
2
3
4
5
6
7
8
int add(int x, int y) // 函数add()的实现
{
    int z{ x + y };   // 实例化变量 z

    return z;
}

int x;                // 实例化变量 x

在C++中,所有定义都是声明。因此 int x; 既是定义又是声明。

相反,并非所有的声明都是定义。不是定义的声明称为纯声明。纯声明的类型包括函数、变量和类型的前向声明。

当编译器遇到标识符时,会进行检查以确保该标识符的使用是有效的(例如,该标识符在作用域内、语法正确,等等)。

在大多数情况下,声明就足以让编译器确认标识符的使用是否正确。例如,当编译器遇到函数调用add(5, 6)时,如果已经看到了add(int, int)的声明,便可验证add确实是一个接受两个int参数的函数。编译器不需要看到函数add的实际定义(定义可能存在于其他文件中)。

然而,在某些情况下,编译器必须看到完整的定义才能使用标识符(例如模板定义和类型定义,将在以后的课程中讨论)。

下面是一个摘要表:

术语 含义 样例
定义 实现函数,或者实例化变量,定义也是声明 int x; void foo() { }
声明 告诉编译器标识符的信息 void foo(); int x;
纯声明 非定义的声明 void foo();
初始化 给一个对象初始值 int x { 2 };

单定义规则

单定义规则(简称ODR,The one definition rule)是C++众所周知的规则。ODR由三部分组成:

  1. 在同一文件的同一作用域内,函数、变量、类型、模板只能定义一次。不同作用域中的同名定义不违反此规则(例如不同函数中的同名变量,不同命名空间内的同名函数)。
  2. 在同一程序中,函数或变量只能定义一次。因为程序可能有多个文件,发生冲突时无法处理。不被链接器可见的定义不受此规则限制。
  3. 类型、模板、内联函数、内联变量可以在多个文件中同时定义,只要它们的定义是一致的。

以上情形大多数还未讲到,后面在对应的知识点处会再次讲解。

违反ODR的第1条将导致编译器发出重新定义错误。违反ODR第2条将导致链接器发出重新定义错误或导致未定义的行为。违反ODR第3条将导致未定义的行为。

下面是违反规则一的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int add(int x, int y)
{
     return x + y;
}

int add(int x, int y) // 违反了规则一
{
     return x + y;
}

int main()
{
    int x;
    int x; // 违反了规则一
}

由于上述程序违反ODR规则一,这导致Visual Studio编译器发出以下编译错误:

1
2
3
4
project3.cpp(9): error C2084: function 'int add(int,int)' already has a body
project3.cpp(3): note: see previous definition of 'add'
project3.cpp(16): error C2086: 'int x': redefinition
project3.cpp(15): note: see declaration of 'x'

ODR不适用于纯声明(它是关于定义的规则,而不是关于声明的规则),因此可以根据需要为标识符提供任意多的纯声明(尽管多个纯声明是多余的)。


2.5 为什么需要函数

上一节

2.7 多代码文件程序

下一节