跳转到内容

C 编程/预处理器指令和宏

来自维基教科书,开放的书籍,开放的世界
上一页:UNIX 中的网络 C 编程 下一页:常见做法

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

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

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

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

C 语言包含一些作为语言一部分的功能,还包含一些作为标准库一部分的功能,标准库是与每个符合标准的 C 编译器一起提供的代码库。当 C 编译器编译程序时,它通常也会将程序与标准 C 库链接。例如,在遇到#include <stdio.h>指令时,它会用stdio.h头文件的内容替换该指令。stdio.h头文件。

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

#include <stdio.h>

上面的代码行会导致 C 声明(在stdio.h 头文件中)被包含在您的程序中以便使用。通常,这是通过将头文件称为stdio.h的内容插入到您的程序中来实现的,该文件位于系统相关的位置。此类文件的位置可能在您的编译器文档中描述。标准 C 头文件的列表在下面的“头文件”表中列出。

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

如果使用与上面的示例类似的尖括号,则预处理器被指示沿着标准包含的开发环境路径搜索包含文件。

#include "other.h"

如果您使用引号(" "),则预处理器预计会在一些额外的、通常是用户定义的位置中搜索头文件,并且仅当它在这些额外的位置中找不到时才会回退到标准包含路径。这种形式通常包括在包含#include指令的文件所在的相同目录中进行搜索。

注意:您应该检查正在使用的开发环境的文档以了解#include指令的文件所在的相同目录中进行搜索。

头文件

[编辑 | 编辑源代码]

C90 标准头文件列表

自 C90 以来添加的头文件

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

Pragma 用于源程序中。

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

例如,最常实现的预处理器指令之一是 #pragma once,当它放在头文件开头时,表示如果预处理器多次包含它所在的代码,则会跳过该文件。

注意:存在其他方法来执行此操作,通常被称为使用包含守卫



每个 #define 预处理器指令都定义一个宏。例如,

   #define PI 3.14159265358979323846 /* pi */

以名称后紧跟空格定义的宏称为常量或文字。以名称后紧跟括号定义的宏称为函数式宏。[2]

警告:虽然预处理器宏很诱人,但如果使用不当,可能会产生意想不到的结果。请始终牢记,宏是在编译任何内容之前对源代码进行的文本替换。编译器不知道宏,也永远不会看到它们。这可能会导致难以理解的错误,以及其他负面影响。如果存在等效功能,请优先使用语言功能。例如,使用 const intenum 而不是 #define 定义的常量)。

也就是说,在某些情况下,宏非常有用(请参阅下面的 debug 宏以了解示例)。

stdio.h#define指令用于定义宏。宏由预处理器在编译之前用于操作程序源代码。由于预处理器宏定义是在编译器对源代码进行操作之前进行替换的,因此由#define引入的任何错误都很难追踪。

按照惯例,使用#define定义的宏以大写字母命名。虽然这样做不是必需的,但被认为是一种非常不好的做法。这使得在阅读源代码时可以轻松识别宏。(我们在后面的章节中提到了使用 #define 的许多其他常见约定,C 编程/常见做法)。

今天,#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=8j=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=3j=5。这对于任何期望的人来说都是非常令人惊讶的MAX()表现得像一个函数。

那么正确的解决方案是什么?解决方案是根本不使用宏。一个全局的内联函数,像这样

inline int max(int a, int b) { 
  return 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( "I'm eating " #what " today." )
eat( fruit )

宏处理器将将其转换为

puts( "I'm eating " "fruit" " today." )

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

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

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

puts(num2str(CONST));

这有点棘手,因为它分两步展开。首先,num2str(CONST) 被替换为str(23),这反过来被替换为"23"。这在以下示例中很有用

#ifdef DEBUG
#define debug(msg) fputs(__FILE__ ":" num2str(__LINE__) " - " msg, stderr)
#else
#define debug(msg)
#endif

这将为您提供一个不错的调试消息,包括发出消息的文件和行。但是,如果未定义 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);
   return 0;
 }

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

注意
通常不建议在头文件中定义宏。

只有在无法通过函数或其他机制实现相同结果时,才应定义宏。一些编译器能够优化代码,以将对小函数的调用替换为内联代码,从而消除了任何可能的加速优势。使用 typedef、枚举和inline(在 C99 中)通常是一个更好的选择。

内联函数无法正常工作的情况之一(因此您几乎被迫使用类似函数的宏)是初始化编译时常量(结构的静态初始化)。当宏的参数是编译器可以优化为另一个文字的文字时,就会发生这种情况。 [3]

#error

[edit | edit source]

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

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

  #error message

#warning

[edit | edit source]

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

  #warning message

#undef

[edit | edit source]

#undef 指令取消定义宏。标识符不必先前已定义。

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

[edit | edit source]

#if 命令检查控制条件表达式是否计算为零或非零,并分别排除或包含代码块。例如

 #if 1
    /* This block will be included */
 #endif
 #if 0
    /* This block will not be included */
 #endif

条件表达式可以包含任何 C 运算符,除了赋值运算符、递增和递减运算符、取地址运算符和 sizeof 运算符。

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

#endif 命令结束由 #if#ifdef#ifndef 启动的块。

#elif 命令类似于 #if,但它用于从一系列代码块中提取一个。例如

 #if /* some expression */
   :
   :
   :
 #elif /* another expression */
   :
 /* imagine many more #elifs here ... */
   :
 #else
 /* The optional #else block is selected if none of the previous #if or
    #elif blocks are selected */
   :
   :
 #endif /* The end of the #if block */

#ifdef,#ifndef

[edit | edit source]

#ifdef 命令类似于 #if,但如果宏名称已定义,则选择其后的代码块。在这方面,

#ifdef NAME

等效于

#if defined NAME


#ifndef 命令类似于 #ifdef,但测试结果相反

#ifndef NAME

等效于

#if !defined NAME

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

有用的预处理宏用于调试

[编辑 | 编辑源代码]

ANSI C 定义了一些有用的预处理宏和变量,[4][5] 也称为“神奇常量”,包括

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

编译时断言

[编辑 | 编辑源代码]

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

在 C11 标准之前,有些人[6][7][8] 定义了一个预处理宏来允许编译时断言,类似于

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

COMPILE_TIME_ASSERT( BOOLEAN CONDITION );

static_assert.hpp Boost 库 定义了一个类似的宏。[9]

从 C11 开始,这些宏已经过时,因为 _Static_assert 及其宏等效项 static_assert 已标准化并内置于语言中。

C 预处理器的另一个鲜为人知的用法模式被称为“X-宏”。[10][11][12][13] X-宏是一个 头文件 或宏。通常它们使用扩展名“.def”而不是传统的“.h”。该文件包含一个类似宏调用的列表,可以称为“组件宏”。然后,该包含文件在以下模式中被反复引用。这里,包含文件是“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

(etc...)

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

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

一旦 X-宏已被处理以创建对象列表,就可以重新定义组件宏来生成例如 访问器和/或修改器 函数。结构 序列化和反序列化 也通常完成。

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

文件 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) type member;
  #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"
  }

void deserialize_star(starStruct *const star, const unsigned char *buffer) {
  #define EXPAND_EXPAND_STAR_MEMBER(member, type) \
    memcpy(&(star->member), buffer, sizeof(star->member)); \
    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 will be replaced with print_int or 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) will be replaced with FORMAT_int or 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) type member;
  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
  }

void deserialize_star(starStruct *const star, const unsigned char *buffer) {
  #define EXPAND_STAR_MEMBER(member, type) \
    memcpy(&(star->member), buffer, sizeof(star->member)); \
    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 will be replaced with print_int or 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) will be replaced with FORMAT_int or 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(member, type, dummy) type 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) will be replaced with FORMAT_int or 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)
  }

void deserialize_star(starStruct *const star, const unsigned char *buffer) {
  EXPAND_STAR(DESERIALIZE_MEMBER, star, buffer)
  }

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

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

该技术由 Lars Wirzenius[14] 在 2000 年 1 月 17 日的网页中报道,他在其中将“完善和发展”该技术归功于 Kenneth Oksanen,该技术早于 1997 年。其他参考文献将其描述为至少在世纪之交前十年的方法。


我们在后面的部分更详细地讨论了 X-宏,序列化和 X-宏

  1. 理解 C++/C 预处理器
  2. "为乐趣和利益利用预处理器".
  3. David Hart, Jon Reid. "预处理器的 9 种代码异味". 2012.
  4. HP C 编译器参考手册
  5. C++ 参考:预定义预处理变量
  6. "C 中的编译时断言",作者 Jon Jagger,1999 年
  7. Pádraig Brady. "静态断言".
  8. "三元运算符与常量(真)值?".
  9. 维基百科:C++0x#静态断言
  10. Wirzenius, Lars. "C 预处理器技巧:实现类似的数据类型". 检索于 2011 年 1 月 9 日.
  11. Meyers, Randy (2001 年 5 月 1 日). "新的 C:X 宏". Dr. Dobb's Journal. 检索于 2024 年 4 月 5 日.
  12. Beal, Stephan (2004 年 8 月 22 日). "超级宏". 检索于 2008 年 10 月 27 日.{{cite web}}: CS1 maint: date and year (link)
  13. Keith Schwarz. "高级预处理器技术". 2009 年. 包含 "预处理器的实际应用 II:X 宏技巧".
  14. Wirzenius, Lars. C 预处理器技巧:实现类似的数据类型 检索于 2011 年 1 月 9 日.
上一页:错误处理 C 编程 下一页:常见做法
华夏公益教科书