跳转到内容

C 编程/预处理指令和宏

来自维基教科书,开放书籍,开放世界
前一页: UNIX 中的网络 C 编程 下一页: 通用实践

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

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

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

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

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

当你使用库中的功能时,C 要求你声明你将使用什么。程序中的第一行是一个预处理指令,它应该如下所示

#include <stdio.h>

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

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

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

#include "other.h"

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

注意:你应该检查你正在使用的开发环境的文档,以了解任何供应商特定的实现#include指令的文件所在的相同目录中搜索。

头文件

[编辑 | 编辑源代码]

C90 标准头文件列表

自 C90 以来添加的头文件

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

Pragmas 用于源程序中。

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

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

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



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

   #define PI 3.14159265358979323846 /* pi */

在名称后面紧跟着一个空格的宏称为常量或文字。在名称后面紧跟着一个括号的宏称为类函数宏。[2]

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

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

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

按照惯例,使用#define定义的宏使用大写字母命名。虽然这不是必需的,但反之则被认为是非常糟糕的习惯。这样一来,在阅读源代码时可以很容易地识别宏。(在后面的章节中,我们提到了许多其他使用#define的通用约定,C 编程/通用实践)。

今天,#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包含表达式,则定义必须为每个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) { 
  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")

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

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

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

#error

[edit | edit source]

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

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

  #error message

#warning

[edit | edit source]

许多编译器支持#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

#line

[edit | edit source]

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

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

[edit | edit source]

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

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

编译时断言

[编辑 | 编辑源代码]

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

在 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. "带有常量(true)值的条件运算符?".
  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 维護:日期和年份 (链接)
  13. Keith Schwarz. "高级预处理器技术". 2009. 包括“预处理器的实际应用二:X 宏技巧”。
  14. Wirzenius, Lars. 用于实现类似数据类型的 C 预处理器技巧 检索于 2011 年 1 月 9 日。
上一页:错误处理 C 编程 下一页: 通用实践
华夏公益教科书