- 可以包含,但不推荐:C语言语法允许你使用
#include来包含一个.c文件(源文件)。 - 为什么可以?:预处理器(Preprocessor)在编译前,会将被包含文件的内容原封不动地粘贴到
#include指令的位置。#include "my_func.c"在效果上等同于把my_func.c文件的所有代码复制粘贴到当前文件中。 - 为什么不推荐?:这会导致多重定义错误,是C/C++编程中的一个大忌,因为一个程序中不能有两个或以上同名的函数定义或全局变量定义。
- 正确做法是什么?:头文件(
.h)只包含声明,.c文件包含定义,然后通过编译器将多个.c文件一起编译链接成一个可执行文件。
详细解释
#include 的工作原理
#include 是C语言预处理器的一个指令,它的工作方式非常简单粗暴:

当编译器处理到 #include "header.h" 或 #include <header.h> 时,预处理器会找到对应的文件,并将其复制粘贴到 #include 指令所在的位置。
这个过程发生在编译之前,预处理器处理完所有的 #include 指令后,会生成一个临时的、合并后的巨大源文件,然后编译器才开始对这个合并后的文件进行语法分析和编译。
包含 .c 文件的“陷阱”
让我们通过一个具体的例子来看看为什么直接包含 .c 文件是危险的。
假设我们有以下三个文件:

main.c
#include <stdio.h>
// 尝试包含一个.c文件
#include "my_functions.c"
int main() {
print_message();
return 0;
}
my_functions.c
// 函数定义
void print_message() {
printf("Hello from my_functions.c!\n");
}
my_functions.h (这个文件我们暂时不需要,但为了完整性先列出来)
// 函数声明 void print_message();
我们来编译 main.c:
gcc main.c -o my_program
编译过程如下:
-
预处理阶段:预处理器看到
#include "my_functions.c",于是将my_functions.c的内容(即void print_message() { ... })复制粘贴到main.c中。 -
编译器现在看到的“有效”代码是:
#include <stdio.h> // 这里是粘贴过来的内容 void print_message() { printf("Hello from my_functions.c!\n"); } int main() { print_message(); return 0; } -
编译阶段:编译器成功编译了上述代码,没有问题。
-
链接阶段:链接器开始工作,它发现:
- 在
main.c中,有一个函数print_message()的定义。 - 它也找到了
my_functions.c这个独立的源文件,并且里面也包含了一个print_message()函数的定义。
链接器懵了:同一个程序中出现了两个
print_message()的定义!它不知道应该使用哪一个,它会报错,错误信息通常是:/tmp/ccXXXXXX.o:(.text+0x0): multiple definition of `print_message'; my_functions.c:(.text+0x0): first defined here collect2: error: ld returned 1 exit status这个错误就是多重定义错误。
- 在
即使只有一个文件包含,也可能出错:
my_functions.c 中还定义了一个全局变量:
// my_functions.c
int global_counter = 0;
void print_message() {
printf("Hello! Counter is: %d\n", global_counter);
}
然后在 main.c 中包含它:
// main.c
#include "my_functions.c"
int main() {
print_message();
global_counter = 10;
print_message();
return 0;
}
编译 main.c 时,因为 global_counter 的定义被包含了两次(一次在 main.c 的“粘贴”代码中,一次在 my_functions.c 文件中),同样会报“多重定义”错误。
正确的C项目组织方式
为了避免上述问题,C语言社区约定俗成了一套标准的项目组织方法:
原则:声明与定义分离
-
头文件 (
.h文件):存放声明。- 函数原型(如
int add(int a, int b);) extern全局变量声明(如extern int global_counter;)- 宏定义、结构体定义、
typedef等。 - 作用:告诉其他模块“我有哪些函数/变量可供你使用”,但不提供具体实现。
- 函数原型(如
-
源文件 (
.c文件):存放定义。- 函数的完整实现(如
int add(int a, int b) { return a + b; }) - 全局变量的实际定义(如
int global_counter = 0;)。 - 作用:提供具体的代码实现。
- 函数的完整实现(如
正确的实践示例
我们重构之前的例子:
my_functions.h (头文件,只包含声明)
#ifndef MY_FUNCTIONS_H #define MY_FUNCTIONS_H // 函数声明 void print_message(); // extern 全局变量声明 extern int global_counter; #endif // MY_FUNCTIONS_H
my_functions.c (源文件,包含定义)
#include "my_functions.h" // 包含自己的头文件是好习惯
// 函数定义
void print_message() {
printf("Hello! Counter is: %d\n", global_counter);
}
// 全局变量定义
int global_counter = 0;
main.c (主程序)
#include <stdio.h>
#include "my_functions.h" // 只包含头文件,获取声明
int main() {
print_message();
global_counter = 10;
print_message();
return 0;
}
如何编译?
我们需要将所有 .c 文件一起交给编译器:
gcc main.c my_functions.c -o my_program
编译过程:
- 预处理:
- 对
main.c:#include "my_functions.h"被替换,main.c有了print_message()的声明和global_counter的extern声明。 - 对
my_functions.c:#include "my_functions.h"被替换,my_functions.c也有了这些声明。
- 对
- 编译:
- 编译器编译
main.c,知道print_message()和global_counter存在,但不知道它们在哪,所以只生成调用它们的指令。 - 编译器编译
my_functions.c,找到了print_message()的定义和global_counter的定义,并生成相应的机器码。
- 编译器编译
- 链接:
- 链接器将
main.c和my_functions.c编译出的目标文件(.o文件)链接在一起。 - 它发现
main.c调用的print_message()和使用的global_counter,在my_functions.c的目标文件中有对应的定义。 - 链接器将它们正确地关联起来,生成最终的可执行文件
my_program。
- 链接器将
这种方式的优点:
- 避免多重定义:每个函数和全局变量的定义只存在于一个
.c文件中。 - 清晰的依赖关系:通过
#include头文件,可以清楚地看到模块间的接口。 - 高效的编译:修改
my_functions.c后,只需要重新编译my_functions.c和依赖它的文件,而不需要重新编译整个项目(现代构建工具如make会自动处理这个依赖关系)。
特殊情况:包含 .c 文件的“正当理由”?
虽然不推荐,但在极少数特定情况下,人们可能会这么做:
- 单文件实现:为了将一个模块的所有代码(声明和定义)打包成一个单一的
.c文件,方便分发或嵌入到其他项目中,使用者只需#include "this_module.c"即可使用,这牺牲了模块化,但简化了使用。 - 模板代码:在C++中,模板代码必须在编译时看到其定义,因此通常直接包含
.h文件(.h文件里是模板定义),在C语言中没有模板,但有些宏技巧(如泛型编程)也要求定义必须可见,因此可能会在一个头文件中包含实现。
即使在这些情况下,最佳实践仍然是:
- 将实现放在
.c文件中。 - 提供一个
.h文件,只包含声明,并让用户包含这个.h文件。 - 在项目的构建系统中,将
.c文件作为源文件进行编译,而不是通过#include来包含。
| 特性 | #include "header.h" (推荐) | #include "source.c" (不推荐) |
| :--- | :--- | :--- || 声明 (函数原型, extern 变量) | 定义 (函数体, 变量定义) |
| 编译方式 | 包含头文件,然后编译所有 .c 文件 | 直接包含 .c 文件,通常只编译这一个文件 |
| 链接 | 链接器将多个目标文件链接,正确匹配声明和定义 | 导致多重定义错误 |
| 项目结构 | 清晰,模块化,易于维护 | 混乱,耦合度高,难以维护 |
| 适用场景 | 所有标准C/C++项目 | 极少数单文件分发或特殊宏技巧 |
永远不要在 .c 文件中使用 #include 来包含另一个 .c 文件,请坚持使用 .h 文件进行声明,并通过编译器将多个 .c 文件一起编译链接。
