这是一个非常经典且重要的话题,很多初学者都会有这个疑问。

强烈不建议,并且在绝大多数情况下是错误的做法。
虽然在极少数非常特殊的情况下,它可能“看起来”能用,但这会破坏C语言的模块化设计原则,带来严重的、难以维护的副作用,现代C语言开发中,应该始终使用头文件(.h)来声明,并分别编译和链接源文件(.c)。
#include "xxx.c" 到底做了什么?
#include 指令的本质是“文本包含”或“复制粘贴”。
当编译器遇到 #include "mylib.c" 时,它不会去链接 mylib.c,而是会找到 mylib.c 这个文件,并将其原封不动地复制粘贴到当前 #include 指令的位置。

举个例子:
假设你有两个文件:
main.c
#include "mylib.c" // 这里把 mylib.c 的内容复制过来了
int main() {
my_function(); // 直接调用 mylib.c 中定义的函数
return 0;
}
mylib.c

// mylib.c 的内容
#include <stdio.h>
void my_function() {
printf("Hello from mylib!\n");
}
在编译 main.c 时,编译器实际处理的内容等价于:
编译器看到的“虚拟”文件
// 这是 mylib.c 的内容被复制过来的部分
#include <stdio.h>
void my_function() {
printf("Hello from mylib!\n");
}
// 下面是 main.c 原来的内容
int main() {
my_function(); // 直接调用 mylib.c 中定义的函数
return 0;
}
为什么这种做法是错误的和有害的?
尽管上面的例子可以成功编译和运行,但这隐藏了巨大的问题。
多重包含导致“重定义”错误
这是最常见、最致命的问题,C语言规定,一个变量或函数在一个编译单元(一个 .c 文件及其包含的所有头文件)中只能被定义一次。
考虑一个更复杂的场景:
mylib.c
#include "mylib.h" // 假设 mylib.h 中声明了 int global_var;
int global_var = 10; // 定义一个全局变量
void my_function() {
// ...
}
main.c
#include "mylib.c" // 复制了 mylib.c 的内容,包括 int global_var = 10;
#include "some_other_lib.c" // 假设这个文件也 #include "mylib.c" 了
int main() {
my_function();
return 0;
}
当你编译 main.c 时:
- 编译器首先处理
#include "mylib.c",int global_var = 10;被复制进来。 - 接着处理
#include "some_other_lib.c",这个文件又包含了mylib.c,int global_var = 10;再一次被复制进来。
在同一个 main.c 的编译单元中,global_var 被定义了两次,编译器会立刻报错:
error: redefinition of 'global_var'
note: previous definition of 'global_var' was here
即使你用宏保护(#ifndef)来防止头文件被重复包含,对 .c 文件是无效的,因为 #include "xxx.c" 是复制定义,而宏保护只能防止声明被重复包含。
破坏模块化和可重用性
C语言的核心思想之一是“分离声明与定义”。
- 头文件 (
.h):包含函数原型(声明)、宏定义、类型定义等,它像一个“说明书”或“接口”,告诉别人如何使用你的模块。 - 源文件 (
.c):包含具体的变量定义和函数实现,它是“实现细节”。
这种分离的好处是:
- 模块化:其他代码只需要
#include "mylib.h"就能知道如何使用my_function,而无需关心my_function是如何实现的。 - 可重用性:你可以将
mylib.c和mylib.h提供给多个不同的项目使用。 - 高效编译:当你修改
mylib.c的内部实现时,只需要重新编译mylib.c即可,所有依赖它的其他.c文件不需要重新编译(只要接口没变),如果使用#include "mylib.c",那么任何修改都会导致所有包含它的文件都重新编译,大大降低了编译效率。
隐藏的依赖关系
当你 #include "mylib.c" 时,你隐式地告诉了编译器:main.c 的正确性依赖于 mylib.c 的完整内容,这种依赖关系非常脆弱且不透明。mylib.c 中修改了一个内部使用的静态函数(static 函数),而这个函数恰好被 main.c 通过某种方式间接依赖了,你的代码可能会在毫无预警的情况下崩溃。
无法使用链接器进行优化
现代C项目通常采用“分别编译,然后链接”的模式:
gcc -c main.c-> 生成main.ogcc -c mylib.c-> 生成mylib.ogcc main.o mylib.o -o my_program-> 链接成最终的可执行文件
链接器在这个过程中扮演着至关重要的角色,它可以:
- 解析符号引用。
- 进行跨模块的优化(比如去冗余)。
- 链接静态库(
.a文件)或动态库(.so/.dll文件)。
而 #include "xxx.c" 的方式完全绕过了链接器,将所有代码都混在一个编译单元里,丧失了链接器带来的所有好处。
正确的做法是什么?
正确的做法是遵循C语言的标准范式:
-
创建头文件(
.h)用于声明mylib.h#ifndef MYLIB_H #define MYLIB_H // 函数声明 void my_function(); int add(int a, int b); // 可以 extern 全局变量声明 extern int global_var; #endif // MYLIB_H
-
创建源文件(
.c)用于定义mylib.c#include "mylib.h" // 包含自己的头文件,确保声明和定义一致 #include <stdio.h> // 变量定义 int global_var = 10; // 函数定义 void my_function() { printf("Hello from mylib!\n"); } int add(int a, int b) { return a + b; } -
在主程序中包含头文件,并编译链接所有
.c文件main.c#include <stdio.h> #include "mylib.h" // 只包含声明,告诉编译器 my_function 的样子 int main() { my_function(); printf("The result is: %d\n", add(5, 3)); printf("Global var is: %d\n", global_var); return 0; }
编译命令:
# 分别编译 gcc -c main.c -o main.o gcc -c mylib.c -o mylib.o # 链接 gcc main.o mylib.o -o my_program # 运行 ./my_program
有没有极少数的“合理”用法?
虽然极其罕见,但在一些非常老旧的代码库或特定的嵌入式开发环境中,你可能会偶尔看到这种用法,通常有以下几种情况:
- 非常简单的单文件工具或示例程序:为了代码的“自包含”,让一个
.c文件就能独立运行,作者可能会把其他小模块的实现直接#include进来,这牺牲了模块化,但方便了快速理解和运行。 - 一些老旧的构建系统:某些非常古老的、非标准的构建系统可能不支持多文件编译,开发者被迫将所有代码合并成一个文件。
- 一些宏技巧:在极少数情况下,开发者可能会利用
#include来加载一段由宏生成的代码模板。
即使在这些情况下,这也是一种“坏味道”(Code Smell),是应该被重构和摒弃的遗留代码。 对于任何现代项目,都应该坚持使用标准的模块化开发方法。
| 特性 | #include "xxx.h" (正确) |
#include "xxx.c" (错误) |
|---|---|---|
| 本质 | 包含声明 | 包含定义(文本复制) |
| 模块化 | 支持,清晰的接口与实现分离 | 破坏,代码耦合严重 |
| 编译效率 | 高,修改实现只需重编译单个.c文件 |
低,修改任何被包含的文件都会导致所有包含者重编译 |
| 可重用性 | 高,.h和.c可独立分发 |
低,.c被污染 |
| 多重包含 | 安全,通过宏保护(#ifndef)避免重定义 |
危险,极易导致“重定义”编译错误 |
| 依赖关系 | 清晰,依赖的是公开的接口 | 隐藏,依赖的是实现细节 |
| 链接器 | 利用链接器进行最终链接和优化 | 绕过链接器 |
请永远不要在 .c 文件中 #include 另一个 .c 文件。 坚持使用 .h 文件进行声明,并让编译器和链接器来完成它们的工作,这是编写健壮、可维护、高效C代码的基石。
