返回介绍

3. 预处理

发布于 2024-10-12 21:58:10 字数 6664 浏览 0 评论 0 收藏 0

编译器会对每个源码文件进行预处理。预处理操作插入头文件内容,识别条件编译代码段,删除注释,并用定义替换宏。预处理指令用 # 开头,以换行结束。且在 # 之前只能有空白符。

可用 \ 续行符换行。预处理器会删除换行符和续行符,使其合并成一行。所以反斜线后面不能有任何内容,包括注释。

宏定义

所谓宏(macro)就是为文本定义名字,随后被预处理器直接展开并替换。

宏名称通常全部大写,以区分变量。如果宏内容为表达式,应该用括号包含起来,避免展开后因优先级导致错误。

宏直接文本替换,不能用来创建新的宏定义,预处理器不会递归处理。

#define A #define B print("abc")

int main (void) 
{
	A
	return 0;
}
$ gcc -E main.c

int main (void)
{
    #define B print("abc")
    return 0;
}

也可仅作为条件,没有文本内容。

#define X

#ifdef X
printf("aa");
#endif

宏定义作用于整个翻译单元。不能重复定义,除非先用 #undef 取消。

宏函数

可为宏指定参数,使其看上去像函数。预处理器先替换参数,随后在引用位置替换宏文本。

参数标识符以逗号分隔,没有类型,因为直接文本替换。注意宏名字和参数列表左括号之间没有空格,否则就成了无参数宏了。如果调用参数也是宏,则参数宏先被替换。

参数列表使用 ... 表示可选参数 __VA_ARGS__

#define PRINT(fmt, name, ...) \
    printf(fmt, name, __VA_ARGS__)

int main (void) 
{
    PRINT("%s, %d, %d\n", "test", 1, 2);
    return 0;
}
$ gcc -E main.c

int main (void)
{
    printf("%s, %d, %d\n", "test", 1, 2);
    return 0;
}

如作为独立计算单元,那么需有独立作用域,避免和展开处冲突。用 ({...}) 包含起来,其最后一个表达式作为返回值。

区别于纯逗号运算符。它从左到右处理,并返回最后一个。 (1,2,3)-> 3

#define ADD(a, b)    \
({                   \
    int z = a + b;   \
    z;               \
})

int main (void) 
{
    int x = ADD(1, 2);
    printf("%d\n", x);
    return 0;
}
$ gcc -E main.c

int main (void)
{
    int x = ({ int z = 1 + 2; z; });
    printf("%d\n", x);
    return 0;
}

如没有返回值,直接用单次循环语句包含即可。

#define TEST(a, b)      \
do {                    \
    int z = a + b;      \
    printf("%d\n", z);  \
} while(0)

int main (void) 
{
    TEST(1, 2);
    return 0;
}
$ gcc -E main.c

int main (void)
{
    do { int z = 1 + 2; printf("%d\n", z); } while(0);
    return 0;
}

操作符

基本上是将参数转换为字符串。

  • 单个 # 将参数转换为引号包含的字符串字面量。
  • ## 将左右标识符连接起来。

自动对相关符号做转义处理,使其符合语法,并原样输出。

struct user {
    char *name;
    void(*action)(void);
}

#define USER(name) { #name, name ## _command }

int main (void) 
{
    struct user users[] = {
        USER(a),
        USER(b),
    };

    return 0;
}
$ gcc -E main.c

int main (void)
{
    struct user users[] = {
        { "a", a_command },
        { "b", b_command },
    };

    return 0;
}

泛型选择

_Generic ,根据条件表达式类型返回特定结果。

#define DEFAULT(x)    \
    _Generic((x),     \
        int    : 1,   \
        float  : 1.2, \
        default: 0    \
    )

int main (void) 
{
    float x = DEFAULT(x);
    printf("%f\n", x);

    return 0;
}
$ gcc -E main.c

int main (void)
{
    float x = _Generic((x), int : 1, float : 1.2, default: 0 );;
    printf("%f\n", x);

    return 0;
}

表达式不能直接提供 int、double 这样的类型关键字。

实现 typename 宏函数。

#define typename(x) _Generic((x),        /* Get the name of a type */               \
                                                                                  \
        _Bool: "_Bool",                  unsigned char: "unsigned char",          \
         char: "char",                     signed char: "signed char",            \
    short int: "short int",         unsigned short int: "unsigned short int",     \
          int: "int",                     unsigned int: "unsigned int",           \
     long int: "long int",           unsigned long int: "unsigned long int",      \
long long int: "long long int", unsigned long long int: "unsigned long long int", \
        float: "float",                         double: "double",                 \
  long double: "long double",                   char *: "pointer to char",        \
       void *: "pointer to void",                int *: "pointer to int",         \
      default: "other")

int main (void) 
{
    char *s = typename(1);
    return 0;
}

可返回函数,包括提供调用参数。

#define ADD(x, y)           \
    _Generic((x),           \
        int    : int_add,   \
        double : float_add, \
        default: str_add    \
    )(x, y)

int main (void) 
{
    ADD(1, 2);
    return 0;
}
$ gcc -E main.c

int main (void)
{
    _Generic((1), int: int_add, double: float_add, default: str_add )(1, 2);
    return 0;
}

如只获取参数类型,可使用 typeof( -std=gnu11 )。

#define X(x, y)          \
do {                     \
    typeof(x) z = x + y; \
    printf("%d\n", z);   \
} while(0)

int main (void) 
{
    X(1, 2);
    return 0;
}

预定义宏

ISO C 支持的几个预定义宏。

  • __DATE__ : 当前日期。
  • __TIME__ : 当前时间。
  • __FILE__ : 源码文件名(不含路径)。
  • __LINE__ : 行号。
  • __func__ : 函数名。
  • __STDC__ : 编译器是否符合 ISO C 规范(1)。
  • __STDC_VERSION__ : ISO C 版本号。
int main (void) 
{
    printf("%s, %s, %s, %d, %s\n", 
        __DATE__, __TIME__,
        __FILE__, __LINE__,
        __func__);
    
    return 0;
}
#if __STDC_VERSION__ < 201710L
    #error "C17"
#endif

调试器支持

使用 -g3 编译参数,使调试信息包含宏定义。

#define X(a)           \
do {                   \
    int z = a;         \
    z++;               \
    printf("%d\n", z); \
} while(0)

int main (void) 
{
    X(100);
    return 0;
}
$ gcc -g3 -O0 -o test main.c
$ gdb test

(gdb) info macro X
Defined at /root/c/test/main.c:4
#define X(a) do { int z = a; z++; printf("%d\n", z); } while(0)

(gdb) macro expand X(10)
expands to: do { int z = 10; z++; printf("%d\n", z); } while(0)

条件编译

使用 #if...#elif...#else...#endif 按条件选择源码。

#if#elif 条件表达式必须是整形常量,不能使用转换语句。或用 defined 判断某个宏是否定义。但可能已被 #undef 取消。可直接使用 #ifdef#ifndef ,等价于 #if defined(...)#if !define(...)

可结合 -D<macro> 编译参数使用。

#ifdef RELEASE
	puts("release version");
#else
	puts("debug version");
#endif
$ gcc -DRELEASE -E main.c

编译器指令

  • #error : 让预处理器发出错误信息。
  • #pragma : 向编译器提供附加信息。
#ifndef __STDC__
	#error  "This compiler does not conform to the ANSI C standard."
#endif
#pragma pack(8)
struct data {
    char a;
    long double b;
};
#pragma pack()

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文