一、能不能?
稍微了解一点C\C++的朋友都知道#include
关键字
现在有两个文件:file1.c和file2.c,它们的内容如下:
// file1.c
#include <stdio.h>
void function(){
printf("file1 : hello world!\n");
}
//file2.c
#include "file2.c"
int main(){
function();
printf("file2 : hello world!\n");
return 0;
}
这时有一个问题:上述代码能不能通过编译并生成可执行文件?为什么?
实践是检验真理的唯一标准,能否通过编译并且生成可执行文件,我们可以试着编译它们
二、实践出真理
-
编译器:gcc 8.3.0
-
编译环境:debian 10 for Windows
-
执行结果:
-
从报错信息我们可以得知file1.c和file2.c这两个文件都通过编译生成了相对应的.o文件,但是在链接这个阶段时发生了重定义错误,将编译阶段分步执行可以更清晰地看出链接阶段的错误:
-
究其原因便是我们在file1.c和file2.c两个文件中均定义了函数
function()
,熟悉C\C++的朋友应该记得#include
关键字的作用:将被包含文件中的内容拷贝到包含文件,我们分别看看这两个源文件在预处理后生成的内容: -
可以清晰地看到,在两个源文件进行预处理后均存在了函数
function()
,在针对它们单独编译-汇编的过程中编译器还能够正常处理,可到了最后将两个对象文件链接并可执行文件时,由于它们均生成了重复符号function
而导致出现重定义错误- 那么问题来了,假设我们删除掉某个.i中function函数的定义是否可以编译成功?
- 那么问题来了,假设我们删除掉某个.i中function函数的定义是否可以编译成功?
-
在将file2.i中的
function()
定义改为声明后,便能够将它们最后生成的对象文件链接并生成可执行文件,那么反过来只对file2.c进行编译是否能够生成可执行文件呢?前面我们有提到#include
关键字的工作原理是将被包含文件拷贝到包含文件中,根据这一原理,可以推断出在我们编译file2.c的时候实际上就已经编译了file1.c,因为在预处理过程中file1.c的文件内容已经被拷贝至file2.c,这也解释了为什么一开始编译会在链接时报错——编译器实际上编译了两次file1.c!!- 只编译file2.c:
- 这种方法属不属于未定义行为?在其他编译器中是否也能实现这样的操作?接下来,我们可以用微软的Visual Studio重复以上步骤:
- (仅在工程中加入file2.c,但file1.c与file2.c存在同一目录下)
运行结果也证实了我们的猜想:当file2.c包含file1.c时只对file2.c进行编译便可以生成可执行文件
- 这种方法属不属于未定义行为?在其他编译器中是否也能实现这样的操作?接下来,我们可以用微软的Visual Studio重复以上步骤:
- 只编译file2.c:
三、结论
-
回到一开始的问题:源文件包含源文件能成功编译并生成可执行文件吗?答案显而易见,那是否只对main函数所在的源文件进行编译就一定能生成可执行文件?
-
我们再来看一个例子,现在有4个源文件:file1.c,file2_1.c,file2_2.c,file3.c,它们各自的代码如下:
//file1.c #include <stdio.h> void function(){ printf("hello world!!\n"); }
//file2_1.c #include "file1.c"
//file2_2.c #include "file1.c"
//file3.c #include "file2_1.c" #include "file2_2.c" int mian(){ function(); return 0; }
-
根据#include的工作原理,在预处理阶段file3.c中的
#include
会替换为file_1.c和file_2.c中的内容,而二者分别#include了file1.c;看到这里,学过面向对象的朋友是否想起了一个名词:菱形继承,根据#include
的原理,最后预处理生成的file3.i文件结构大致如下://file2_1.c void function(){ printf("hello world!!\n"); } //file2_2.c void function(){ printf("hello world!!\n"); } //file3.c int mian(){ function(); return 0; }
-
很明显,在这种“菱形继承”的情况下,只对main函数所在的源文件进行编译依旧会出现重定义的错误,在菱形继承问题中,C++所使用的解决方法是声明虚基类;那么针对文件重定义是否有相似的方法呢?很遗憾,C\C++并没有什么“虚文件”的说法,但是我们有
#ifndef
和#pragma once
啊!这二者我们每次编写头文件时都会用到- 修改file1.c文件为:
//file1.c #include <stdio.h> #ifndef __FILE_ONE__ #define __FILE_ONE__ void function(){ printf("hello world!!\n"); } #endif
- 根据
#include
和#ifndef
的工作原理,我们可以写出预处理阶段时file3.i的大致结构://file2_1.c void function(){ printf("hello world!!\n"); } //file2_2.c //file3.c int mian(){ function(); return 0; }
- 修改file1.c文件为:
-
gcc预处理生成的文件也证明了加上
#ifndef
语句后便可以防止重复定义函数function()
,因此,为被包含的源文件添加预处理命令#ifndef
之后对main所在的源文件进行单独编译是可以生成可执行文件的 -
回到最早的问题:C\C++能否在源文件中包含源文件?答案是肯定的,但这么做的前提是你必须为被包含的源文件添加
#ifndef
语句,并且只能对主函数所在源文件进行编译。若参与项目开发的不止一个开发者,那就不应该在源文件中包含源文件——因为你不清楚其他的开发者是否会对源文件进行#ifndef
的预处理(事实上绝大多数人并不会这样做……),也无法确定这个项目最后是否只对主函数进行编译(绝大多数项目也不会这么搞……) -
前面我们提到预处理命令
#ifndef
以及#pragma once
,通常情况下我们在头文件中都会使用这两条预处理命令以防止发生发生重定义错误,但是即使使用预处理命令,我们在头文件中直接定义函数,并且由多个源文件包含该头文件时依旧会发生重定义错误,这又是为什么呢?
四、为什么不能在头文件中定义函数?
-
我们可以写一个实例:
//sum.h #ifndef __SUM_H__ #define __SUM_H__ #include <stdio.h> //位于output.c中的函数声明: void output_sum(int start, int end); //sum(): 求start到end之间整数的和(包含start和end) int sum(int start, int end){ int result = start++; while(start <= end) result += start++; return result; } #endif
//output.c #include "sum.h" void output_sum(int start, int end){ printf("%d + %d+...+%d + %d = %d\n", start, start + 1, end - 1, end, sum(start, end)); }
//main.c #include "sum.h" #define NUMBER_START 1 #define NUMBER_END 10 int main(){ output_sum(NUMBER_START, NUMBER_END); return 0; }
-
用gcc执行编译,有没有发现,这个报错信息简直和我们一开始编译file1.c和file2.c时如出一辙!
-
还记得编译器预处理时是如何处理
#include
的吗?#include
关键字的工作原理是将被包含文件拷贝到包含文件中,根据这一原理,我们可以分别写出预处理阶段过后output.i与main.i的大致结构://output.i //sum.h void output_sum(int start, int end); int sum(int start, int end){ int result = start++; while(start <= end) result += start++; return result; } //output.c void output_sum(int start, int end){ printf("%d + %d+...+%d + %d = %d\n", start, start + 1, end - 1, end, sum(start, end)); }
//main.i //sum.h void output_sum(int start, int end); int sum(int start, int end){ int result = start++; while(start <= end) result += start++; return result; } //main.c int main(){ output_sum(1, 10); return 0; }
-
在对main.c与output.c进行预处理之后分别生成的文件中都定义了函数
sum()
,这一幕是不是似曾相识?前面我们对file1.c与file2.c分别进行编译时也出现两个function()
!即两个编译单元中出现了相同的符号!因此,在最后的阶段,必然会发生重定义错误! -
那么为什么同样都是在多个文件中被编译多次,函数声明、结构体定义就不会导致出现重定义错误,而函数定义、变量(对象)定义就会导致重定义错误?
-
笔者认为,函数声明、结构体定义这类操作就好比于你告诉别人存在小明这个人,此时他上并不存在实体;而函数定义、变量(定义)就相当于你把小明拉到别人面前,这时候小明是有实体的;你可以在同一时间告诉不同地点的人:存在小明这个人,但是你无法在同一时间不同地点让小明与其他人有实体接触