暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一文讲解C语言预处理器

海人为记 2021-11-21
499

一个 C 程序被编译成可执行目标程序,需要经过许多步骤,第一个步骤为预处理阶段,通过预处理器在 C 程序进行编译之前进行一些文本性质的操作,如删除注释、插入文件内容、指定编译时错误消息以及是否根据条件编译指定进行编译等等。

C 语言提供了以 #
符号开头的预处理命令来告知预处理器需要完成的特定操作。

C 语言中允许将 #define
定义的符号替换到程序中,称为定义宏 defined macro
,简称宏 macro

#define

宏定义的声明方式如下所示。

#define name[(args)] stuff

  • #define
    :定义宏的预处理命令。
  • name
    :定义宏的名字,可以出现在代码的任何地方。
  • args
    :参数列表,是由逗号分隔的符号列表,可能出现在 stuff
    中。
  • stuff
    :替换文本,所有出现 name
    的地方都将被替换为 stuff

该定义宏会将文本中出现 name
的地方都替换为 stuff
。方括号[]
中出现的 (args)
是可选的,省略后的声明方式为 #define name stuff
。参数列表 args
的左括号 (
必须与 name
紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff
一部分。

当宏被调用时,参数列表 args
中的参数的值会与宏定义中的替换文本 stuff
中的参数一一对应。如声明一个带有参数的宏:

#define SQUARE(x) x * x;

如果定义的 stuff
非常长,可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠 \
,如下所示。

#define PRINT(FORMAT, VALUE)    \
    printf("The value is " FORMAT "\n", VALUE)


最好把宏名字全部大写,用于区分宏和函数的区别,因为宏的语法和函数语法完全相同。当把 SQUARE
写成 square
的话,在程序中都不知道你调用的是函数还是宏。

宏替换

使用宏定义可以把任何文本替换到程序中,如数值、字符串、表达式、函数以及其他。定义的 #define
宏在预处理阶段被其替换时,需涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,如果其中包含由 #define
    定义的符号,会先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代。
  3. 最后,再次对结果文本进行扫描,如果还包含由 #define
    定义的符号,就重复上述处理过程。

如上所示,宏参数和 #define
定义可以包含其它 #define
定义的符号,但不可出现递归。

当在程序中出现 SQUARE
宏时,会替换成 x * x

int a = 10;              =>  int a = 10, b = 5;
int c = SQUARE(10 + 1);  =>  int c = 10 + 1 * 10 + 1;
printf("%d\n", c);       =>  printf("%d\n", c);    // 21

预处理器会将上面程序中的 SQUARE(10 + 1)
替换成 10 + 1 * 10 + 1
,计算出的结果是 21
,不是我们理解的 121
。这是因为替换只是文本替换,并没有按照预期的次序求值,我们只要在替换文本中添加括号即可。

#define SQUARE(x) ((x) * (x))

如上所示,将整个表达式都用括号括起来,就会先计算括号中的表达式,这样就可以避免使用宏时,产生由参数中的操作符或临界操作符之间不可预料的相互作用。

避免滥用宏,因为使用宏过多,会造成程序很难理解。

由于函数中的参数必须强制声明为一种类型,想实现一种用于多种类型的函数必须定义多个函数,如 square
函数。

int square_int(int x) {
    return x * x;
}

double square_double(double x) {
    return x * x;
}

而宏是与类型无关的,可以通过宏代替函数实现与类型无关的操作,就像上面定义的宏 SQUARE

int a = SQUARE(5);    // 25
double b = SQUARE(5.5);    // 30.25

当想将宏参数替换为一个字符串时,就可以使用 #argument
结构,让预处理器替换为 "argument",可以将上面的 PRINT
宏安如下形式更改。

#define PRINT(FORMAT, VALUE)    \
    printf("The value of " #VALUE    \
    " is " FORMAT "\n", VALUE)


int a = 5;
PRINT("%d", x + 5);    // The value of x + 5 is 10

除了 #argument
,预处理器中还有 ##
结构,用于将两边符号连接成一个符号,如下所示。

#define PRINT(FORMAT, VALUE, INDEX)    \
    printf("The value of " #VALUE #INDEX    \
    " is " FORMAT "\n", VALUE ##INDEX)


int a = 1, a1 = 3, a2 = 5, a3 = 7;
PRINT("%d", a, 1);    // The value of a1 is 3
PRINT("%d", a, 2);    // The value of a2 is 5
PRINT("%d", a, 3);    // The value of a3 is 7

副作用

当宏定义的 SQUARE
中,传入的参数为 x+1
的话,无论执行多少次,结果都是一样。

int a = 10;            =>  int a = 10;
int b = SQUARE(a + 1); =>  int b = ((10 + 1) * (10 + 1));
printf("%d\n", b);     =>  printf("%d\n", b);    // 121

当传入的是 x++
的时候,因为在 SQUARE
中出现了两次,它会增加 x
的值,使得结果都会不一样。

int a = 10;                    =>  int a = 10;
int b = SQUARE(a++);           =>  int b = ((x++) * (x++));  =>  int b = ((10++) * (11++));
printf("a = %d, b = %d\n", b); =>  printf("a = %d, b = %d\n", b);    // a = 12, b = 110

这种情况就是宏参数在宏定义中出现的次数超过了一次而产生的副作用,增加了 a
的值。

#undef

C 语言提供了 #undef
来移除一个宏定义,它的声明语法如下。

#undef name

  • name
    :宏定义的符号。

使用 #undef
移除宏定义后,之后就不能调用名称为 name
的宏了。

#define SQUARE(x)  x * x

int main() {
    int a = SQUARE(5);
#undef SQUARE
    int b = SQUARE(10); //Implicit declaration of function 'SQUARE' is invalid in C99
    return 0;
}

预定义宏

C 语言还提供了一些标准预定义宏,它们的值或者是字符串常量,或者是十进制数字常量,如下表所示。

符号含义例子
__FILE__
进行编译的源文件名称,用字符串常量表示"process.c"
__LINE__
文件当前行的行号,十进制整型常量表示,会随着#line指令改变64
__DATE__
文件被编译的日期,"Mmm dd yyy" 形式的字符串常量表示“Nov 22 1999”
__TIME__
文件被编译的时间,"hh:mm:ss" 形式的字符串常量表示"20:20:20"
__STDC__
编译器遵循ANSI C时,定义为 1,否则未定义。整型常量表示1

文件包含

每当需要使用库函数的时候,都需要使用 #include
指令来实现文本包含的操作,它实际是宏替换的延伸,预处理会删除这条指令,并将 #include
指令指定的文件编译进来取而代之。

#include
指令有库函数和本地文件两种形式。

库函数文件包含的形式的语法如下所示。

#include <filename>

而本地文件包含的形式的语法如下所示。

#include "filename"

库函数文件包含形式会使得编译器按照库函数头文件的处理方式来处理。而本地文件包含形式会让先按照编译器的策略来处理本地头文件,如果失败,再按照库函数头文件的处理方式来处理。处理本地头文件的常见策略就是在源文件所在当前目录下进行查询,如果该头文件并未找到,编译器就像查询函数库头文件一样的标准位置查找本地头文件。

如果在使用库函数头文件时,使用的是本地文件包含方式,就无法区分到底是库函数头文件还是本地头文件。

#include "stdio.h"

这种情形下,编译器会浪费时间在查找头文件上。当然,在使用文件包含时,也可以给出绝对路径。

#include "/usr/projects/algo.h"

在使用头文件包含时,可能会出现一种一个头文件被多次包含的情况。

// #include "cmath.h"    cmath.h头文件
double average(int n_values, ...) { ... }

// #include "cstring.h"    cstring.h头文件
#include "cmath.h"
size_t length(char *str) { ... }

// #include "collection.h"    collection.h头文件
#include "cmath.h"

struct Collection *create() { ... }

#include "cstring.h"
#include "collection.h"
int main() {
    return 1;
}

上面的例子中,cmath.h
main
函数所在的文件中出现了两次,面对这种情况,需要使用条件编译,如下所示。

#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1    =>  #define _HEADERNAME_H  // 将 1 去掉的效果也是一样的。尽管现在它的值是一个空字符串而不是 `"1"`,但这个符号仍然被定义。
/*
** All the stuff that you want in the header file
*/

#endif

上面的方式,会在头文件被第一次被包含时正常处理,再次包含时,通过条件编译,会将所有内容忽略。符号 _HEADERNAME_H
按照被包含文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。

不管多重包含是否能拖慢编译速度,也要尽量避免多重包含的出现。

条件编译

C 语言提供了条件编译 conditional compilation
告知编译器在不同的条件下是编译执行还是完全忽略。条件编译的指令有 #if
#elif
#else
#endif
#ifdef
#ifndef

#if...#endif

用于支持条件编译的完整语法形式如下所示。

#if constant-expression
    statements
#elif constant-expression
    statements
#elif constant-expression
    statements
···
#else
    statements
#endif

  • #if
    :条件编译开始,当 #if
    中的常量表达式为假时,才会向下执行其他条件,为真时会被正常编译。
  • #elif
    :当 if
    中的常量表达式为假时,#elif
    中的常量表达时为真时,该 #elif
    中的 other statements
    才会被正常编译。该子句出现的次数不限。
  • #else
    :当 #if
    与所有的 #elif
    的常量表达式为假时,才会被正常编译。
  • #endif
    :结束条件编译的标志。

上述条件编译的语法是从上到下,每个条件编译中的常量表达式 constant-expression
只有当前面所有的常量表达式的值都为假时才会往下判断是否要编译,常量表达式可以是字面值常量,或者使用宏定义的符号。上述条件编译语法中,只有一个 statements
会被正常编译,其他的会被预处理器删除。

#if UNIX
    #include "systemu.h"
#elif LINUX
    #include "systeml.h"
#elif MACOS
    #include "systemm.h"
#elif WIN
    #include "systemw.h"
#endif

给上述条件编译命令中的 LINUX
设置为 1
,其余为 0
,就会编译 #include "systeml.h"
头文件,其余忽略。

#define LINUX 1

条件编译的完整语法可以将 #elif
#else
省略,只保留 #if
#endif
组成最简单的条件编译指令。

#if constant-expression
    statements
#endif

#ifdef

C 语言的预处理指令提供了 #ifdef
来测试宏是否被定义过,其完整语法如下所示。

#ifdef SYMBOL
    statements
#else
    other statements
#endif

通过判断宏 SYMBOL
是否被定义过,如果定义了执行 statements
语句;如果未定义,执行 other statements
语句。#else
指令可以省略。

#ifdef LINUX
    printf("Linux is defined\n");
#else
    printf("Linux is not defined\n");
#endif

#ifndef

C 语言提供的预处理指令 #ifndef
#ifdef
的效果相反,其完整语法如下所示。

#ifndef SYMBOL
    statements
#else
    other statements
#endif

如果 SYMBOL
未定义,执行 statements
语句;如果已经定义,执行 other statements
语句。#else
指令可以省略。

#ifndef LINUX
    printf("Linux is not defined\n");
#else
    printf("Linux is defined\n");
#endif

其效果刚好与 #ifdef
相反。

defined

C 语言提供了 defined
指令来确定宏是否被定义,其语法形式如下所示。

#if defined(SYMBOL)
    statements
#endif

如果宏 SYMBOL
已被定义,执行 statements
语句;如果未定义,执行 other statements
语句。#elif
#else
指令也可以加入上述语法中。

#if defined(SYMBOL)  => #ifdef SYMBOL
#if !defined(SYMBOL) => #ifndef SYMBOL

如上所示,使用 defined
可以实现与 #ifdef
#ifndef
一样的效果。

#error

预处理指令还提供了 #error
指令,用于让处理器产生一条错误消息,其语法形式如下所示。

#error message

当编译过程失败,程序中断,如果使用了 #error
指令,就可以发出一个提示。

#ifdef UNIX
    #include "systemu.h"
#else
    #error unix compilation error
#endif

#line

C 语言提供 #line
指令,用于改变当前行号与文件名称,其语法形式如下所示。

#line number filename

  • number
    :表示当前行号。
  • filename
    :表示当前文件名称,可省略。

通常用于在编译过程中发现错误。这条指令中的 number
将修改 __LINE__
的值,filename
将修改  __FILE__
的值。

int main() {
    printf("This code is on line %d, in file %s\n", __LINE__, __FILE__);
#line 20
    printf("This code is on line %d, in file %s\n", __LINE__, __FILE__);
#line  30 "test"
    printf("This code is on line %d, in file %s\n", __LINE__, __FILE__);
    return 0;
}
// This code is on line 11, in file xx/line.c
// This code is on line 20, in file xx/line.c
// This code is on line 30, in file test

#progma

C 语言提供的 #progma
指令,用于为不同编译器提供不同的特性,其语法形式如下所示。

#progma param

param
#progma
的参数,通过不同的参数,编译器提供不同的功能,并且不同的编译器提供的参数也会有所差异,编译器的预处理器会忽略它所不认识的 #progma
指令。

#ifdef LINUX
#pragma message("LINUX is defined!")
#endif

// xx/progma.c:11:9: note: #pragma message: os is defined!

如上所示,使用该 #progma message
指令可以在编译期间,将message
中的内容输出到相应的窗口上。




文章转载自海人为记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论