C语言头文件包含与宏
2026/9/24 4:34:48 网站建设 项目流程

从扫雷项目看#include、宏和条件编译:这几道 C 语言题终于串起来了

作业题目

下面先把截图中的题目列出来,答案和理由放在后文,读的时候可以先自己想一遍。

第 1581 题:以下关于头文件,说法正确的是( )。

  • A.#include <filename.h>,编译器寻找头文件时,会从当前编译的源文件所在的目录去找。
  • B.#include "filename.h",编译器寻找头文件时,会从通过编译选项指定的库目录去找。
  • C. 多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯。
  • D. 在大型项目开发中,把所有自定义的数据类型、函数声明都放在一个头文件中,各个源文件都只需要包含这个头文件即可,省去了要写很多#include语句的麻烦,是好的编程习惯。

第 1585 题:下面哪个不是宏和函数的区别?( )。

  • A. 函数可以递归,宏不能递归。
  • B. 函数参数有类型检查,宏参数无类型检查。
  • C. 函数的执行速度更快,宏的执行速度慢。
  • D. 由于宏是通过替换完成的,所以操作符的优先级会影响宏的求值,应该尽量使用括号明确优先级。

第 1586 题:下面哪个是条件编译指令?( )。

  • A.#define
  • B.#ifdef
  • C.#pragma
  • D.#error

从题目回到源码

这次作业有三道选择题:头文件怎么找、宏和函数有什么区别、哪个指令用于条件编译。只看选项时,我很容易把它们记成三条互不相干的结论。后来翻了自己的《C学习记录7.27》《C学习记录7.28》《C学习记录8.18》《C学习记录8.19》,又找出扫雷项目和以前的课堂代码,才发现它们其实都发生在预处理和多文件编译这条线上。

这篇文章就用手头的代码把三道题讲明白。文末附有完整的复现实验,以及我实际编译、运行到哪一步的记录。

先从我的扫雷项目说起

扫雷项目不是只有一个main文件,而是拆成了game.hgame.ctest.cgame.h开头是这样的(摘取与本文有关的部分):

#pragmaonce#include<stdio.h>#include<time.h>#include<stdlib.h>#defineROW9#defineCOL9#defineROWSROW+2#defineCOLSCOL+2#defineEASY_COUNT10voidInitBoard(charboard[11][11],intr,intc,charset);voidDisplayBoard(charboard[ROWS][COLS],intr,intc);voidSetMine(charmine[ROWS][COLS],intr,intc);voidFindMine(charmine[ROWS][COLS],charshow[ROWS][COLS],intr,intc);

game.ctest.c都写了#include "game.h"。在test.c里,可以看到宏真正参与了数组声明和函数调用:

charmine[ROWS][COLS]={0};charshow[ROWS][COLS]={0};InitBoard(mine,ROWS,COLS,'0');InitBoard(show,ROWS,COLS,'*');SetMine(mine,ROW,COL);

我以前看到#include "game.h",会简单地说“引入头文件”。现在更准确的理解是:预处理器先处理包含和宏展开,编译器随后分别编译各个.c文件,最后链接器把需要的定义组合起来。#include "game.h"让当前.c文件看见声明和宏,不会自动把game.c的函数实现编译进来。如果构建配置只编译test.c而漏掉game.c,可能到链接阶段才报找不到函数定义。这个区别在《C学习记录7.27》的多文件例子里也写过。

双引号和尖括号究竟差在哪

项目中的两种写法正好都出现了:

#include"game.h"/* 项目自己的头文件 */#include<stdio.h>/* 工具链提供的标准库头文件 */

在我用的 MSVC 环境中,双引号形式会先从包含该指令的文件所在目录开始找,之后还会查找相关的包含路径;尖括号形式从配置的包含路径查找。用 MSVC 命令行时,这些路径包括/I选项与INCLUDE环境变量;在 Visual Studio IDE 中,项目配置的包含目录起作用。具体搜索顺序属于编译器实现,不能把“尖括号先找当前源文件目录”当作所有 C 编译器的规则。微软的#include文档列出了 MSVC 的查找顺序。

这里还有一个我之前混淆的词:包含目录是找头文件的地方,库目录通常与链接时找库文件有关。作业选项 B 的意思是双引号形式还可能通过编译器指定的目录寻找头文件;理解这道题时,应把那里的“库目录”按“头文件包含目录”来读。

为什么头文件里放声明,定义通常留在.c

game.h放了InitBoard等函数声明,game.c放它们的函数定义。这样test.c可以知道函数怎么调用,而定义仍然只有一份。普通全局变量也要留心:《C学习记录7.28》举过一个容易踩坑的例子:

/* shared.h:供多个 .c 文件使用的声明 */externintyear;/* shared.c:唯一的定义 */intyear=2025;

如果把int year = 2025;直接写进被多个.c文件包含的头文件,每个翻译单元都会获得一个定义,链接时通常会报重复定义。#pragma once只是在同一个翻译单元内避免同一头文件被重复处理,不能把跨多个.c文件的普通全局变量定义变成一份。

扫雷项目用了#pragma once。它在 MSVC 等工具链上很方便;如果要写更强调可移植性的 C 头文件,也可以采用传统的包含保护:

#ifndefGAME_H#defineGAME_H/* 头文件声明放这里 */#endif/* GAME_H */

这段是说明另一种写法,不是对原项目的修改。关于#pragma once的支持范围,可以看微软文档。

宏展开不认识我想表达的“一个整体”

扫雷代码里的ROWS很适合拿来检查自己是否真的理解宏:

#defineROW9#defineROWSROW+2

char mine[ROWS][COLS]中,ROWS展开成9+2,所以第一维是 11,符合当前 9×9 棋盘加边框的设计。但如果写ROWS * 2,预处理后的关键部分是9 + 2 * 2,按 C 表达式优先级算出来是13,不是想象中的 22。

所以我现在会把这一类宏写成#define ROWS (ROW + 2);这样ROWS * 2才会按(9 + 2) * 2计算。这里只是在说明更稳妥的写法,仓库里的原始源码仍是#define ROWS ROW+2。另外,原项目的InitBoard参数还把数组第二维直接写成11。如果以后改COL,只改宏并不足以保证所有声明同步。这是读源码时看到的耦合点,本篇没有调整扫雷接口,也没有验证修改棋盘大小后的行为。

《C学习记录8.18》和《C学习记录8.19》还有函数式宏的例子:

#defineMAX(x,y)((x)>(y)?(x):(y))

这个写法把参数和整个结果都加了括号,解决了不少运算符优先级问题;但它没有解决参数重复求值。比如传入a++b++,比较时两边各执行一次,比较后被选中的那个实参还要再执行一次。我的study8_19.c课堂文件 里有MAX(a++, b++)Max(a++, b++)的对照思路,但这一段当时是注释代码,不能把它写成原项目的运行结果。

为了把结果看准,我把相关定义提出来,另写了一个可独立编译的复现实验;完整代码如下,也单独保存为C语言头文件与宏_源码复现实验.c

#include<stdio.h>/* 与 game.h 一样,故意保留未加括号的替换列表,以观察展开结果。 */#defineROW9#defineROWSROW+2/* 从 study8_19.c 中被注释的课堂片段提取定义,单独复现。 */#defineMAX(x,y)((x)>(y)?(x):(y))staticintMax(intx,inty){returnx>y?x:y;}intmain(void){inta=10;intb=20;intmacro_result=MAX(a++,b++);printf("macro: result=%d, a=%d, b=%d\n",macro_result,a,b);a=10;b=20;intfunction_result=Max(a++,b++);printf("function: result=%d, a=%d, b=%d\n",function_result,a,b);printf("ROWS=%d, ROWS*2=%d, (ROW+2)*2=%d\n",ROWS,ROWS*2,(ROW+2)*2);return0;}

我用 MSVC 以 Debug x64、/W4且警告视为错误编译了这个独立程序,运行结果是:

macro: result=21, a=11, b=22 function: result=20, a=11, b=21 ROWS=11, ROWS*2=13, (ROW+2)*2=22

第一行可以自己手算:最初a=10, b=20。条件里的a++ > b++用旧值比较,条件为假;此时a=11, b=21。随后又执行一次b++,表达式取得旧值 21,最后b=22。普通函数调用会先算好两个实参,再把各自的值传进去,所以第二行里ab都只增加了一次。这个例子说明的是具体宏的求值次数,不能据此推出“宏一定比函数快”或“函数一定比宏快”。现代编译器还可能内联小函数;代码大小、优化设置和实际工作量都影响性能。宏替换与副作用的注意事项也见微软#define文档。

还有一个细节:宏形参xy本身没有像函数形参那样声明 C 类型,但宏展开后的 C 表达式仍会接受编译器的类型检查。说“宏完全没有类型检查”就过头了。

#ifdef是编译前选择代码,不是运行时if

我在另一个真实源码study9_2.c里找到了正在使用的条件编译:

#include"Queue.h"#include"LeetCode225.h"#include<stdio.h>#include<stdlib.h>#ifdef_WIN32#include<windows.h>#endifintmain(void){#ifdef_WIN32SetConsoleOutputCP(CP_UTF8);#endif/* 后面是队列与栈的练习代码 */}

这里的意思是:当_WIN32已定义时,预处理阶段保留 Windows 头文件与设置控制台编码的语句;否则跳过这些内容。运行时if做不到“让当前平台没有的头文件或函数调用不参与本轮编译”。上面只摘了与条件编译有关的代码,没有在本次写博客时编译整个队列项目

#ifdef DEBUG判断的是宏有没有定义。就算写了#define DEBUG 0,它也会进入#ifdef DEBUG分支;要按数值开关判断,应使用#if DEBUG,同时明确DEBUG的默认值。我的《C学习记录8.19》把这两种情况放在了一起,重新看代码后就不容易混淆了。具体语法可查微软#ifdef文档。

回头做三道题

第 1581 题:关于头文件,哪个说法正确?

我选B,但会在旁边补一句“这里应说包含目录”。

选项我的判断原因
A.<filename.h>会从当前源文件目录找尖括号形式通常走工具链配置的包含路径,不能概括为先找当前源文件目录。
B."filename.h"会通过编译选项指定的目录找本地目录找不到时,还会考虑编译器配置的包含路径;具体顺序依实现而定。
C. 多个源文件共用的全局变量,其声明和定义都放头文件普通全局变量定义放进被多个.c包含的头文件,容易造成多个定义;可在头文件用extern声明,在一个.c文件中定义。
D. 所有自定义类型和函数声明塞进同一个大头文件这样会让无关源文件也依赖整份大头文件;按模块提供需要的声明更清楚。

这题里最容易混的是 A、B 的搜索目录,以及 C 中“声明”和“定义”不是一回事。

第 1585 题:哪个不是宏和函数的区别?

我选C

选项我的判断原因
A. 函数可以递归,宏不能像函数那样递归基本正确C 函数可以调用自身;宏自引用不会像函数调用那样无限递归展开。
B. 函数参数有类型检查,宏参数没有函数形参式的类型声明基本正确宏替换后的表达式仍要由编译器检查,不能理解成“宏不受类型规则约束”。
C. 函数执行速度更快,宏执行速度慢没有这种固定的快慢关系。上面的实验检验的是求值次数,不是性能基准。
D. 宏替换可能改变运算优先级,书写时要留意括号ROWS * 2的 13 和预期的 22 就是现成例子。

我之前会只记“宏是替换、函数是调用”,现在更愿意先问:替换后到底长什么样,参数会算几次?

第 1586 题:哪个属于条件编译指令?

答案是B:#ifdef#define用来定义宏,#pragma用于实现相关的编译器指示,#error用于发出预处理错误;它们都是预处理指令,但题目问的是条件编译#ifdef正好用于按宏是否已定义选择代码。完整指令分类可查微软预处理指令列表。

这次实际验证到了哪里

  • 扫雷项目:我把原始game.hgame.ctest.c和项目文件复制到隔离目录,用 MSVC Debug x64 构建成功;运行程序输入0,看到了菜单与“退出游戏”,进程正常退出。构建仍报告_CRT_SECURE_NO_WARNINGS重复定义(C4005)和GetMinecount可能缺少返回值(C4715)。只验证了退出路径,未验证扫雷玩法;这两个警告也不是本篇博客对源码做过修复后的结果。
  • 宏实验:上面的独立程序用 MSVC/W4和警告视为错误编译成功,实际输出已列出。它使用了扫雷头文件的宏定义和study8_19.c里被注释的课堂思路,不是直接运行原课堂文件得出的结果
  • study9_2.c:本篇引用其已有的_WIN32条件编译写法,没有把整个队列项目的编译或运行算作已验证。

写完后,我给自己留下的一句提醒是:遇到预处理题,先看看自己的.h.c到底怎么配合;遇到宏,再把它展开成普通表达式算一遍。这样比背“宏快、函数慢”之类的口诀可靠得多。

笔记与源码出处:个人笔记《C学习记录7.27》《C学习记录7.28》《C学习记录8.18》《C学习记录8.19》;以上链接指向我仓库中对应版本的game.hgame.ctest.cstudy8_19.cstudy9_2.c。编译器相关规则以文中链接的微软官方文档为准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询