前向声明
本节阅读量:如下示例程序,看似没有问题:
|
|
该程序期望结果:
|
|
但事实上,它无法编译通过!Visual Studio生成以下编译错误:
|
|
编译失败的原因是编译器按顺序编译代码。函数add直到第9行才定义,因此编译器在main的第5行遇到add的函数调用时,不知道add是什么。
有两种常见方法来解决该问题。
选项1:重新排序函数定义
一种方法是重新排序函数定义,以便在main前定义add:
|
|
当main调用add时,编译器已经知道了什么是add。因为此程序很简单,所以这种更改很容易做。然而,在较大的程序中,试图弄清楚函数之间的调用关系(及顺序)并按顺序声明它们,会非常繁琐。
此外,这种方法并不总是可行的。假设正在编写一个包含两个函数A和B的程序。如果函数A调用函数B,函数B也调用函数A,那么就没有办法对函数进行排序来满足编译器的要求。如果先定义A,编译器会报错不知道B是什么。如果先定义B,编译器会报错不知道A是什么。
选项2:使用前向声明
通过使用前向声明可修复此问题。
前向声明允许在实际定义标识符之前告诉编译器标识符的存在。
对于函数,前向声明允许在定义函数体之前告诉编译器该函数的存在。这样,当编译器遇到函数调用时,就能理解这是一个函数调用,并检查是否正确地调用了该函数,即使编译器还不知道函数的实际定义。
为了编写函数的前向声明,需要使用函数声明语句(也称为函数原型)。函数声明由函数的返回类型、名称和参数类型组成,以分号结尾。参数名称是可选的,且声明中不包括函数体。
下面是add函数的函数声明:
|
|
下面是修改后能通过编译的程序,使用了函数add的前向声明:
|
|
现在,当编译器到达main中对add的调用时,它已经知道add函数是什么样子的(接受两个整数参数并返回一个整数),因此不会编译报错。
注意,函数声明不需要指定参数的名称(因为参数名称不是函数声明的组成部分)。在上面的代码中,也可以这样写前向声明:
|
|
当然,带参数名的声明更好(建议使用与实际函数相同的名称),因为通过查看声明就可以理解函数参数的含义。
最佳实践
在函数声明中保留参数名。
提示
通过复制/粘贴函数头并添加分号,可以轻松创建函数声明。
为什么需要前向声明?
如果可以重新排序函数的顺序使程序工作,为什么要使用前向声明?
大多数情况下,前向声明用于告诉编译器在其他代码文件中定义的函数。在这种情况下,无法通过重新排序来解决,因为调用者和被调用者位于完全不同的文件中!下一节中将更详细地讨论这一点。
前向声明也可以让我们以不受顺序限制的方式定义函数,以最容易阅读和理解的顺序来组织函数。
在不太常见的情况下,两个函数会相互调用。这时也不可能通过重新排序来解决,因为无论怎么排列都无法让两个函数都在对方之前定义。前向声明提供了一种解决这种循环依赖的方法。
缺少函数定义的情形
新手常常想了解:如果只前向声明了函数但不定义,会发生什么。
答案是:视情况而定。如果进行了前向声明但从未调用该函数,则程序将正常编译和运行。然而,如果进行了前向声明并调用了该函数,但程序中从未定义该函数,则程序可以编译通过,但链接器将报错,提示无法找到对应的函数定义。
考虑以下程序:
|
|
在此程序中,前向声明了add并调用了add,但从未在任何地方定义add。尝试编译此程序时,Visual Studio会生成以下消息:
|
|
正如所看到的,程序编译正常,但在链接阶段失败,因为从未定义int add(int, int)。
其他类型的前向声明
前向声明最常用于函数。然而,前向声明也可以与C++中的其他标识符一起使用,例如变量和类型。变量和类型具有不同的前向声明语法,将在以后课程中介绍这些。
声明与定义
在C++中,经常听到使用“声明”和“定义”两个词,并且通常可以互换。它们是什么意思?现在有足够的基础知识来理解两者之间的区别。
声明告诉编译器标识符的存在及其关联的类型信息。下面是一些声明的示例:
|
|
定义是一个声明,它实际实现(对于函数和类型)或实例化(对于变量)标识符。
下面是一些定义示例:
|
|
在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 }; |
注
通常,术语”声明”用于表示”纯声明”,”定义”用于表示”同时也是声明的定义”。因此,int x; 通常被称为定义,即使它既是定义又是声明。
单定义规则
单定义规则(简称ODR,The one definition rule)是C++众所周知的规则。ODR由三部分组成:
- 在同一文件的同一作用域内,函数、变量、类型、模板只能定义一次。不同作用域中的同名定义不违反此规则(例如不同函数中的同名变量,不同命名空间内的同名函数)。
- 在同一程序中,函数或变量只能定义一次。因为程序可能有多个文件,发生冲突时无法处理。不被链接器可见的定义不受此规则限制。
- 类型、模板、内联函数、内联变量可以在多个文件中同时定义,只要它们的定义是一致的。
以上情形大多数还未讲到,后面在对应的知识点处会再次讲解。
违反ODR的第1条将导致编译器发出重新定义错误。违反ODR第2条将导致链接器发出重新定义错误或导致未定义的行为。违反ODR第3条将导致未定义的行为。
下面是违反规则一的示例:
|
|
由于上述程序违反ODR规则一,这导致Visual Studio编译器发出以下编译错误:
|
|
ODR不适用于纯声明(它是关于定义的规则,而不是关于声明的规则),因此可以根据需要为标识符提供任意多的纯声明(尽管多个纯声明是多余的)。
对于高级读者
有不同参数的同名函数是不同的函数。在函数重载章节会讨论。