C语言的include没你想的那么简单

张开发
2026/4/14 2:30:33 15 分钟阅读

分享文章

C语言的include没你想的那么简单
C语言中的include很简单但不是你想象中的简单。你对#include的认识是不是只停留在包含头文件的认知中好像也没有别的用处小小东西也翻不起什么风浪#include #include user_header.h // bala bala#include就是包含头文件用的不是吗我之前也一直这么认为的直到我看了某些大神写的代码后来我还特意查阅了C99标准。人家是这么用的# define DET_START_SEC_VAR_INIT_UNSPECIFIED # include MemMap.h # define DET_STOP_SEC_VAR_INIT_UNSPECIFIED # include MemMap.h # define DET_START_SEC_VAR_NOINIT_8BIT # include MemMap.h # define DET_STOP_SEC_VAR_NOINIT_8BIT # include MemMap.h还有这样用的#define STRUCT_GEN_START #include defines.h #include param_gen.h #include defines.h #include param_gen.h #include defines.h #include param_gen.h #include defines.h #include param_gen.h #include defines.h #include param_gen.h当时看得我一愣一愣的……其实简单来说#include就是“包含”某个文件的意思但这个“包含”不能将思维限死在“头文件”这个概念中而应该有更多的想象#include在C语言中算是预编译指令preprocessing directive范畴而预编译指令在C语言就是一个大学问了。但是我们先不要被这个“预编译指令”名称绕晕。上文我们提到了头文件这个概念当然我们也知道还有一个叫源文件的概念。这些我就不解释了。但是在C99标准中有一段这样的话需要研究下A source file together with all the headers and source files included via the preprocessing directive #include is known as a preprocessing translation unit. After preprocessing, a preprocessing translation unit is called a translation unit.ISO/IEC 9899:1999 (E)简单地理解一个source file和一些由#include包含着的headers和source files通过预编译后变成一个叫translation unit的东西。从这里可以看出来#include不但可以包含headers还可以包含source files。所以我下面这个#include add.h和#include minus.c都是正确的编译一点问题都没有。// main.c #include add.h #include minus.c int add(int a, int b) { return ab; } int main(void) { int c add(1,2); int d minus(2-1); return 0; }// add.h extern int add(int a, int b);// minus.c int minus(int a, int b) { return a-b; }不妨将脑洞开大一点除了*.h和*.c文件我还可以include点别的么答可以。例如// main.c #include multiply.txt int main(void) { int e multiply(2,2); return 0; }甚至这样也行// main.c #include devide.fxxk int main(void) { int f devide(2,2); return 0; }继续啊#include不是放在文件上方放中间行么。当然// main.c int main(void) { #include squel.xx int g squel(2,2); return 0; }好家伙这么下去我是不是可以这么干// data.txt 1,2,3,4,5,6,7,8,9// main.c int arr[] { #include data.txt } int main(void) { return 0; }然后你又好奇了能不能将data.txt换成二进制形式的data.bin呵呵这种不行编译器在预编译阶段只认得是text文本才行。好吧……你不是说这是个预编译指令吗我很好奇#include预编译后成啥样子的这好办动动手指头一个gcc -E命令即可搞定。就以上面第一个例子命令行执行gcc ./main.c -E -o main.i# 0 .\\main.c # 0 # 0 命令行 # 1 .\\main.c # 1 add.h 1 extern int add(int a, int b); # 3 .\\main.c 2 # 1 minus.c 1 int minus(int a, int b) { return a-b; } # 4 .\\main.c 2 int add(int a, int b) { return ab; } int main(void) { int c add(1,2); int d minus(2-1); return 0; }看到了吧#include就是把它后面的文件内容直接include进来。就这么简单粗暴。那么#include在C语言中是不是很简单你说呢我见过有人这么写代码的还TM的一整个团队是这么做的。将整个所以.h文件全部包含在一个includes.h的头文件中然后在其他.c文件里面就直接#include includes.h。// includes.h #include adc.h #include uart.h #include spi.h #include iic.h #include dma.h #include pwm.h #include pin.h #include led.h #include os.h #include timer.h ...真TM的简便。我第一次见到这玩意简直是惊呆了还有这种操作。不好吗有什么不好多简洁啊从上面的分析看#include就是将它后面包含的头文件源文件全部展开哦。简洁你问过编译器啥感受么带来的最直接的感受是编译过程慢includes.h里包含得越多就越慢另外一个隐含的问题是会造成include里的内容混乱头文件里的内容全部是全局的了。我绝对不推荐这种玩法的。因为预编译还有更好玩的玩法。不过在介绍新玩法之前得想个问题如果一个头文件重复包含多次会怎样也许你会回答我是不允许出现这种情况的就算出现这种情况我也可以用#ifdef...#endif这种方式规避。如果你是应届生面试这样回答面试官也许是点点头说你有点经验的。因为重复include就相当于把头文件重复展开了多次C语言中有些定义是不允许重复多次的。例如上面的例子// main.c #include add.h #include minus.c #include minus.c这样是有问题的因为上面相当于重复定义了两次int minus(int a, int b)函数了。In file included from .\main.c:4: minus.c:1:5: 错误‘minus’重定义 1 | int minus(int a, int b) | ^~~~~如果将minus.c改成这样就行了#ifndef _MINUS_ #define _MINUS_ int minus(int a, int b) { return a-b; } #endif这个简单啊我也会啊。嗯但是我不是想说这个我真的想说重复include有意想不到的好处呢。这就不得不提下我以前写的X-MACRO大法了。以下是一个MEMORY字段分配的设想将Memory的物理地址映射到自定义逻辑地址逻辑地址按Memory的Block对齐逻辑地址从0开始用户数据按逻辑地址分配应用接口按实际内容大小操作底层接口根据逻辑地址对齐读写Memory我想定义一些内容条目这些条目分别对应不同的内存地址不同的长度以后有需要还可以继续从后面添加就这样可以在一个头文件里面做这样的定义// defines.h #ifdef ENTRY_ID #define ENTRY(id,addr,size) id, #undef ENTRY #undef ENTRY_ID #endif #ifdef ENTRY_ADDR #define ENTRY(id,addr,size) addr, #undef ENTRY #undef ENTRY_ADDR #endif #ifdef ENTRY_SIZE #define ENTRY(id,addr,size) size, #undef ENTRY #undef ENTRY_SIZE #endif接着在C文件里面这么玩// memory.c #define ALL_ENTRIES() \ ENTRY(ID_DATA1, 0, 8) \ ENTRY(ID_DATA2, 8, 8) \ ENTRY(ID_DATA3, 16, 16) \ ENTRY(ID_DATA4, 32, 8) #define ENTRY_ID #include defines.h typedef enum { ALL_ENTRIES() MEM_ID_MAX } MEM_ID; #define ENTRY_ADDR #include defines.h const uint32_t mem_addr[] { ALL_ENTRIES() }; #define ENTRY_SIZE #include defines.h const uint16_t mem_size[] { ALL_ENTRIES() };你也许会反问我定义一个结构体不就搞定了吗别急这样做的好处是enum的ID顺序跟addr和size是一一对应的不会错乱另一个好处是可以随便在ALL_ENTRIES()下面扩展条目也不影响ID的对应关系。如果用结构体去定义的话也很好但是会增加数组遍历时间如果是很庞大的条目数的话这个效率问题就要考虑了。其实对上面的做法我还做了优化写在了这两篇文章中X-MACRO是个很酷的玩法哦欢迎查阅和讨论。如果你喜欢我的文章请关注并转发、点赞和在看这是对我莫大的鼓励

更多文章