C++ 自学教程 LearnCPP 第1.7章 前置定义和声明

本文是C++自学教程的一部分,讲解了前置声明和函数原型的概念,包括其重要性和使用场景。通过示例解释了为什么在调用未定义的函数时会出现编译错误,以及如何通过函数原型避免这种问题。还提到了忘记函数主体时的编译和链接错误,以及声明和定义的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++ 自学教程 第1.7章 前置定义和声明

让我们看看一下这个一脸无辜无害的程序:

#include <iostream>

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

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

你可能会推测它的输出结果是:

The sum of 3 and 4 is: 7

但事实上这段程序根本无法编译!如果你在Visual Studio 2015运行,系统 会提供以下报错信息:

add.cpp(5) : error C3861: ‘add’: identifier not found
add.cpp(9) : error C2365: ‘add’ : redefinition; previous definition was ‘formerly unknown identifier’

这段程序无法被编译的原因是因为编译器自上而下阅读代码。当编译器在主函数的第五行看到对add()的调用的时候,函数add()还没被定义呢(直到第九行我们才告诉编译器这个函数是什么)!所以系统报错“identifier not found”。

当vs2005读到第九行,对add()函数的定义的时候,系统报错说add被重复定义了。这个报错可能听上去有点混乱,因为这是这个函数之前从未被定义。更新版本的vs去掉了这个错误信息。

虽然第二个报错信息有点冗余,但一个错误引起多个报错的情况其实并不少见。

规则:当调试程序报错的时候,从第一个错误信息开始处理。

想要“订正”这个程序,我们需要解决第一个错误信息:“add是什么?”。 有两个常见的解决方法。
解法1:调整两个函数的顺序,使得add的定义出现在主函数之前:

#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) << std::endl;
    return 0;
}

这样做后,当主函数调用add的时候,编译器已经知道了这个函数是什么。因为这个例子比较简单,变换顺序还是比较简单易行的。但是当处理更复杂的程序的时候,要想清楚哪个函数应该出现在哪个函数的前面可能很麻烦。

另外,这个解法不是什么时候都可行的。比如说我们想要编一个程序,使用了函数A跟函数B。如果函数A调用了函数B,函数B又调用了函数A,那根本没办法决定哪个函数应该先被定义。哪个在后面都会引起编译错误。


函数原型和函数的前置声明

解法2:使用前置声明。

前置声明(forward declaration)使我们可以在定义某个东西之前先告诉编译器这个东西的存在。

以定义函数为例,我们可以告诉编译器有这样的一个函数,然后在具体定义这个函数。这样的话,当编译器看到对这个函数的调用的时候,它能明白我们是在调用某函数,并检查我们是否正确调用了这个函数,虽然它还不知道函数具体会在哪里被定义。

函数的前置声明用一个叫做函数原型(function prototype)的叙述语句来实现。函数原型由函数的返回值类型,名称,参数组成,不包含函数定义的主体。因为函数原型是一个叙述语句,它以分号结尾。
举个栗子:

int add(int x, int y); // 函数原型由函数的返回值类型,名称,参数组成,不包含函数定义的主体!

回到一开始的那个例子,当我们在主函数前面放一个函数原型的叙述:

#include <iostream>

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

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

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

这样,当编译器读到对add的调用的时候,它能知道这个函数大致长什么样(有两个整数参数并返回一个整数),因此不会报错。

值得注意的是,定义函数原型的时候不需要给出具体的参数名。在上面的例子里,函数原型也可以这样给出:

int add(int, int);

但是我们还是推荐上面一种给出参数名的定义方式。这样更便于阅读和理解代码,不然需要找到具体定义函数的地方才知道每个参数是什么。

小Tip:你可以直接复制黏贴函数定义的第一行来生成它的函数原型,别忘记最后加个分号就行了。

忘记函数主体

讲到这里,新手们可能会问:那么如果我前置声明了函数原型,却忘记定义函数主体会发生什么呢?

答案是:不一定。如果你前置声明了一个函数,但是这个函数从未被调用, 那这个程序会正常编译运行。但是如果你前置声明并且调用了一个缺少主体的函数,程序能够编译,但是连接器会报错,因为它没法正确调用函数。

比方说下面这个例子:

#include <iostream>

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

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

在这个程序里,我们前置声明了函数add(),然后我们调用了它,但是我们没在任何地方定义这个函数。当我们试着编译这个程序,VS2005会显示以下报错信息:

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

如你所见,函数能够正确编译但会在后面连接阶段报错。

前置声明的其他形式

前置声明主要应用于函数上,但也可以用在函数,自定义类型等等其他标识符上。它们的前置定义使用的格式不同,我们后面会具体讲解相关内容。

声明VS定义

在C++里面,你常常会看见声明(declaration)定义(definition)这两个词。它们都是什么意思呢?下面的例子能让你大致了解它们的 异同。
定义确实执行或者初始化(得到被分配的内存)了某个标识符。比方说:

int add(int x, int y) // 执行函数add()
{
    return x + y;
}

int x; // 给一个叫做x的参数分配内存

定义必须能够满足连接器的需求。如果你使用了一个标识符但又没有提供定义,连接器会报错。
众所周知,“定义有且只有一次。”是C++里的黄金定律(又被称作ODR)。它具体由以下两点:
1) 一个文件中,一个标识符只能有被定义一次。
2) 在一个程序中,一个对象或者函数只能有一个定义。这一点必须要另外说明,因为一个程序可以有多个文件组成。注意某些标识符(比如类,函数模板,行内函数等)不用遵从这个规则,在之后的教程会具体阐述。
违背ODR规则通常会引起你的编译器或者连接器显示定义错误。

声明是一个告诉编译器某个标识符存在的叙述语句。比方说:

int add(int x, int y); // 告诉编译器函数add有两个整数参量
int x; // 告诉编译器有个叫做x的变量

声明只需要满足编译器就可以了。这也是为什么前置声明能够让函数正常编译。如果你在声明前使用了一个标识符,编译器会报错。

你可能会注意到“int x”在两者里面都出现了。在C++里,所有的定义也同时是声明。因为“int x”是个定义语句,它同时也是个声明语句。所以,在许多情况下,我们只需要定义就可以了。不过如果你想要在定义之前使用某个标识符,你就必须使用声明(外国人好喜欢车轱辘话来回说= =)。

有一小部分声明不是定义,比如说函数原型。这些声明又被称作纯声明(pure declarations)。其他纯声明有对变量的,对类的前置声明。纯声明可以想有多少就有多少(虽然多余的声明是冗余的)。


小测试

1) 函数原型和前置声明有什么不同?
2) 写出下面函数的函数原型:
3) 说出下面这些函数哪些可以编译,可以连接。如果你不确定,编译一下试试!

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

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

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

4)

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

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

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

5)

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

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

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

6)

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

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

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

答案

1) 函数原型是一句包含了函数名,返回值类型,和参数的声明叙述。前置声明告知编译器某个标识符的存在。对函数来说,函数原型被用做函数的前置声明。其他标识符的前置声明使用别的格式。

2)

// 第一种解法更好
int doMath(int first, int second, int third, int fourth); // better solution
int doMath(int, int, int, int);

3) 无法编译
4) 无法编译
5) 无法连接
6) 可以编译,可以连接。


说明: 这系列笔记是基于网上一个英文教程LearnCPP

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值