以"#"开头的命令是C语言的预处理指令,比如#include, #define等。所谓预处理是指在进行编译之前的第一遍扫描,由预处理器进行操作,主要包括宏定义,文件包含,条件编译等。合理使用预处理能使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序的设计。

宏定义

在C语言中允许使用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在预处理时,会对程序中所有出现的宏名进行宏替换,用宏定义中的字符串去替换宏名,称为宏替换或是宏展开。定义宏包括两种,一种是无参数的宏,一种是有参数的宏。

无参数的宏

无参宏经常使用的是数值宏常量和字符串宏常量,如下:

#define MAX 1000
#define PI 3.1415926
#define ERROR_INPUT -1
#define PATH "/myWork/day11"

除此之外,还可以使用宏来定义表达式,比如:

#define SECOND_PER_HOUR 60*60

对于表达式的宏,我们建议一般加上括号,如下:

#define (60*60)
#define SEC_PER_YEAR (60*60*24*365)UL

宏定义一般写在函数之外,它的作用域为从本行开始到文件结束。对于定义过的宏,可以使用#undef来结束其作用域,比如:

#define MAX 1000
#undef MAX //此处及以下MAX宏的作用已终止

除此之外,对字符串中的宏,将不进行替换操作,如下:

#define OK 100
printf("OK"); //此处的OK不会进行宏替换

有参数的宏

定义宏时也可以带上一定的参数,让宏在某种程序上具有“函数”的功能,比如:

#define SQR(x) x * x

SQR(6)将会展开成6 * 6。但是在在求取SQR(a + b)时,这个宏会展开成a + b * a + b。这显然与想要的结果不符合。为了解决这个问题,可以在宏里使用括号,如下:

#define SQR(x) ((x) * (x))

最外层的括号也别省略,看例子:

#define SUM(x) (x) + (x)

如果x的值是个表达式,比如5+3,而代码又写成这样:SUM(5+3) * SUM(5+3),则宏展开之后将变成:(5+3) + (5*3) * (5+3) + (5+3)。这显然也是不对的,所以最外层的括号也别省略。以下是正确的写法:

#define SUM(x) ((x) + (x))

对于有参数的宏,最保险的做法就是,在所有参数两边都加上括号,然后在整个表达式加上括号。

另外,如果需要严谨一些的话,在定义有参数宏时,还需要自增自减运算符的影响。请对比以下两个MAX宏,分析自增自减运算符对运算结果的影响。

#define MAX1(a, b) ((a) > (b) ? (a) : (b))

#define MAX2(a, b)         \
    ({                     \
        typeof(a) _a = a;  \
        typeof(b) _b = b;  \
        _a > _b ? _a : _b; \
    })

int main()
{
    int a = 3;
    int b = 2;
    int c = MAX1(a++, b); //当使用MAX2宏时结果又如何?
    printf("c is %d\n", c);
    printf("a is %d\n", a);
    return 0;
}

预定义宏

GCC中预定义了几个可以直接使用的宏,如下:

宏名描述
__FILE__表示正在编译的文件名字符串
__LINE__表达正在编译行号
__func__或__FUNCTION__表达正在编译的函数名字符串
__DATE__表示编译时刻的日期字符串
__TIME__表示正在编译时刻的时间字符串

用宏定义语句

可以用宏来定义语句,如下:

#define LOG(format, arg)  printf(format, arg)

LOG("%d", 5)会展开成printf("%d", 5)

宏定义只能在一行中完成,如果一行写不下,可以使用\将跨行的语句连接成一行,如下:

#define SAFEFREE(p)    \
    do                 \
    {                  \
        if (p != NULL) \
        {              \
            free(p);   \
            p = NULL;  \
        }              \
    } while (0)

可变参宏

C99标准以后,GCC在预处理阶段,可以用可变参数宏__VA_ARGS__来进行可变参数替换,如下:

#define LOG(format, ...) printf(format, ##__VA_ARGS__)

其中的...部分表示该宏可以接收可变参数,而__VA_ARGS__则会替换所有的可变参数(“##”的作用用于处理可变参数为空的情况),以下是替换示例:

LOG("hello world\n");  ==>  printf("hello world\n");
LOG("%d\n", 5);        ==>  printf("%d\n", 5);
LOG("%d %d\n", 5, 6);  ==>  printf("%d %d\n", 5, 6);

"#"运算符

在宏定义中,可以使用#将参数转化成字符串的形式,比如:

#define STRFY(x) #x 

则:

char *str = STRFY(123456);  ==>  char *str = "123456";

"##"运算符

##运算符可在宏定义中进行字符串的连接,比如:

#define VAR(n)  a##n

则:

int a1 = 1;
int a2 = 2;
int a3 = 3;

int arr[] = {VAR(1), VAR(2), VAR(3)};  ==>  int arr[] = {a1, a2, a3};

条件编译

条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于调试和移植程序是很有用的,条件编译有以下几种形式。

第1种形式

#ifdef 标识符
语句块
#endif

这里只有标识符是被#define定义的才会执行语句块。

第2 种形式

#ifdef 标识符
语句块1
#else
语句块2
#endif

第3种形式

#ifdef 标识符1
语句块1
#elif 标识符2
语句块2
#elif 标识符3
语句块3
...
#endif

第4种形式

#ifndef 标识符
语句块
#endif

第5种形式

#if 常量表达式
语句块1
#else
语句块2
#endif

这里对视常量表达式的值来判断执行哪个语句块,而之前的是判断标识符有没有被定义过。

第6种形式

#if defined(宏1)
语句块1
#elif defined(宏2) && defined(宏3)
语句块2
#elif !defined(宏4) && !defined(宏5)
语句块3
#endif

文件包含

文件包含是预处理的一个重要功能,它将多个源文件链接成一个源文件进行编译,结果将只生成一个目标文件。C语言提供#include命令来实现文件的包含,它有两种格式。

#include <filename>
#include "filename"

其中,filename是要包含的文件名称,一般是后缀为.h的头文件。用尖括号括起时,表示这个文件要到系统规定的路径中去获得这个文件(一般是/usr/include目录),而用双引号括起的头文件,则表示这个头文件要到当前目录中去寻找,如果没找到,则到系统指定的目录去寻找。

头文件

在程序有多个源文件时,可以将函数或类型的声明写到头文件中,头文件的后缀一般是.h结尾。对于头文件的使用,最重要的是防止头文件重复包含。一般使用如下的格式避免这个问题。

#ifndef 头文件标识符
#define 头文件标识符
//头文件内容
#endif

注意一点,即使使用了防止重复包含语句,也不要在头文件中分配内存(定义变量或是malloc堆内存),因为使用了防止重复包含语句,也只是让一个文件中的多条#include语句防止重复包含,有多个源文件都包含同一个头文件时,定义的变量仍然会被定义很多次,产生重复定义的错误。所以,一般,全局变量都要放到源文件中去定义。

gcc编译流程

一般C语言的编译流程分为四步,分别是:预处理,编译,汇编,链接。
预处理主要操作是的程序中以#开头的预处理指令,包括宏定义,条件编译,文件包含三项内容,预处理的选项是-E,生成的文件以.i作为后缀。

gcc -E hello.c -o hello.i

预处理之后,编译器就可以开始把源文件翻译成机器码了。但是在编译之前,对于gcc编译器来说,还存在一个中间步骤,会把源文件先转化为汇编语言,然后再转化成机器码,转化为汇编语言这一步称为编译,使用-S选项。

gcc -S hello.i -o hello.s

转化成汇编代码后,再把汇编代码转化成机器码,这一步称为汇编,gcc的汇编选项是-c,生成的文件以.o作为后缀,称为目标文件。

gcc -c hello.s -o hello.o

最后一步,是将各个目标文件以及系统提供的库文件一起进行链接,生成一个最终的可执行文件。

gcc hello.o -o hello

  • 无标签