2-7 前向声明与定义

请看这个看似无害的示例程序:

#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;
}

你可能会期望这个程序产生以下结果:

The sum of 3 and 4 is: 7

但实际上,它根本无法编译!Visual Studio 报出了以下编译错误:

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

我使用的clang报错如下
image

该程序无法编译的原因在于编译器会按顺序处理代码文件的内容。当编译器执行到主函数第5行的函数调用add()时,它无法识别add函数——因为我们直到第9行才定义add函数!这导致了“标识符未找到”的错误。

旧版本的Visual Studio还会额外报错:

add.cpp(9) : error C2365: 'add'; : redefinition; previous definition was 'formerly unknown identifier'

这种说法多少有些误导,因为add函数从一开始就未被定义。尽管如此,值得注意的是,单个错误引发多个冗余或相关错误/警告的情况相当常见。有时很难判断后续出现的错误或警告究竟是源于最初问题,还是需要单独解决的独立问题。

最佳实践:
在处理程序中的编译错误或警告时,请先解决列表中第一个问题,然后重新编译。

要解决这个问题,我们需要解决编译器不知道 add 是什么的问题。有两种常见的方法可以解决这个问题。


方案1:重新排列函数定义顺序

解决此问题的一种方法是重新排列函数定义顺序,使add函数在main函数之前定义:

#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;
}

这样,当主函数调用add时,编译器已经知道add是什么了。由于这是个非常简单的程序,这种改动相对容易实现。但在大型程序中,要弄清楚哪些函数调用哪些其他函数(以及调用顺序),以便按顺序声明它们,可能会相当繁琐。

此外,这种方案并非总是可行。假设我们编写了一个包含函数A和函数B的程序。若函数A调用函数B,而函数B又调用函数A,则无法通过排序使编译器满意。若先定义A,编译器会报错称未知B;若先定义B,编译器则会报错称未知A。


方案二:使用前向声明

我们也可以通过前向声明来解决这个问题。

前向声明forward declaration允许我们在实际定义标识符之前,就向编译器告知该标识符的存在。

对于函数而言,这意味着在定义函数体之前,编译器已知晓该函数的存在。这样当编译器遇到函数调用时,即使尚未知晓函数的具体定义位置,也能识别这是函数调用操作,并检查调用是否正确。

编写函数的前向声明时,需使用函数声明function declaration语句(亦称函数原型function prototype)。函数声明包含函数的返回类型、名称及形参类型,末尾以分号结束。形参名称可选。函数主体不包含在声明中。

以下是 add 函数的声明示例:

int add(int x, int y); // function declaration includes return type, name, parameters, and semicolon.  No function body!

现在,这是我们最初无法编译的程序,其中使用函数声明作为函数 add 的前向声明:

#include <iostream>

int add(int x, int y); // forward declaration of add() (using a function declaration)

int main()
{
    std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n'; // this works because we forward declared add() above
    return 0;
}

int add(int x, int y) // even though the body of add() isn't defined until here
{
    return x + y;
}

image

现在当编译器遇到 main 中的 add 调用时,它将知道 add 的定义(一个接受两个整数参数并返回整数的函数),因此不会报错。

值得注意的是,函数声明无需指定参数名称(因为它们不被视为函数声明的一部分)。在上面的代码中,你也可以像这样对函数进行前向声明:

int add(int, int); // valid function declaration

然而,我们更倾向于为形参命名(使用与实际函数相同的名称)。这样仅通过查看声明就能理解函数参数的含义。例如,当看到声明 void doSomething(int, int, int) 时,你可能自以为记得每个参数的含义,但也可能记错。

此外,许多自动文档生成工具会从头文件内容生成文档,而声明通常就位于头文件中。我们在第2.11节——头文件中将详细讨论头文件与声明的相关内容。

最佳实践
在函数声明中保留参数名称。

技巧
通过复制粘贴函数头部并添加分号,即可轻松创建函数声明。


为何需要前向声明?

你或许会疑惑:既然只需调整函数顺序就能使程序运行,为何还要使用前向声明?

前向声明最常见的用途是告知编译器某个函数已在其他代码文件中定义。这种情况下无法通过调整顺序解决问题,因为调用方和被调用方位于完全不同的文件中!我们将在下一课(2.8——多代码文件程序)中详细探讨这个问题。

前向声明还能让我们以顺序无关的方式定义函数。这使我们能够根据最大化组织性(例如将相关函数聚合)或提升可读性等需求,自由定义函数顺序。

较少见的情况是存在两个相互调用的函数。此时同样无法重新排序,因为无法将两个函数的调用顺序调整为彼此之前。前向声明为我们提供了解决此类循环依赖的方法。


忘记函数体

新手程序员常疑惑:若对函数进行前向声明却未定义,会发生什么?

答案是:视情况而定。若仅声明未调用该函数,程序编译运行均无问题。但若声明后调用该函数却未定义,程序虽能编译通过,链接器将报错指出无法解析函数调用。

请看以下程序:

#include <iostream>

int add(int x, int y); // forward declaration of add()

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

// note: No definition for function add

在此程序中,我们预先声明了 add 函数,并调用了 add,但从未在任何地方定义过 add。当尝试编译此程序时,Visual Studio 会显示以下消息:

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) 从未被定义。

clang会显示以下消息
image


其他类型的前向声明

前向声明最常用于函数。然而,在C++中,前向声明也可用于其他标识符,如变量和类型。变量与类型的前向声明语法不同,我们将在后续课程中详细讲解。


声明与定义

在C++中,你经常会听到“声明”和“定义”这两个词,它们常被混用。它们究竟有何区别?现在你已具备足够的基础知识来理解二者的差异。

声明declaration向编译器告知某个标识符的存在及其关联的类型信息。以下是一些声明的示例:

int add(int x, int y); // tells the compiler about a function named "add" that takes two int parameters and returns an int.  No body!
int x;                 // tells the compiler about an integer variable named x

定义definition是声明,它实际实现了(针对函数和类型)或实例化了(针对变量)该标识符。

以下是一些定义的示例:

// because this function has a body, it is an implementation of function add()
int add(int x, int y)
{
    int z{ x + y };   // instantiates variable z

    return z;
}

int x;                // instantiates variable x

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

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

命名法
在通用语言中,“声明”一词通常指“纯声明”,而“定义”则指“同时兼具声明功能的定义”。因此,我们通常将 int x; 称为定义,尽管它同时具备定义和声明的双重属性。

当编译器遇到一个标识符时,它会检查以确保该标识符的使用是有效的(例如,该标识符在作用域内,且以语法上正确的方式使用等)。

在大多数情况下,声明足以让编译器确保标识符被正确使用。例如,当编译器遇到函数调用 add(5, 6) 时,若已看到 add(int, int) 的声明,即可验证 add 确实是接受两个 int 参数的函数。此时无需实际看到函数 add 的定义(该定义可能存在于其他文件中)。

但存在少数情况,编译器必须看到完整定义才能使用标识符(例如模板定义和类型定义,这两者将在后续课程中讨论)。

以下是总结表:

Term Technical Meaning Examples
Declaration Tells compiler about an identifier and its associated type information. void foo(); // function forward declaration (no body)
void goo() {}; // function definition (has body)
int x; // variable definition
Definition Implements a function or instantiates a variable. Definitions are also declarations. void foo() { } // function definition (has body)
int x; // variable definition
Pure declaration A declaration that isn't a definition. void foo(); // function forward declaration (no body)
Initialization Provides an initial value for a defined object. int x { 2 }; // x is initialized to value 2

术语“声明”通常指代“纯声明”,而术语“定义”则用于同时兼具定义与声明功能的任何内容。我们在示例列注释中沿用了这种通用命名法。


单定义规则(ODR)

单定义规则one definition rule(简称ODR)是C++中广为人知的规则。该规则包含三个部分:

  1. 在单个文件内,给定作用域中的每个函数、变量、类型或模板只能有一个定义。不同作用域中的定义(例如不同函数内部定义的局部变量,或不同命名空间内部定义的函数)不违反此规则。
  2. 在程序内部,给定作用域内的每个函数或变量只能有一个定义。该规则存在的原因在于程序可能包含多个文件(下节将详细说明)。链接器不可见的函数和变量不受此规则约束(详见第7.6节——内部链接)。
  3. 类型、模板、内联函数和内联变量允许在不同文件中存在重复定义,前提是每个定义完全相同。目前我们尚未讲解这些概念,因此不必过早关注——相关知识将在后续课程中重新引入。

相关内容
我们在后续课程中将进一步探讨ODR第3部分的豁免条款:

违反 ODR 第一部分将导致编译器报出重定义错误。违反 ODR 第二部分将导致链接器报出重定义错误。违反 ODR 第三部分将导致未定义行为。

以下是违反第一部分的示例:

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

int add(int x, int y) // violation of ODR, we've already defined function add(int, int)
{
     return x + y;
}

int main()
{
    int x{};
    int x{ 5 }; // violation of ODR, we've already defined x
}

在此示例中,函数 add(int, int) 在全局作用域内被定义两次,局部变量 int x 在 main() 函数作用域内被定义两次。因此 Visual Studio 编译器报出以下编译错误:

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'

我的clang显示的信息如下:
image

然而,main() 中定义局部变量 int x,而 add() 中也定义函数参数 int x,并不违反 ODR 第一部分。这些定义出现在不同的作用域中(各自函数的作用域内),因此被视为对两个不同对象的独立定义,而非对同一对象的定义与重定义。

对于高级读者
共享同一标识符但具有不同参数集的函数也被视为不同的函数,因此此类定义并不违反 ODR(未定义重载)。我们将在第 11.1 课——函数重载简介中进一步探讨此内容。


测验时间

问题 #1

什么是函数原型?

显示解答

函数原型是一种声明语句,包含函数名、返回类型、参数类型,以及可选的参数名称。它不包含函数主体,用于在函数定义之前告知编译器该函数的存在。

问题 #2

什么是前向声明?

显示解答

前向声明告知编译器,某个标识符在实际定义之前就已存在。

问题 #3

如何为函数声明前向声明?

显示解答

对于函数而言,函数声明/原型即作为前向声明。

问题 #4

为该函数编写函数声明(使用首选的带名称形式):

int doMath(int first, int second, int third, int fourth)
{
     return first + second * third / fourth;
}

显示答案

// Do not forget the semicolon at the end, since these are statements.
int doMath(int first, int second, int third, int fourth);

问题 #5

对于以下每个程序,说明它们是否编译失败、链接失败,还是编译和链接均成功。若不确定,请尝试编译它们!

a)

#include <iostream>
int add(int x, int y);

int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
    return 0;
}

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

显示答案

无法编译。编译器会报错,指出找不到接受3个参数的add()函数。add()的前向声明仅有两个参数。

b)

#include <iostream>
int add(int x, int y);

int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
    return 0;
}

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

显示答案

无法编译。编译器会报错,指出找不到接受3个参数的add()函数。add()的前向声明仅有两个参数,而具有3个参数的add()函数定义尚未出现。

c)

#include <iostream>
int add(int x, int y);

int main()
{
    std::cout << "3 + 4 = " << add(3, 4) << '\n';
    return 0;
}

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

显示答案

无法链接。编译器会将 add 的前向声明与 main() 中对 add() 的函数调用进行匹配。然而,从未实现过接受两个参数的 add() 函数(我们只实现了一个接受 3 个参数的版本),因此链接器会报错。

d)

#include <iostream>
int add(int x, int y, int z);

int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
    return 0;
}

int add(int z, int y, int x) // names don't match the declaration
{
    return x + y + z;
}

显示答案

编译并链接。函数调用中add()的类型与前向声明匹配,add()的定义也匹配。名称与声明不匹配并不重要,因为声明中的名称是可选的(即使提供,编译器也会忽略)。

e)

#include <iostream>
int add(int, int, int); // no parameter names

int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
    return 0;
}

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

显示答案

编译并链接。这与前一种情况相同。函数声明无需指定参数名称(尽管我们通常建议包含参数名)。

f)

#include <iostream>

int add(int x, int y);

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

int main()
{
    std::cout << "3 + 4 + 5 = " << add(3, 4, 5) << '\n';
    return 0;
}

显示答案

编译并链接。带两个参数的add()的前向声明未被使用。
posted @ 2026-02-09 14:25  游翔  阅读(2)  评论(0)    收藏  举报