C语言硬件编程栏目
往期文章:工作数年还是被指针坑了——谈谈C语言指针运算
前言
C语言编程中离不开预编译指令,编译器在预编译阶段,会根据预编译指令对你编写的代码进行处理。
例如,我们在写第一段C程序"HelloWorld"的时候,就用到了#include指令来包含stdio.h这样的头文件了;
而用于定义宏名称的指令#define也属于预编译指令,而且也是用的最多的预编译指令。
下面我们就围绕预编译指令和宏的应用场景,看看哪些是容易被忽略的预编译指令以及重要的应用场景。
一、预编译指令
C中的预编译指令很多,包括#define,#undef,#if,#ifdef,#ifndef,#error,#line, #pragma等等,常用且重要的指令主要是如下这些:
#define指令
编程中最常用的预编译指令要属#define宏定义指令了,其用法及形式如下:
#define NAME stuff
这样在code中其他使用NAME宏名的地方,都会被替换为stuff,这里的stuff可以是各种文本;最常见的常量数值,例如
#define PI 3.1415926
如果要移除一个前面定义的宏,用#undef即可。
#if指令
#if指令用于编译过程的分支判断,和#elif/#else/#endif配合使用。其形式如下:
#if Constant-Expression
statements
#elif Constant-Expression2
statements2
#else
statements3
#endif
因为在Constant-Expression域内可以填写各种表达式的组合,所以该指令往往使用起来灵活多变。例如和defined/!defined配合使用,从而限定某个条件下编译的code,该用法也是常常不被重视但颇有用的一种:
#if defined(MACRO_A) && !defined(MACRO_B)
statements
#endif
这样只有在定义MACRO_A和未定义MACRO_B的时候,才会执行如下的statements。
#ifdef指令
#ifdef指令和#if defined指令是等价的,所以常用#ifdef和#ifndef来实现宏定义下或非定义宏的条件下对code的执行,使用起来简单,但是没次只能对一个宏进行判断,不如#if指令灵活。
#include指令
该指令用于包含code中所需要的头文件,需要注意的是,包含的形式有两种:
//first one
#include <Library/file.h>
//second one
#include "file.h"
两者在检索头文件的过程中,是完全不一样的。第一种会按照编译器指定的库文件目录为根,向下检索头文件;而第二种则会按照当前编辑的C文件为根,去检索file.h。因此我们在使用的时候,如果头文件位于当前目录下,使用第2种方式最佳。
#pragma指令
一定注意,该指令名称是#pragma,不是#progma!
该指令稍显复杂,用于设定编译器的状态,指示编译器完成特定的动作。经常配合使用的参数包括message,warning和pack。而我们最常用和最重要的,就是和pack的搭配使用了。使用形式如下:
#pragma pack(n)
在默认情况下,对于结构体是按照最节省空间的方式进行对齐的,如下面的TestStruct1按照最大长度的int i进行对齐,c1/s/c2整体拼成一个单元,如表中所示,即按照pack(1)一个字节进行对齐:
struct TestStruct1
{
char c1;
short s;
char c2;
int i;
};
内存分布表
当然,上述默认的方式不一定适合编程时数据的访问,如果统一按照4个字节int类型进行访问,那么在结构体声明时使用pack(4)即可,那么这里的内存分布会变成:
c1 |
s |
c2 |
i |
取消自行设定的对齐方式,再使用#pragma pack()即可。下次我们以UEFI kernel为模板,再聊聊宏的应用场景~