
预处理、条件编译与多文件编程一、本篇文章要解决什么问题你一直在单个 .c 文件里写所有代码。但真实的 C 语言项目可能有几十上百个文件。这篇文章帮你理解三件事预处理指令#include、#define、#ifdef在编译之前做了什么怎么把声明和定义分开头文件.h是干什么的一个多文件项目怎么组织——怎么编译、怎么链接二、先用一个简单例子理解2.1 出版一本书的流程出版一本书有三个阶段预处理编辑通读稿件把参见第 X 章替换成实际页码处理所有的见上文/见下文编译排版工人把稿件排成印刷版链接把各章节的排版文件合并成一本书C 语言的编译过程也是这样预处理先把所有#include和#define展开 → 编译器把每个 .c 文件编译成 .obj → 链接器把所有 .obj 合并成一个 .exe。2.2 头文件就是目录/索引一本书前面的目录告诉你第 3 章讲了什么、从第几页开始——你不需要读完第 3 章就知道它提供了什么内容。头文件.h就是代码的目录它告诉你有哪些函数、它们的参数是什么但不需要你去看函数的具体实现。三、核心知识点讲解3.1 预处理——编译器看到你的代码之前发生了什么预处理指令以#开头在编译之前由预处理器处理。#include——把另一个文件的内容贴进来#includestdio.h// 从系统路径找#includemyheader.h// 从当前目录找实际上就是把stdio.h的全部内容复制粘贴到这里。#define——宏替换#defineMAX100#defineSQUARE(x)((x)*(x))// 宏函数注意括号intarr[MAX];// → int arr[100];intsSQUARE(5);// → int s ((5) * (5));宏函数的括号陷阱#defineBAD_SQUARE(x)x*xintr1BAD_SQUARE(5);// → 5 * 5 25没问题intr2BAD_SQUARE(12);// → 1 2 * 1 2 5不是 9// 正确((x) * (x))这就是为什么宏函数中的每个参数和整个表达式都要用括号包起来。图17-1 C 语言编译过程流程图帮助读者建立编译不是一步完成的概念。3.2 条件编译——同一份代码在不同情况下的不同表现#includestdio.h#defineDEBUG1// 改成 0 就关闭调试输出intmain(void){#ifDEBUGprintf(调试信息程序开始运行\n);#endifprintf(正常输出\n);#ifdefDEBUGprintf(调试信息程序结束\n);#endifreturn0;}条件编译的常见用途调试开关开发时打开发布时关闭跨平台代码#ifdef _WIN32…#elif __linux__…头文件防重复包含见下一节3.3 头文件防重复包含——#ifndef 经典模式// student.h#ifndefSTUDENT_H// 如果没有定义过 STUDENT_H#defineSTUDENT_H// 定义它// 结构体声明、函数声明等structStudent{...};voidprintStudent(conststructStudent*s);#endif// 结束如果 student.h 被多个 .c 文件包含或被同一个 .c 文件间接包含多次#ifndef保证里面的内容只会被处理一次避免重复定义错误。图17-2 头文件防重复包含原理图解释为什么每个头文件都需要 #ifndef 保护。3.4 声明和定义的区别// student.h —— 头文件声明#ifndefSTUDENT_H#defineSTUDENT_HstructStudent// 结构体类型的定义放在头文件{intid;charname[20];doublescore;// 成绩};voidprintStudent(conststructStudent*s);// 函数声明只有签名没有函数体intcompareScore(conststructStudent*a,conststructStudent*b);#endif// student.c —— 源文件函数定义#includestdio.h#includestudent.hvoidprintStudent(conststructStudent*s){printf(%d %s\n,s-id,s-name);}intcompareScore(conststructStudent*a,conststructStudent*b){if(a-scoreb-score)return1;if(a-scoreb-score)return-1;return0;}// main.c —— 主程序#includestdio.h#includestudent.hintmain(void){structStudents{1,Tom};printStudent(s);return0;}核心规则声明函数签名、extern 变量放在 .h 中定义函数体、变量赋值放在 .c 中。图17-3 声明 vs 定义对比图让读者记住声明和定义分离是 C 语言多文件编程的核心原则。3.5 多文件项目的编译在 Visual Studio 中把所有 .c 文件添加到同一个项目的源文件文件夹中VS 会自动处理编译和链接。在命令行中GCC/MSVCgcc main.c student.c-oprogram.exe图17-4 多文件项目结构图帮读者理解真实项目的文件组织方式。四、完整代码示例下面是一个两文件的学生管理小程序展示头文件/源文件的拆分方式文件 1student.h#ifndefSTUDENT_H#defineSTUDENT_H#defineNAME_LEN30#defineMAX_STUDENTS50typedefstruct{intid;charname[NAME_LEN];doublescore;}Student;voidprintStudent(constStudent*s);voidaddStudent(Student arr[],int*count);#endif文件 2main.c#define_CRT_SECURE_NO_WARNINGS#includestdio.h#includestudent.hvoidprintStudent(constStudent*s){printf(学号%d 姓名%-10s 成绩%.1f\n,s-id,s-name,s-score);}voidaddStudent(Student arr[],int*count){if(*countMAX_STUDENTS){printf(已满\n);return;}printf(请输入学号、姓名、成绩);scanf(%d %29s %lf,arr[*count].id,arr[*count].name,arr[*count].score);(*count);}intmain(void){Student students[MAX_STUDENTS];intcount0;addStudent(students,count);printStudent(students[0]);return0;}五、运行结果请输入学号、姓名、成绩1001 Tom 90.5 学号1001 姓名Tom 成绩90.5六、代码逐行解析头文件防重复包含#ifndefSTUDENT_H#defineSTUDENT_H// ...头文件内容...#endif第一次包含时STUDENT_H未定义 →#ifndef通过 → 定义STUDENT_H→ 内容被处理第二次包含时STUDENT_H已定义 →#ifndef失败 → 整个#endif之前的内容被跳过STUDENT_H是约定俗成的命名规则头文件名大写 下划线换点号函数定义和声明分离头文件里只有函数签名声明——告诉其他文件有这些函数可以用.c 文件里有函数体定义——具体的实现代码main.c 通过#include student.h知道这些函数的存在编译器就能检查调用是否正确宏常量定义在头文件中#defineNAME_LEN30#defineMAX_STUDENTS50把这些放在头文件里所有包含这个头文件的 .c 文件都能用到这些常量——保证了全局一致。七、初学者常见错误错误1在头文件中定义函数不是声明// student.h——错误voidprintStudent(constStudent*s){printf(...);// 函数定义不要放在头文件里}// 如果两个 .c 文件都包含这个头文件链接时会出现重复定义错误错误2忘了头文件防重复包含导致重复定义// student.h——没有 #ifndef 保护structStudent{...};// 如果被包含两次结构体被定义两次→编译错误错误3宏函数忘了给参数加括号#defineMUL(a,b)a*b// 错误#defineMUL(a,b)((a)*(b))// 正确错误4头文件中定义了全局变量// student.hinttotal;// 错误每个包含此头文件的 .c 文件都会创建一个 total// 正确在 .c 中定义在 .h 中用 extern 声明错误5#include 用了尖括号来包含自己的头文件#includestudent.h// 错误——尖括号只在系统路径中搜索#includestudent.h// 正确——双引号先在当前目录搜索八、练习题练习题1拆分当前代码把第 15 篇的完整学生管理代码拆分为student.h声明和student.c定义加main.c的三文件结构。在 VS 的项目中添加所有 .c 文件编译运行确认能正常工作。练习题2用条件编译实现调试开关在练习题 1 的基础上在头文件中加#define DEBUG 1。在 .c 文件中用#ifdef DEBUG包裹调试输出如添加了一个学生、“正在显示列表”。把 DEBUG 改成 0重新编译观察调试输出是否消失。练习题3宏函数练习定义一个宏函数#define MAX(a, b) ((a) (b) ? (a) : (b))。用不同参数测试包括MAX(3, 5)和MAX(32, 12)。观察有括号和没括号的版本在MAX(32, 12)的宏展开下有什么区别。九、本篇总结预处理在编译之前#include粘贴文件内容#define做文本替换宏函数每个参数和整个表达式都要用括号包起来防止展开后的优先级错误#ifndef/#define/#endif防止头文件被重复包含每个 .h 文件必备声明放 .h函数签名、extern定义放 .c函数体、变量初始化多文件编译时把所有 .c 加入项目链接器会自动合并