前向声明
本节阅读量:如下示例程序,看似没有问题:
|
|
该程序期望结果:
|
|
但事实上,它无法编译通过!Visual Studio生成以下编译错误:
|
|
编译失败的原因是编译器按顺序编译代码。而第9行才定义add,编译器在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不适用于纯声明(它是描述定义的规则,而不是关于声明的规则),因此可按需为标识符提供任意多的纯声明(尽管多个纯声明是多余的)。
对于高级读者
有不同参数的同名函数是不同的函数。在函数重载章节会讨论。
