1.7前置声明与定义

先来看一个表面看起来没有错误的程序add.cpp。

#include <iostream>
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl;
    return 0;
}
 
int add(int x, int y)
{
    return x + y;
}

预计结果应为:

The sum of 3 and 4 is: 7

但事实上,这个程序在Visual Studio 2005中根本无法编译通过,错误提示为:

add.cpp(6) : error C3861: ‘add’: identifier not foundadd.cpp(10) : error C2365: ‘add’ : redefinition; previous definition was ‘formerly unknown identifier’

此程序无法编译的原因是编译器会按顺序读取文件,当编译器到达第六行即main()函数中调用add()的部分时,并不能明确add()是什么,我们知道第10行才开始定义add,于是导致了第一个错误:“identifier not found”(未找到标识符)。同时Visual Studio 2005还会郁闷为何add被定义了两遍。这可能产生一些误解,因为前面根本没有定义add()函数啊,Visual Studio后续的版本修正了这个错误,略去了这个多余的错误提示。虽然第二个提示是多余的,我们还是要注意一个错误造成多个错误提示的情况是屡见不鲜的。

Note:解决编译错误时,首先解决提示中的第一个错误。

要解决这个问题,首先需要明确编译器并不知道add是什么,通常有两种方法来告诉它。

第一种:将add定义在main()之前。

#include <iostream>
 
int add(int x, int y)
{
    return x + y;
}
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl;
    return 0;
}

这种情况下,在main函数调用add函数时,编译器已经能够知道add是什么,因为这个程序比较简单,所以这种改动相对来说容易做,但是在一个大型的程序中,试图弄清楚哪个函数调用了哪个函数,以便于将各个函数按照相应的顺序定义是一件很烦心的事。

另外,这个方法并不总是可行的,比如我们写了一个有两个函数A、B的函数,如果A调用了B,B又调用了A,就无法找到合适的顺序定义A、B以使两者兼顾。如果先定义A,编译器就会抱怨自己根本不认识B,先定义B的话,同样也会产生错误。

函数原型与前置声明

第二种:使用前置声明

前置声明允许我们在真正定义一个标识符前告知编译器这个标识符的存在。对于函数来说,这允许我们在定义函数体之前告知编译器函数的存在,这样编译器在遇到函数调用时就能明白这是一个函数调用并且能够检查我们是否正确调用了这个函数,即使编译器还不知道这个函数是如何以及在哪里被定义的。

为了给函数写前置声明,我们使用一个称为函数原型的声明语句,函数原型由函数返回值类型、函数名、参数列表组成,不包括函数体(花括号之间的部分),函数原型是一个语句,因为它以分号结束。以下就是函数add的函数原型:

int add(int x, int y);

这样,之前那个无法编译通过的程序可以使用函数原型修改为:

#include <iostream>
 
int add(int x, int y); // 使用函数原型的前置声明
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl; 
    return 0;
}
 
int add(int x, int y) // add()的函数体在此定义
{
    return x + y;
}

当编译器遇到main中的add时,就能够大致了解add是什么样的了:有两个整型形参的返回值类型为整型的函数。

值得注意的是,函数原型中并不需要指定参数的名称,也可以这样声明函数:

int add(int, int);

但是我们习惯上都是要命名参数的,这样便于通过函数原型理解函数,否则就需要定位函数体的位置。

Tip:可以使用复制粘贴函数体处的语句来创建函数原型,别忘了在后面添加分号。

忘写函数体

也许有人会好奇,如果我们前置声明了函数却没有定义它会发生什么?

答案是:视情况而定。如果声明的函数并没有被调用,程序则可以编译通过并且能够正确运行。如果已经声明却未定义的函数被调用了,那么程序同样可以编译,但连接器却无法完成函数调用。(参看0.4开发流程简介

考虑下面的程序:

#include <iostream>
 
int add(int x, int y); // forward declaration of add() using function prototype
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl;
    return 0;
}

我们前置声明了add()函数却没有定义它,当使用VS2005编译此程序时,产生如下的提示:

Compiling…add.cppLinking…add.obj : error LNK2001: unresolved external symbol “int __cdecl add(int,int)” (?add@@YAHHH@Z)add.exe : fatal error LNK1120: 1 unresolved externals

可见,程序可以编译,但却在链接阶段失败了因为add()并没有被定义。

其他类型的前置声明

前置声明通常伴随着函数使用,但是前置声明也可以用于C++中的其他标识符,例如变量和用户自定义类型,只是语法不同。后续课程会继续讨论。

声明与定义

声明与定义是C++中常会听到的两个词语,他们是什么意思呢?现在你应当有足够的认识来理解两者的区别了。

定义实际上是实现或者说实例化(分配了相应内存空间)了标识符,下面是两个定义的例子:

int add(int x, int y) // 定义函数add()
{
   return x + y;
}

int x; // 实例化名为x的整型变量(内存分配了空间)

每个标识符只能有一个定义。定义是用来“满足”链接器要求的。

声明是定义标识符(变量/函数名)及其类型的语句,以下是两个声明的例子:

int add(int x, int y); // 声明一个名为add的函数,拥有两个整型形参,返回值类型为int,不包括函数体

int x; // 声明整型变量x

声明是用来“满足”编译器要求的,这就是为什么只使用前置声明而无定义也能编译通过。

你可能已经注意到了,int x;兼有两种类型,事实上,在C++中,所有的定义同时也能起到声明的作用,因为int x;是一个定义,所以其默认也是声明,这种情况适用于大多数声明。

但是有一小部分声明并非定义,例如函数原型,这些称为纯粹的声明(还包括变量、类声明、类型声明的前置声明)。一个标识符可以有任意数量的纯粹声明,但一个以上通常是冗余的。

小测验

1、函数原型与前置声明的区别是什么?

2、写出这个函数的函数原型。

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

3、说一说下面几个程序是无法编译?无法链接?还是既无法编译也无法链接?如果无法确定,可以尝试动手编译一下。

#include <iostream>
int add(int x, int y);
 
int main()
{
    using namespace std;
    cout << "3 + 4 + 5 = " << add(3, 4, 5) << endl;
    return 0;
}
 
int add(int x, int y)
{
    return x + y;
}

4、

#include <iostream>
int add(int x, int y);
 
int main()
{
    using namespace std;
    cout << "3 + 4 + 5 = " << add(3, 4, 5) << 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()
{
    using namespace std;
    cout << "3 + 4 + 5 = " << add(3, 4) << 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()
{
    using namespace std;
    cout << "3 + 4 + 5 = " << add(3, 4, 5) << 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); // 更好

int doMath(int, int, int, int);

3、无法编译,编译器会发现main函数中调用的add函数与前置声明中的add函数参数不匹配。

4、无法编译,理由同上。

5、无法链接,编译器发现main中的add与前置声明中的add匹配,编译通过。但链接器发现并没有一个有着两个形参的add函数被实现(只有一个有三个形参的add函数),因而链接失败。

6、正确。

关于 “1.7前置声明与定义” 的 1 个意见

评论关闭。