章节目录

前向声明

本节阅读量:

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

 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

编译失败的原因是编译器按顺序编译代码。而第9行才定义add,编译器在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 多代码文件程序

下一节