C: 多文件编程
多文件编程就像团队协作——声明是契约(头文件),实现是干活(源文件),extern 是跨部门引用,Makefile 是项目进度表。
1. 为什么需要多文件
小程序一个 .c 文件就够了,但真实项目代码量大,需要按功能拆分到多个文件:
- 逻辑清晰,职责分明
- 编译加速——改一个文件只重编该文件
- 代码复用——通用模块可被多个项目使用
- 团队协作——不同人负责不同模块
2. 头文件编写
头文件(.h)放声明,源文件(.c)放定义,这是 C 语言最基本的模块化规则。
(1) 头文件内容
C
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
double average(int *arr, int n);
#endif
(2) 源文件内容
C
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
double average(int *arr, int n) {
if (n <= 0) return 0.0;
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return (double)sum / n;
}
(3) 主程序
C
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("3 + 5 = %d\n", add(3, 5));
printf("10 - 4 = %d\n", subtract(10, 4));
int scores[] = {80, 90, 75, 88};
printf("平均: %.1f\n", average(scores, 4));
return 0;
}
💡 头文件只放声明(函数原型、类型定义、宏、extern 声明),不放定义(函数体、变量初始化)。重复定义会导致链接错误。
3. extern 跨文件引用
一个 .c 文件定义的全局变量,其他文件用 extern 声明即可访问。
(1) 定义(在某个 .c 中)
C
int g_count = 0;
(2) 声明(在头文件或其他 .c 中)
C
extern int g_count;
extern 告诉编译器"这个变量在别处定义,这里只声明",不分配新空间。
▶ 示例
项目结构:
TEXT
📖 仅展示
project/
├── counter.h
├── counter.c
└── main.c
counter.h:
C
#ifndef COUNTER_H
#define COUNTER_H
extern int g_count;
void increment(void);
int get_count(void);
#endif
counter.c:
C
#include "counter.h"
int g_count = 0;
void increment(void) {
g_count++;
}
int get_count(void) {
return g_count;
}
main.c:
C
#include <stdio.h>
#include "counter.h"
int main(void) {
increment();
increment();
increment();
printf("count = %d\n", get_count());
printf("g_count = %d\n", g_count);
return 0;
}
TEXT
📖 仅展示
count = 3
g_count = 3
⚠️
extern 声明不要带初始化。extern int g_count = 0; 在某些编译器上会变成定义,造成重复定义错误。
4. 多文件编译命令
(1) 逐个编译再链接
BASH
gcc -c counter.c -o counter.o
gcc -c main.c -o main.o
gcc counter.o main.o -o myapp
-c 只编译不链接,生成 .o 目标文件。最后把所有 .o 链接成可执行文件。
(2) 一次性编译
BASH
gcc counter.c main.c -o myapp
简单但改任何文件都要全部重编,不适合大项目。
💡 逐个编译的优势:改
main.c 只需 gcc -c main.c 再链接,不用重编 counter.c。
5. Makefile 入门
Makefile 自动管理编译规则,只重编修改过的文件。
MAKEFILE
CC = gcc
CFLAGS = -Wall -g
myapp: main.o counter.o
$(CC) $(CFLAGS) main.o counter.o -o myapp
main.o: main.c counter.h
$(CC) $(CFLAGS) -c main.c -o main.o
counter.o: counter.c counter.h
$(CC) $(CFLAGS) -c counter.c -o counter.o
clean:
rm -f *.o myapp
⚠️ Makefile 中缩进必须用 Tab,不能用空格。这是初学者最常见的错误。
使用方式:
BASH
make
make clean
Makefile 规则格式:
MAKEFILE
目标: 依赖列表
命令
- 目标:要生成的文件名
- 依赖:目标依赖的文件,任何一个比目标新就重新执行命令
- 命令:如何从依赖生成目标
6. 防止重复包含
头文件可能被多个源文件包含,同一个源文件也可能间接包含多次。不加保护会重复声明导致错误。
(1) 方式一:头文件保护符(Include Guard)
C
#ifndef MYHEADER_H
#define MYHEADER_H
void my_func(void);
#endif
第一次包含时 MYHEADER_H 未定义,进入并定义它;再次包含时 #ifndef 为假,跳过整个内容。
(2) 方式二:#pragma once
C
#pragma once
void my_func(void);
简洁,大多数编译器支持。但不是标准,理论上不如 Include Guard 可移植。
💡 实际项目中两种都有用,
#pragma once 更简洁,Include Guard 更标准。选一种坚持用即可。
7. 头文件组织原则
- 每个
.c对应一个同名的.h .h只放声明,不放定义.c文件第一行包含自己的.h(验证声明与定义一致)- 头文件必须加防止重复包含保护
extern全局变量声明放.h,定义放某个.c
❓ 常见问题
Q 头文件里能放变量定义吗?
A 不应该。头文件被多个 .c 包含会导致重复定义。变量定义放 .c,用 extern 在 .h 中声明。
Q extern 和直接 include 有什么区别?
A include 把整个头文件内容插入;extern 是显式声明某个变量或函数在别处定义。头文件中通常包含 extern 声明,两者配合使用。
Q Makefile 缩进必须用 Tab 吗?
A 是的,这是 Make 的硬性规定。用空格会报错。编辑器中建议设置 Makefile 文件用 Tab 缩进。
Q #pragma once 和 Include Guard 选哪个?
A 项目内统一选一种即可。#pragma once 简洁但非标准;Include Guard 标准但稍繁琐。大型开源项目多用 Include Guard。
📖 小节
- 头文件(.h)放声明,源文件(.c)放定义,分离是模块化的基础
extern声明跨文件的全局变量和函数,告诉编译器"定义在别处"- 多文件编译推荐"逐个编译 -c 再链接",只重编修改过的文件
- Makefile 自动管理编译依赖,避免手动输入长命令
- 防止重复包含用 Include Guard 或
#pragma once
📝 作业
- 创建一个包含 3 个文件的项目:
string_utils.h、string_utils.c、main.c,实现字符串反转和统计字符出现次数两个函数,在 main 中调用测试 - 为第 1 题编写 Makefile,实现
make编译和make clean清理 - 在头文件中故意不加重复包含保护,然后在 main.c 中
#include同一头文件两次,观察编译错误