许多人在学习 C++ 时,似乎对声明和定义的区别及其目的感到困惑。特别是关于函数或类。为了理解所有这些看似迂腐和重复的东西的必要性,让我们回顾一下构建过程。
首先,每个 .cpp 文件都会被独立编译。在此过程中,任何 #include 指令都会首先将包含的文件插入到 .cpp 中。然后,编译器会从头到尾处理 .cpp 文件,生成机器语言代码,并将其输出到 .obj 文件。为了本次讨论的目的,需要记住的是,这是一个从头到尾的单次遍历过程,并且在任何时候,它都完全不知道其他 .cpp 文件中的任何内容。编译器甚至在处理当前 .cpp 文件的任何一点之上,都不知道文件中的任何内容。编译器生成的代码并不能完全解析内存地址和函数调用。那是链接器的任务。当编译器在 .cpp 文件中遇到函数调用时,它通常不知道该函数做什么,也不知道它是如何做的。编译器唯一需要知道的是传递了哪些参数以及返回值的类型。函数调用的 .obj 代码可以生成,然后链接器将在稍后确定确切的跳转位置。
现在,记住所有这些,我们可以讨论真正的主题。
声明: 声明将一个名称引入一个作用域。总的来说,作用域可以是整个 .cpp 文件,也可以是代码中由 {} 界定的任何内容,无论是函数、函数内的循环,甚至是在函数内任意放置的 {} 块。引入的名称在其声明点到该作用域的结束点内都是可见的。声明只是告诉编译器如何使用某个实体,它实际上并没有创建任何东西。
1 2
|
extern int y; // declares y, but does not define it. y is defined elsewhere,
// but the program can now use it since it knows what it is (an integer)
|
原型: 原型只是函数声明的另一种说法。
|
double someFunction( double, int );
|
定义: 定义完全指定一个实体。定义是实体在内存中实际创建的地方。所有定义也是声明,但并非所有声明都是定义。
|
Int x; // declares and defines x. it is a definition because it creates the variable allocating memory
|
实现: 实现是函数定义的另一种说法。也就是说,实现是函数本身的实际代码。
1 2 3 4
|
double someFunction( double x, int y )
{
return x * y;
}
|
任何实体都可以被多次声明,但只能被定义一次。
为什么这一切对您很重要
1 2 3 4 5 6 7 8 9 10 11 12 13
|
int main()
{
double a = 3.5;
int b = 2;
double c;
c = someFunction( a, b );
}
double someFunction( double x, int y )
{
return x * y;
}
|
回想一下,编译是一个单次的自上而下的过程。因为这个原因,上面的代码无法工作,因为编译器在到达 "c = someFunction( a, b );" 行时,并不知道如何处理 someFunction()。解决方案是在此之前声明 someFunction()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
double someFunction( double, int );
int main()
{
double a = 3.5;
int b = 2;
double c;
c = someFunction( a, b );
}
double someFunction( double x, int y )
{
return x * y;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13
|
double someFunction( double x, int y )
{
return x * y;
}
int main()
{
double a = 3.5;
int b = 2;
double c;
c = someFunction( a, b );
}
|
由于定义也是声明,所以以上两种方法都可以。第一种使用了原型,而第二种只是将 someFunction() 的定义移到了 main() 中调用它的前面。作为一般规则,使用原型是首选方法,因为它允许更好地组织代码(您不必从底部开始向上阅读),并且可以防止在代码重新组织时引入错误。此外,请考虑一组递归函数,其中每个函数都调用其他函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
funcA()
{
if( condition )
funcB();
return;
}
funcB()
{
if( condition )
funcC();
return;
}
funcC()
{
if( condition )
funcA();
return;
}
|
这对于新手来说可能有点奇怪,但在某些类型的算法中,如排序和语法解析,这是相当常见的。需要注意的是,没有任何方法可以在没有至少一个原型的情况下组织这些函数。所以,请给自己一个方便,养成使用原型的习惯。
关于 .h 和 .cpp 组织的一个说明
以上所有内容都直接与您应如何将代码组织到头文件和源文件相关。大多数学习 C++ 的学生在学习的第一个月左右可能不会编写足够大的程序来需要多个源文件。但当他们需要时,我看到很多人对哪些内容应该放在哪个文件中感到困惑。经验法则是这样的:头文件应该包含
声明,源文件应该包含
定义。原因现在应该很清楚了。一个实体可以被多次声明,但只能被定义一次。如果头文件包含定义,您可能会导致同一个实体被多次定义。这很可能会在链接过程中导致问题,因为链接器试图解析外部地址时会发现多个同名的实体。
有关头文件组织的更深入讨论,请参阅
Disch 的这篇文章。