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. 头文件组织原则

❓ 常见问题

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。

📖 小节

📝 作业

  1. 创建一个包含 3 个文件的项目:string_utils.hstring_utils.cmain.c,实现字符串反转和统计字符出现次数两个函数,在 main 中调用测试
  2. 为第 1 题编写 Makefile,实现 make 编译和 make clean 清理
  3. 在头文件中故意不加重复包含保护,然后在 main.c 中 #include 同一头文件两次,观察编译错误
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏