《C编程.高级C》2.预处理器指令和宏

预处理器是一种在实际编译之前使用C程序进行文本处理的方法。在每个C程序的实际编译之前,它通过预处理器传递。预处理器查看程序,试图找出它可以理解的称为预处理程序指令的特定指令。所有预处理程序指令都以#(哈希)符号开头。C ++编译器使用相同的C预处理器。

该预处理是执行预备操作(有条件编译代码,包括文件等)到你的代码编译器看到它之前的编译器的一部分。这些转换是词法,意味着预处理器的输出仍然是文本。

注意:从技术上讲,C的预处理阶段的输出由一系列令牌而不是源文本组成,但输出源文本很简单,它等同于给定的令牌序列,并且编译器通常通过以下方式支持- E或/ E选项 – 虽然C编译器的命令行选项不是完全标准的,但许多都遵循类似的规则。

指令

指令是指向预处理器(预处理器指令)或编译器(编译器指令)的特殊指令,指示它应如何处理部分或全部源代码或在最终对象上设置一些标志,并用于简化编写源代码(例如,更便携)并使源代码更容易理解。指令由预处理器处理,预处理器是由编译器调用的单独程序或编译器本身的一部分。

#include 

C有一些功能作为语言的一部分,其他一些功能作为标准库的一部分,标准库是一个代码存储库,可以与每个符合标准的C编译器一起使用。当C编译器编译您的程序时,它通常还将它与标准C库链接。例如,在遇到#include <stdio.h>指令时,它将指令替换为stdio.h头文件的内容。

当您使用库中的功能时,C要求您声明要使用的内容。程序的第一行是预处理指令,它应如下所示:

#include <stdio.h>

上面的行导致包含stdio.h 头文件中的C声明,以便在程序中使用。通常,只需在程序中插入名为stdio.h的头文件的内容即可实现,该文件位于系统相关的位置。可以在编译器的文档中描述此类文件的位置。标题C头文件列表在Headers表中列出。

的stdio.h中头包含使用的称为I / O机制抽象用于输入/输出(I / O)的各种声明。例如,有一个名为stdout的输出流对象,用于将文本输出到标准输出,通常在计算机屏幕上显示文本。

如果使用如上例所示的尖括号,则指示预处理器在标准包含的开发环境路径中搜索包含文件。

#include“other.h”

如果使用引号(“”),预期处理器应在头文件的一些额外的,通常是用户定义的位置进行搜索,并且只有在那些其他位置找不到标准包含路径时才回退到标准包含路径。此表单通常包括在与包含#include指令的文件相同的目录中进行搜索。

注意:您应该检查您正在使用的开发环境的文档,以获取#include指令的任何供应商特定实现。

标题

C90标准标题列表:

<assert.h> 2用于<文件ctype.h><errno.h>中<float.h>中<limits.h>中<locale.h文件><math.h>中<SETJMP.H><signal.h中><STDARG.H><STDDEF.H><stdio.h>中<stdlib.h>中<string.h>的<time.h>中

自C90以来添加的标题:

<complex.h><fenv.h>中<inttypes.h>还<iso646.h>文件<stdbool.h><stdint.h>中<tgmath.h><wchar.h>中<wctype.h>

#pragma 

编译(语用信息)指令是标准的一部分,但任何编译的含义取决于软件实现所使用的标准。#pragma指令提供了一种从编译器请求特殊行为的方法。该指令对于异常大或需要利用特定编译器功能的程序最有用。

在源程序中使用Pragma。

#pragma token(s)
  1. pragma通常后跟一个令牌,表示编译器要遵守的命令。您应该检查您打算使用的C标准的软件实现,以获取支持的令牌列表。毫不奇怪,#pragma指令中出现的命令集对于每个编译器都是不同的; 你必须查阅编译器的文档,看看它允许哪些命令以及这些命令的作用。

例如,#pragma once当放置在头文件的开头时,实现最多的预处理器指令之一指示如果预处理器多次包含它将驻留的文件将被跳过。

注意:执行此操作的其他方法通常称为使用包含警戒

#define 

警告:预处理器宏虽然很诱人,但如果操作不正确,会产生非常意外的结果。始终记住,在编译任何内容之前,宏是对源代码进行的文本替换。编译器对宏没有任何了解,也永远不会看到它们。这可能会产生模糊的错误,以及其他负面影响。如果有等价的话,则更喜欢使用语言功能(例如使用const intenum代替#defined常量)。也就是说,有些情况下,宏非常有用(请参阅debug下面的宏示例)。

所述的#define指令用于定义值或用于由预处理器是编译之前操纵的程序源代码的宏。因为在编译器对源代码执行操作之前替换了预处理程序定义,所以#define引入的任何错误都很难跟踪。

按照惯例,使用#define定义的值以大写形式命名。虽然这样做不是必要条件,否则被认为是非常糟糕的做法。这允许在读取源代码时容易地识别值。

今天,#define主要用于处理编译器和平台差异。例如,define可能包含一个常量,它是系统调用的相应错误代码。因此,除非绝对必要,否则#define的使用应受到限制; typedef语句和常量变量通常可以更安全地执行相同的功能。

#define命令的另一个特性是它可以接受参数,使其作为伪函数创建者非常有用。请考虑以下代码:

#define ABSOLUTE_VALUE(x)(((x)<0)? - (x):( x))
...
int x = -1;
while(ABSOLUTE_VALUE(x)){
...
}

在使用复杂的宏时,使用额外的括号通常是个好主意。请注意,在上面的示例中,变量“x”始终位于其自己的括号内。这样,在与0比较或乘以-1之前,将对其进行整体评估。此外,整个宏被括号括起来,以防止它被其他代码污染。如果你不小心,你就有可能让编译器误解你的代码。

由于副作用,如上所述使用宏功能被认为是非常糟糕的主意。

int x = -10;
int y = ABSOLUTE_VALUE(x ++);

如果ABSOLUTE_VALUE()是一个真正的函数’x’现在具有’-9’的值,但因为它是宏中的一个参数,所以它被展开两次,因此值为-8。

例:为了说明宏的危险,请考虑这个天真的宏#define MAX(a,b)a> b?a:b 和代码i = MAX(2,3)+5; j = MAX(3,2)+5; 看看这个并考虑执行后的值是什么。这些陈述变成了int i = 2> 3?2:3 + 5; int j = 3> 2?3:2 + 5; 因此,在执行i = 8和j = 3之后,而不是i = j = 8的预期结果!这就是为什么你被警告要在上面使用一组额外的括号,但即使有这些,道路也充满了危险。警报阅读器可能会很快意识到,如果ab包含表达式,则定义必须在宏定义中对a,b的每次使用加以括号,如下所示:#define MAX(a,b)((a)>(b)?(a):( b)) 这项工作,提供a,b没有副作用。确实,i = 2; j = 3; k = MAX(i ++,j ++); 会导致k = 4,i = 3且j = 5。对于任何期望MAX()表现得像函数的人来说,这都是非常令人惊讶的。那么正确的解决方案是什么?解决方案是根本不使用宏。一个全局的内联函数,就像这样inline int max(int a,int b){ 返回a> b?a:b } 没有任何陷阱,但不适用于所有类型。注意:inline除非定义在头文件中,否则显式声明不是必需的,因为您的编译器可以为您内联函数(使用gcc可以使用-finline-functions或者-O3)。编译器通常比程序员更好地预测哪些函数值得内联。此外,函数调用并不是很昂贵(过去它们)。编译器实际上可以自由地忽略inline关键字。它只是一个提示(除了inline必须允许在头文件中定义函数而不生成错误消息,因为函数在多个转换单元中定义)。


#,##

##运营商均采用与的#define宏。使用#会导致#之后的一个参数作为引号中的字符串返回。例如,命令

#define as_string(s)#s

将使编译器转动此命令

puts(as_string(Hello World!));

puts(“Hello World!”);

使用##连接##之前的内容和之后的内容。例如,命令

#define concatenate(x,y)x ## y
...
int xy = 10;
...

将使编译器转向

printf(“%d”,concatenate(x,y));

printf(“%d”,xy);

当然,它将显示10到标准输出。

可以将宏参数与常量前缀或后缀连接起来,以获得有效的标识符,如

#define make_function(name)int my_ ## name(int foo){}
make_function(bar)

这将定义一个名为my_bar()的函数。但是不可能使用连接运算符将宏参数集成到常量字符串中。为了获得这样的效果,可以使用ANSI C属性,当遇到两个或多个连续的字符串常量时,它们被认为等同于单个字符串常量。使用此属性,可以编写

#define eat(what)puts(“我正在吃”#what“今天。”)
吃水果 )

宏处理器将变成什么

把(“我正在吃”“水果”“今天”。)

反过来,C解析器将其解释为单个字符串常量。

以下技巧可用于将数字常量转换为字符串文字

#define num2str(x)str(x)
#define str(x)#x
#define CONST 23

放(num2str(CONST));

这有点棘手,因为它分两步扩展。首先num2str(CONST)替换为str(23),然后替换为"23"。这在以下示例中非常有用:

#ifdef DEBUG
#define debug(msg)fputs(__ FILE__“:”num2str(__ LINE__)“ - ”msg,stderr)
#其他
#define debug(msg)
#万一

这将为您提供一个很好的调试消息,包括文件和发出消息的行。如果未定义DEBUG,则调试消息将完全从代码中消失。注意不要将这种结构与任何有副作用的结构一起使用,因为这会导致出现错误,这些错误会根据编译参数出现和消失。

宏未经过类型检查,因此不会评估参数。此外,它们不能正确地服从范围,而只是简单地将字符串传递给它们,并将宏文本中宏参数的每次出现替换为该参数的实际字符串(代码实际上被复制到它被调用的位置)从)。

关于如何使用宏的示例:

 #include <stdio.h>

 #define SLICES 8
 #define ADD(x)((x)/ SLICES)

 int main(void) 
 {
   int a = 0,b = 10,c = 6;

   a = ADD(b + c);
   printf(“%d \ n”,a);
   返回0;
 }

– “a”的结果应为“2”(b + c = 16 – >传递给ADD – > 16 / SLICES – >结果为“2”)

注意:
在标头中定义宏通常是不好的做法。只有在无法通过函数或其他机制实现相同结果时,才应定义宏。一些编译器能够优化代码,以便用内联代码替换对小函数的调用,从而否定任何可能的速度优势。使用typedef,枚举和内联(在C99中)通常是更好的选择。

内联函数无法工作的少数情况之一 – 因此你几乎不得不使用类似函数的宏 – 是初始化编译时常量(结构的静态初始化)。当宏的参数是编译器可以优化到另一个文字的文字时,就会发生这种情况。 

#error 

#ERROR指令停止编译。遇到一个标准时,标准指定编译器应发出包含指令中剩余标记的诊断。这主要用于调试目的。

程序员在条件块中使用“#error”,以便在块的开头的“#if”或“#ifdef”检测到编译时问题时立即停止编译器。通常,编译器会跳过块(以及其中的“#error”指令)并继续编译。

  #错误信息

#warning 

许多编译器都支持 #warning指令。遇到一个时,编译器会发出一个包含指令中剩余标记的诊断信息。

  #警告信息

#undef

和#undef指令取消定义的宏。不需要先前定义标识符。

#if,#else,#elif,#endif(条件)

所述的#if命令检查一个控制条件表达式是否求值为零或非零,并排除或包括分别的代码块。例如:

 #if 1 
    / *此块将包含在* / 
 #endif 
 #if 0中
    / *此块不包含在* / 
#endif中

除了赋值运算符,递增和递减运算符,address-of运算符和sizeof运算符之外,条件表达式可以包含任何C运算符。

预处理中使用的唯一运算符是定义的运算符。如果当前定义了可选地括在括号中的宏名称,则返回1; 如果不是0。

#ENDIF命令结束的块开始通过#if#ifdef#ifndef

#elif指令命令类似于#if,不同之处在于它被用于从一系列代码块中提取一个。例如:

 #if / *一些表达式* /
   :
   :
   :
 #elif / *另一个表达式* /
   :
 / *在这里想象更多#elifs ... * /
   :
 #其他
 / *如果以前没有#if或者,则选择#else块
    #elif块被选中* /
   :
   :
 #endif / * #if块的结尾* /

#ifdef,#ifndef 

所述的#ifdef命令类似于#if,不同之处在于,如果一个宏名称被定义下面它被选择的代码块。在这方面,

#ifdef NAME

相当于

#if定义了NAME

所述的#ifndef命令类似于支持#ifdef,不同之处在于测试是相反的:

#ifndef NAME

相当于

#if!定义了NAME

#line 

此预处理程序指令用于将指令后面的行的文件名和行号设置为新值。这用于设置__FILE__和__LINE__宏。

用于调试的有用预处理器宏

ANSI C定义了一些有用的预处理器宏和变量,]也称为“魔术常量”,包括:

__FILE__ =>当前文件的名称,作为字符串文字
__LINE__ =>源文件的当前行,作为数字文字
__DATE__ =>当前系统日期,作为字符串
__TIME__ =>当前系统时间,作为字符串
__TIMESTAMP__ = > 
C编译器编译C代码时的日期和时间(非标准)__cplusplus => undefined; 199711L当您的C代码由符合1998 C ++标准的C ++编译器编译时。
__func__ =>源文件的当前函数名,作为字符串(C99的一部分)
__ PRETTY_FUNCTION__ =>“decorated”源文件的当前函数名称,作为字符串(在GCC中;非标准)

编译时断言

编译时断言可以帮助您比仅使用运行时assert()语句更快地进行调试,因为编译时断言都在编译时进行了测试,而程序的测试运行可能无法执行某些运行-time assert()语句。

在C11标准之前,有些人 定义了一个预处理器宏来允许编译时断言,例如:

#define COMPILE_TIME_ASSERT(pred)switch(0){case 0:case pred:;}

COMPILE_TIME_ASSERT ( BOOLEAN  CONDITION  );

static_assert.hpp Boost库定义了类似的宏。

从C11开始,这样的宏已经过时了,因为_Static_assert它的宏等价物static_assert被标准化并内置于该语言中。

X-Macros 

Wikibookian建议将本书或章节合并到C Programming / Serialization#X-Macros中
请讨论是否应在讨论页面上进行此合并。

一个鲜为人知的C预处理器使用模式称为“X-Macros”。 X-Macro是头文件或宏。通常这些使用扩展名“.def”而不是传统的“.h”。此文件包含类似的宏调用列表,可以称为“组件宏”。然后以下列模式重复引用包含文件。这里,include文件是“xmacro.def”,它包含样式“foo(x,y,z)”的组件宏列表。

#define foo(x,y,z)doSomethingWith(x,y,z); 
#include  “xmacro.def”
#undef foo

#define foo(x,y,z)doSomethingElseWith(x,y,z); 
#include  “xmacro.def”
#undef foo

(等......)

X-Macros最常见的用法是建立C对象列表,然后自动为每个对象生成代码。一些实现还在#undefX-Macro内执行他们需要的任何操作,而不是期望调用者取消定义它们。

常见的对象集是一组全局配置设置,一组结构成员,用于将XML文件转换为快速遍历树的可能XML标记列表,或枚举声明的主体; 其他清单是可能的。

一旦处理了X-Macro以创建对象列表,就可以重新定义组件宏以生成例如访问器和/或增变器功能。结构序列化和反序列化也是常见的。

下面是一个X-Macro的示例,它建立一个结构并自动创建序列化/反序列化函数。为简单起见,此示例不考虑字节顺序或缓冲区溢出。

文件star.def

EXPAND_EXPAND_STAR_MEMBER (x , int )
EXPAND_EXPAND_STAR_MEMBER (y , int )
EXPAND_EXPAND_STAR_MEMBER (z , int )
EXPAND_EXPAND_STAR_MEMBER (radius , double )
#undef EXPAND_EXPAND_STAR_MEMBER

文件star_table.c

typedef  struct  { 
  #define EXPAND_EXPAND_STAR_MEMBER(member,type)类型成员; 
  #include  “star.def”
  }  starStruct ;

void  serialize_star (const  starStruct  * const  star , unsigned  char  * buffer ) { 
  #define EXPAND_EXPAND_STAR_MEMBER(member,type)\ 
    memcpy(buffer,&(star-> member),sizeof(star-> member)); \ 
    buffer + = sizeof(star-> member); 
  #include  “star.def”
  }

空隙 deserialize_star (starStruct  * const的 星, const的 无符号 字符 * 缓冲器) { 
  #定义EXPAND_EXPAND_STAR_MEMBER(构件,类型)\ 
    的memcpy(&(星形>部件),缓冲液,的sizeof(星形>构件)); \ 
    buffer + = sizeof(star-> member); 
  #include  “star.def”
  }

可以使用令牌连接(“ ##”)和引用(“ #”)运算符来创建和访问各个数据类型的处理程序。例如,以下代码可能会添加以下内容:

#define print_int(val)printf(“%d”,val)
#define print_double(val)printf(“%g”,val)

void  print_star (const  starStruct  * const  star ) { 
  / * print _ ## type将替换为print_int或print_double * / 
  #define EXPAND_EXPAND_STAR_MEMBER(member,type)\ 
    printf(“%s:”,#member); \ 
    print _ ## type(star-> member); \ 
    printf(“\ n”); 
  #include  “star.def”
  }

请注意,在此示例中,您还可以通过定义每种受支持类型的打印格式来避免为此示例中的每种数据类型创建单独的处理函数,还可以减少此头文件生成的扩展代码:

#define FORMAT_(type)FORMAT _ ## type 
#define FORMAT_int“%d” 
#define FORMAT_double“%g”

void  print_star (const  starStruct  * const  star ) { 
  / * FORMAT_(type)将替换为FORMAT_int或FORMAT_double * / 
  #define EXPAND_EXPAND_STAR_MEMBER(member,type)\ 
    printf(“%s:”FORMAT_(type)“\ n”, #member,star-> member); 
  #include  “star.def”
  }

通过创建包含文件内容的单个宏,可以避免创建单独的头文件。例如,上面的文件“star.def”可以在以下开头用这个宏替换:

文件star_table.c

#define EXPAND_STAR \ 
  EXPAND_STAR_MEMBER(x,int)\ 
  EXPAND_STAR_MEMBER(y,int)\ 
  EXPAND_STAR_MEMBER(z,int)\ 
  EXPAND_STAR_MEMBER(radius,double)

然后所有调用#include "star.def"都可以用简单的EXPAND_STAR语句替换。上述文件的其余部分将变为:

typedef  struct  { 
  #define EXPAND_STAR_MEMBER(member,type)类型成员; 
  EXPAND_STAR 
  #undef EXPAND_STAR_MEMBER 
  }  starStruct ;

void  serialize_star (const  starStruct  * const  star , unsigned  char  * buffer ) { 
  #define EXPAND_STAR_MEMBER(member,type)\ 
    memcpy(buffer,&(star-> member),sizeof(star-> member)); \ 
    buffer + = sizeof(star-> member); 
  EXPAND_STAR 
  #undef EXPAND_STAR_MEMBER 
  }

空隙 deserialize_star (starStruct  * const的 星, const的 无符号 字符 * 缓冲器) { 
  #定义EXPAND_STAR_MEMBER(构件,类型)\ 
    的memcpy(&(星形>部件),缓冲液,的sizeof(星形>构件)); \ 
    buffer + = sizeof(star-> member); 
  EXPAND_STAR 
  #undef EXPAND_STAR_MEMBER 
  }

并且可以添加打印处理程序以及:

#define print_int(val)printf(“%d”,val)
#define print_double(val)printf(“%g”,val)

void  print_star (const  starStruct  * const  star ) { 
  / * print _ ## type将替换为print_int或print_double * / 
  #define EXPAND_STAR_MEMBER(member,type)\ 
    printf(“%s:”,#member); \ 
    print _ ## type(star-> member); \ 
    printf(“\ n”); 
  EXPAND_STAR 
  #undef EXPAND_STAR_MEMBER 
}

或作为:

#define FORMAT_(type)FORMAT _ ## type 
#define FORMAT_int“%d” 
#define FORMAT_double“%g”

void  print_star (const  starStruct  * const  star ) { 
  / * FORMAT_(type)将替换为FORMAT_int或FORMAT_double * / 
  #define EXPAND_STAR_MEMBER(member,type)\ 
    printf(“%s:”FORMAT_(type)“\ n”, #member,star-> member); 
  EXPAND_STAR 
  #undef EXPAND_STAR_MEMBER 
  }

避免需要知道任何扩展子宏的成员的变体是接受运算符作为列表宏的参数:

文件star_table.c

/ * 
Generic 
* / 
#define STRUCT_MEMBER(成员,类型,虚拟)类型成员;

#define SERIALIZE_MEMBER(member,type,obj,buffer)\ 
  memcpy(buffer,&(obj-> member),sizeof(obj-> member)); \ 
  buffer + = sizeof(obj-> member);

#define DESERIALIZE_MEMBER(member,type,obj,buffer)\ 
  memcpy(&(obj-> member),buffer,sizeof(obj-> member)); \ 
  buffer + = sizeof(obj-> member);

#define FORMAT_(type)FORMAT _ ## type 
#define FORMAT_int“%d” 
#define FORMAT_double“%g”

/ * FORMAT_(type)将替换为FORMAT_int或FORMAT_double * / 
#define PRINT_MEMBER(member,type,obj)\ 
  printf(“%s:”FORMAT_(type)“\ n”,#member,obj-> member) ;

/ * 
starStruct 
* /

#define EXPAND_STAR(_,...)\ 
  _ _(x,int,__ VA_ARGS__)\ 
  _(y,int,__ VA_ARGS__)\ 
  _ _(z,int,__ VA_ARGS__)\ 
  _ _(radius,double,__ VA_ARGS__)

typedef  struct  { 
  EXPAND_STAR (STRUCT_MEMBER , )
  }  starStruct ;

void  serialize_star (const  starStruct  * const  star , unsigned  char  * buffer ) { 
  EXPAND_STAR (SERIALIZE_MEMBER , star , buffer )
  }

空隙 deserialize_star (starStruct  * const的 星, const的 无符号 字符 * 缓冲器) { 
  EXPAND_STAR (DESERIALIZE_MEMBER , 星形, 缓冲器)
  }

void  print_star (const  starStruct  * const  star ) { 
  EXPAND_STAR (PRINT_MEMBER , star )
  }

这种方法可能很危险,因为整个宏集总是被解释为它在单个源代码行上,这可能会遇到具有复杂组件宏和/或长成员列表的编译器限制。

Lars Wirzenius [在2000年1月17日的一个网页上报告了这种技术,他在1997年之前将Kenneth Oksanen称为“精炼和开发”该技术。其他参考文献将其描述为来自at的方法。至少在世纪之交的十年之前。

猜你想读:《C编程.高级C》3.网络

THE END
分享