012-C语言预处理与头文件

plana_lab 发布于 15 天前 112 次阅读



前面我们已经学习了:

数组
函数
指针
结构体
动态内存
文件操作

随着程序越来越大,会遇到一个问题:

一个 C 文件
↓
几百行
↓
几千行
↓
越来越难维护

例如:

void print_student(...);
void add_score(...);
void save_student(...);
void load_student(...);

如果全部写在:

main.c

里面,代码会越来越乱。

这时候就需要:

预处理器
+
头文件
+
多个 .c 文件

一、什么是预处理

C 程序在真正编译之前,会先经过一个:

预处理阶段。

可以简单理解成:

.c 源代码
   ↓
预处理
   ↓
预处理后的代码
   ↓
编译
   ↓
目标文件
   ↓
链接
   ↓
可执行文件

预处理器主要处理:

#

开头的指令。

例如:

#include <stdio.h>
#define PI 3.14159

这些都属于:

预处理指令。


二、预处理指令

常见的预处理指令:

#include
#define
#undef
#if
#ifdef
#ifndef
#else
#elif
#endif

后面还会看到:

#pragma
#error

本章先掌握主要部分。


三、#include

前面经常看到:

#include <stdio.h>

它实际上就是:

让预处理器把指定的头文件内容包含进当前源文件。

例如:

#include <stdio.h>

可以理解成:

main.c
 +
stdio.h
 ↓
预处理后的源代码

所以 #include 并不是普通的 C 函数。

它是:

预处理指令。


四、为什么需要 #include

例如:

printf("Hello\n");

printf() 的声明在:

stdio.h

里面。

因此通常需要:

#include <stdio.h>

同样:

malloc()

需要:

#include <stdlib.h>

例如:

#include <stdio.h>
#include <stdlib.h>

五、系统头文件和自己的头文件

常见的系统头文件:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

使用:

< >

例如:

#include <stdio.h>

而自己写的头文件通常使用:

" "

例如:

#include "student.h"

可以简单记:

系统头文件
↓
#include <stdio.h>

自己的头文件
↓
#include "student.h"

六、头文件是什么

头文件通常使用:

.h

作为扩展名。

例如:

student.h
math_utils.h
config.h

头文件通常用来放:

函数声明
结构体定义
typedef
宏
常量
枚举
类型声明

例如:

// student.h

typedef struct {
    char name[50];
    int age;
    double score;
} Student;

void print_student(Student *p);
void add_score(Student *p, double score);

七、函数声明放进头文件

假设有一个函数:

void hello() {
    printf("Hello\n");
}

如果其他 .c 文件需要使用这个函数,可以把:

void hello();

放到:

hello.h

例如:

// hello.h

void hello();

然后:

// hello.c

#include <stdio.h>
#include "hello.h"

void hello() {
    printf("Hello\n");
}

最后:

// main.c

#include "hello.h"

int main() {
    hello();

    return 0;
}

这样:

main.c
 ↓
#include "hello.h"
 ↓
知道 hello() 的声明
 ↓
调用 hello()

真正的函数实现则在:

hello.c

里面。


八、声明和定义

这个概念非常重要。

例如:

void hello();

这是:

声明(declaration)

表示:

有一个叫 hello 的函数
它的参数和返回值是这样

而:

void hello() {
    printf("Hello\n");
}

这是:

定义(definition)

真正提供函数实现。

可以简单记:

声明
↓
告诉编译器“有这个东西”

定义
↓
真正创建/实现这个东西

九、为什么头文件放声明

假设:

main.c
hello.c
hello.h

其中:

hello.h
↓
声明

hello.c
↓
实现

main.c
↓
使用

结构:

main.c
   │
   └── include
          ↓
       hello.h
          ↓
       知道函数声明
          
hello.c
   │
   └── 实现函数

这样就把代码拆开了。


十、一个完整的多文件例子

目录:

project/
├─ main.c
├─ student.c
└─ student.h

student.h:

#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[50];
    int age;
    double score;
} Student;

void print_student(Student *p);
void add_score(Student *p, double score);

#endif

student.c:

#include <stdio.h>
#include "student.h"

void print_student(Student *p) {
    printf("姓名: %s\n", p->name);
    printf("年龄: %d\n", p->age);
    printf("成绩: %.2f\n", p->score);
}

void add_score(Student *p, double score) {
    p->score += score;
}

main.c:

#include "student.h"

int main() {

    Student s = {
        "Alice",
        18,
        95.5
    };

    print_student(&s);

    add_score(&s, 2.0);

    print_student(&s);

    return 0;
}

现在:

main.c
↓
负责主程序

student.c
↓
负责学生相关函数

student.h
↓
负责对外提供声明

这就是最基础的:

多文件编程。


十一、#define

#define 用来定义:

宏(macro)

例如:

#define PI 3.14159

之后:

printf("%f\n", PI);

预处理阶段会把:

PI

替换成:

3.14159

可以简单理解为:

#define PI 3.14159

PI
↓
3.14159

十二、为什么使用 #define

例如:

#define MAX_SIZE 100

之后:

int arr[MAX_SIZE];

如果以后想改成:

200

只需要修改:

#define MAX_SIZE 200

而不用到处寻找:

100

十三、宏常量

常见写法:

#define MAX_STUDENTS 100
#define NAME_SIZE 50
#define PI 3.1415926

例如:

char name[NAME_SIZE];

这样可以提高代码的可维护性。


十四、#define 和变量的区别

例如:

#define MAX_SIZE 100

和:

int max_size = 100;

并不是一回事。

#define:

预处理阶段
↓
文本替换

变量:

编译后
↓
真正存在于程序的数据

例如:

#define MAX_SIZE 100

它本身并不是一个 int 变量。


十五、宏函数

#define 还可以定义带参数的宏。

例如:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

然后:

printf("%d\n", MAX(10, 20));

结果:

20

十六、为什么宏需要括号

不要随意写:

#define SQUARE(x) x * x

然后:

SQUARE(2 + 3)

展开以后可能变成:

2 + 3 * 2 + 3

结果不是:

25

而是:

11

更安全的写法:

#define SQUARE(x) ((x) * (x))

这样:

SQUARE(2 + 3)

展开为:

((2 + 3) * (2 + 3))

结果:

25

十七、宏的另一个问题

例如:

#define SQUARE(x) ((x) * (x))

然后:

int i = 3;

SQUARE(i++);

展开后相当于:

((i++) * (i++))

i 被增加了两次。

所以:

宏函数并不等同于普通函数。

宏只是预处理阶段的替换。


十八、宏和函数怎么选

普通计算通常推荐使用:

int max(int a, int b);

而不是到处使用复杂宏。

#define 更适合:

简单常量
条件编译
头文件控制
一些确实需要预处理器的场景

例如:

#define MAX_STUDENTS 100

就很常见。


十九、#undef

可以取消一个宏:

#define MAX_SIZE 100
#undef MAX_SIZE

之后:

MAX_SIZE

就不再是这个宏。

一般使用不多,但应该知道它存在。


二十、条件编译

预处理器还可以根据条件决定:

哪些代码参与编译。

例如:

#ifdef DEBUG

printf("debug\n");

#endif

如果定义了:

DEBUG

那么代码会保留。

如果没有定义:

这段代码不会进入后面的编译过程

二十一、#ifdef

例如:

#define DEBUG

#ifdef DEBUG
printf("Debug Mode\n");
#endif

因为:

DEBUG

已经被定义,所以:

printf("Debug Mode\n");

会参与编译。


二十二、#ifndef

#ifndef:

if not defined

意思是:

如果没有定义。

例如:

#ifndef DEBUG
printf("DEBUG 没有定义\n");
#endif

二十三、头文件保护

这是头文件非常重要的内容。

例如:

#ifndef STUDENT_H
#define STUDENT_H

// 头文件内容

#endif

这叫:

头文件保护(include guard)


二十四、为什么需要头文件保护

假设:

#include "student.h"
#include "student.h"

同一个头文件可能被重复包含。

如果头文件里面有:

typedef struct {
    ...
} Student;

就可能出现重复定义问题。

于是使用:

#ifndef STUDENT_H
#define STUDENT_H

...

#endif

逻辑是:

第一次包含
↓
STUDENT_H 没有定义
↓
定义 STUDENT_H
↓
处理头文件内容

第二次包含
↓
发现 STUDENT_H 已经定义
↓
跳过内容

二十五、标准头文件为什么经常不用自己写保护

你可能会看到:

#include <stdio.h>

却没有看到你自己写:

#ifndef

这是因为标准库头文件本身已经按照相应方式处理了重复包含问题。

我们自己写头文件时,应该养成:

#ifndef XXX_H
#define XXX_H

...

#endif

的习惯。


二十六、include guard 的命名

例如:

student.h

可以:

#ifndef STUDENT_H
#define STUDENT_H

例如:

math_utils.h

可以:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

常见习惯:

文件名
↓
转大写
↓
. 改成 _
↓
加 _H

二十七、条件编译中的 #else

例如:

#ifdef DEBUG

printf("Debug\n");

#else

printf("Release\n");

#endif

表示:

DEBUG 已定义
↓
Debug

DEBUG 没定义
↓
Release

二十八、#if

也可以使用:

#if

例如:

#define VERSION 2

#if VERSION >= 2
printf("Version 2\n");
#endif

预处理器会判断这个条件。


二十九、#elif

可以有多个条件:

#if VERSION == 1

printf("Version 1\n");

#elif VERSION == 2

printf("Version 2\n");

#else

printf("Other Version\n");

#endif

结构:

#if
 ↓
条件 1

#elif
 ↓
条件 2

#else
 ↓
其他情况

#endif

三十、defined

可以使用:

defined

判断宏是否存在。

例如:

#if defined(DEBUG)
printf("Debug\n");
#endif

也可以写:

#ifdef DEBUG

两者在很多简单场景下效果相近。


三十一、多个配置

例如:

#define WINDOWS

然后:

#ifdef WINDOWS

printf("Windows\n");

#elif defined(LINUX)

printf("Linux\n");

#endif

这样可以根据不同平台编译不同代码。


三十二、预处理器不是编程语言运行时

例如:

#define A 100

并不是程序运行以后:

A = 100

而是在程序正式编译之前:

A
↓
100

就已经被处理掉了。

所以:

#define
#ifdef
#include

都发生在:

预处理阶段。


三十三、头文件中可以放什么

常见:

#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[50];
    int age;
    double score;
} Student;

void print_student(Student *p);
void add_score(Student *p, double score);

#define MAX_STUDENTS 100

#endif

这里有:

结构体定义
函数声明
宏

这三类东西都非常常见。


三十四、头文件中不要随意放函数实现

例如:

// student.h

void print_student(Student *p) {
    printf(...);
}

对于普通非 static 函数,这样设计很容易在多个源文件包含时造成重复定义。

更常见的做法:

student.h
↓
函数声明

student.c
↓
函数定义

也就是:

// student.h
void print_student(Student *p);
// student.c
void print_student(Student *p) {
    ...
}

三十五、extern

如果多个 .c 文件需要访问一个全局变量,可以使用:

extern

例如:

// config.h

extern int max_students;

然后在某个 .c 文件中真正定义:

// config.c

int max_students = 100;

其他地方:

#include "config.h"

printf("%d\n", max_students);

这里:

extern
↓
声明这个变量在别的地方定义

三十六、声明和定义再次理解

例如:

extern int max_students;

这是:

声明

真正的定义:

int max_students = 100;

只有一个。

可以理解为:

config.h
↓
告诉大家
“有一个 max_students”

config.c
↓
真正创建
“max_students”

这和:

函数声明
+
函数定义

是类似思想。


三十七、头文件的典型结构

推荐:

#ifndef STUDENT_H
#define STUDENT_H

#include <stdio.h>

typedef struct {
    char name[50];
    int age;
    double score;
} Student;

void print_student(Student *p);
void add_score(Student *p, double score);

#endif

然后:

student.h

专门负责:

Student 类型
函数声明
必要的宏

而:

student.c

负责:

函数实现

三十八、项目结构

例如:

my_project/
│
├─ main.c
│
├─ student.c
├─ student.h
│
├─ file.c
├─ file.h
│
└─ config.h

可以按照功能拆开。

例如:

student
↓
学生相关代码

file
↓
文件相关代码

config
↓
程序配置

main
↓
主程序

这样比全部写在一个:

main.c

里面更加容易维护。


三十九、编译多个 .c 文件

例如项目:

main.c
student.c

使用 GCC 可以:

gcc main.c student.c -o main

这里:

main.c
+
student.c
↓
一起编译、链接
↓
main

如果有:

file.c
student.c
main.c

则:

gcc main.c student.c file.c -o main

四十、头文件不会单独生成程序

例如:

student.h

不能理解为:

student.h
↓
单独编译成程序

而是:

main.c
 ↓
#include "student.h"
 ↓
获得声明等内容
 ↓
编译

student.c
 ↓
编译
 ↓
链接

所以真正参与编译和链接的主体通常是:

.c

而:

.h

主要提供接口和声明。


四十一、预处理、编译、链接

现在可以把整个过程理解得更完整:

main.c
student.c
student.h
    │
    ↓
预处理
    │
    ↓
预处理后的源代码
    │
    ↓
编译
    │
    ↓
目标文件 .o / .obj
    │
    ↓
链接
    │
    ↓
可执行文件

其中:

#include
#define
#if
#ifdef

主要属于:

预处理阶段。

而:

gcc main.c student.c

最终还会进行:

编译
+
链接

四十二、一个完整的多文件项目

目录:

project/
├─ main.c
├─ math.c
└─ math.h

math.h:

#ifndef MATH_H
#define MATH_H

int add(int a, int b);
int multiply(int a, int b);

#endif

math.c:

#include "math.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

main.c:

#include <stdio.h>
#include "math.h"

int main() {

    printf("%d\n", add(10, 20));
    printf("%d\n", multiply(10, 20));

    return 0;
}

编译:

gcc main.c math.c -o main

运行:

30
200

现在:

main.c
↓
程序入口

math.c
↓
数学函数实现

math.h
↓
数学函数声明

四十三、与前面知识的连接

到这里:

函数
 ↓
函数声明
 ↓
头文件
 ↓
多个 .c 文件

结构体:

结构体定义
 ↓
放入 .h
 ↓
多个 .c 使用

动态内存:

malloc()
 ↓
可以放在某个功能模块中

文件:

fopen()
fread()
fwrite()
 ↓
也可以单独放在 file.c 中

最终:

main.c
   │
   ├── student.h
   │      ↓
   │   student.c
   │
   ├── file.h
   │      ↓
   │   file.c
   │
   └── config.h

这就是一个真正开始模块化的 C 项目。


四十四、练习

练习 1

创建:

hello.h
hello.c
main.c

要求:

hello.h
↓
声明 hello()

hello.c
↓
实现 hello()

main.c
↓
调用 hello()

练习 2

创建:

math.h
math.c
main.c

实现:

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);

然后在 main.c 中调用。


练习 3

使用:

#define MAX_SIZE 100

创建一个:

int arr[MAX_SIZE];

练习 4

自己写一个:

config.h

定义:

#define MAX_STUDENTS 100
#define NAME_SIZE 50

然后在其他 .c 文件中:

#include "config.h"

使用。


练习 5

创建:

student.h
student.c
main.c

student.h:

Student 结构体
print_student 声明
add_score 声明

student.c:

实现函数

main.c:

创建 Student
调用函数

练习 6

给头文件加入:

#ifndef
#define
#endif

观察重复 #include 是否还会产生重复定义问题。


练习 7

综合练习:

制作一个:

课程表项目

目录:

course/
├─ main.c
├─ course.c
├─ course.h
├─ file.c
├─ file.h
└─ config.h

设计:

course.h
↓
课程结构体
课程相关函数

file.h
↓
文件读写函数

config.h
↓
程序配置

最终:

main.c
↓
调用各个模块

这已经非常接近真实 C 项目的结构了。


此作者没有提供个人介绍。
最后更新于 2026-09-18