C语言的“#include”没你想的那么简单 ·(好文分享)

你对#include的认识是不是只停留在包含头文件的认知中,好像也没有别的用处,小小东西也翻不起什么风浪?

#include <stdio.h>
#include "user_header.h"
// ……吧啦吧啦

#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标准中有一段这样的话,需要研究下:

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 a+b;
}

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 "<built-in>"
# 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 a+b;
}

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"
...

第一次见到这玩意,简直是惊呆了,还有这种操作。

不好吗?有什么不好?多简洁啊!

从上面的分析看,#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有意想不到的好处呢。

以下是一个MEMORY字段分配的设想:

  1. 将Memory的物理地址映射到自定义逻辑地址

  2. 逻辑地址按Memory的Block对齐,逻辑地址从0开始

  3. 用户数据按逻辑地址分配

  4. 应用接口按实际内容大小操作

  5. 底层接口根据逻辑地址对齐读写Memory

想定义一些内容条目,这些条目分别对应不同的内存地址,不同的长度,以后有需要还可以继续从后面添加就这样:

entry nameaddresssize
ID_DATA108
ID_DATA288
ID_DATA31616
...

 可以在一个头文件里面做这样的定义:

// 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的对应关系。

如果用结构体去定义的话,也很好,但是会增加数组遍历时间,如果是很庞大的条目数的话,这个效率问题就要考虑了。

#文章来源于微信公众号C语言的include没你想的那么简单(图文版)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值