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