首页 > 技术文章 > C宏定义的特殊关键字

caso 2020-01-29 19:30 原文

  在分析一些C源码时,经常会遇到各种宏定义操作,本文即总结一下C语言宏定义中常见的预定义宏、调试宏;宏的条件编译用法及特殊的宏关键字用法。

#undef 限定宏的作用域

  一般来讲宏的作用域从 #define 开始直到文件末尾,但如果需要限定宏的作用域就可以用 #undef 来限定宏的作用域,就是该宏只能作用与 #define … #undef 区间内

常见预定义宏

  预定义宏也称为编译器内置宏,这些宏并没有定义在哪个头文件中,而是编译器在预处理时会自动处理这些预定义宏,这里介绍几个常用的预定义宏,其中__func__、__FILE__、__LINE__三个宏在调试时经常用到,来定位bug,为了方便可以自己定义一个DEBUG宏: #define DEBUG printf("%s %s %d\n", __FILE__, func, __LINE__);

  • __DATE__:代表预处理的日期,预处理器会将其替换为"月 日 年"的字符串形式的时间
  • __TIME__:代表对源文件当前预编译的时间,预处理器会将其替换为“hh:mm:ss”格式的时间
  • __FILE__:代表当前预编译的源文件文件名,预处理器会将其替换为当前宏所在的文件名
  • __LINE__:代表当前所在行号,预处理器会将其替换为当前宏所在的行号
  • __func__:会替换为当前__func__所在的函数名
    如下代码:
void test_1() {
    printf("预编译的日期:%s\n", __DATE__);
    printf("预编译的时间:%s\n", __TIME__);
    printf("预编译的文件:%s\n", __FILE__);
    printf("当前所在行号:%d\n", __LINE__);
    printf("当前所在函数:%s\n", __func__);
}

执行测试结果:

E:\C_demo>Chong.exe
预编译的日期:Jan 29 2020
预编译的时间:16:54:24
预编译的文件:e:\C_demo\Chong.c
当前所在行号:18
当前所在函数:test_1

宏的条件编译

  所谓条件编译,也就是在预编译时,预编译器可以通过 “条件编译” 帮你保留某些代码、以及帮你去掉某些代码,第二阶段编译时就只编译保留的代码,条件编译可以包含任意内容,可以是 #include 头文件、整个函数、或者函数内部的代码块、变量定义、或是整个文件的内容(一般头文件中比较多)。条件编译在Linux驱动代码以及一些跨平台软件代码中非常常用,针对不同的硬件环境选择性编译不同的代码块来提高代码的兼容性。下面列出常见的条件编译用法的例子:

#ifdef 和 #ifndef

#ifdef 如果定义了该宏,就编译对应的 #endif 之间的代码块;#ifndef 如果没有定义该宏,就编译对应的 #endif 之间的代码块

#define A
#define C
void test_2() {
#ifdef A
    printf("This is A.\n");
#endif
    printf("This is B.\n");
#ifndef C
    printf("This is C.\n");
#endif
}

执行测试代码:

E:\C_demo>Chong.exe
This is A.
This is B.

#if 用法
void test_3() {
#if 0 // 表达式为假
    printf("条件为假,不会预编译这行代码\n");
#endif
    printf("预编译这行代码\n");
}

如果 #if 表达式为真,预编译保留中间代码,为假时,则不保留中间代码
执行该代码如下:

E:\C_demo>Chong.exe
预编译这行代码

#if 常常会和 defined、#elif、#else 搭配使用

  • #if defined : 功能与 #ifdef 是一样的,这时通过宏存不存在来判断真假,与 #ifdef 不同,#if define 可以实现宏的 “&&”、 “||” 运算
  • #if !defined :与 #if defined 刚好相反
#define A
#define B
void test_4() {
#if defined A && defined B
    printf("如果 A 和 B 都定义\n");
#endif
#if !defined A || !defined B
    printf("如果没有定义 A 或者没有定义 B");
#endif    
}

执行测试代码如下:

E:\C_demo>Chong.exe
如果 A 和 B 都定义

  • #elif 和 #else:else if defined,否则如果定义该宏为真,保留该代码;#else 否则,保留以下代码
void test_5() {
#if 0
    printf("#if 条件为真,保留这句代码\n");
#elif defined A
    printf("如果定义了A,保留这句代码\n");
#else
    printf("如果以上两个都不满足,保留这段代码\n");
#endif    
}

执行测试代码结果如下:

E:\C_demo>Chong.exe
如果以上两个都不满足,保留这段代码

#line

可以根据自己的需求,修改__LINE__和__FILE__的值,也就是修改行号和文件名,并且修改的值永久生效。如下测试代码:

void test_6() {
    printf("修改文件名和行号之前:%s: %d\n", __FILE__, __LINE__);
#line 100 "test.c"
    printf("修改文件名和行号之后:%s: %d\n", __FILE__, __LINE__);
}

执行以上测试代码结果:

E:\C_demo>Chong.exe
修改文件名和行号之前:e:\C_demo\Chong.c: 65
修改文件名和行号之后:test.c: 100

#error

  在Linux驱动移植中,往往会修改源码中的宏定义比较多,因为条件编译需要这些宏来打开和关闭,所以在预编译阶段如果就能准确的提示缺少某个宏的话,可以帮助我们快速排查宏的错误。

  • #error 能够帮助我们在“预编译”阶段,及时准确的报出与该宏相关的错误并且退出预编译;
  • #error是在“预编译阶段”由预编译器处理的“预编译关键字”;
  • #error输出字符串时,信息内容不需要使用括号括起来;
  • 执行#error后,“预编译”的处理过程会被立即终止并报错;
void test_7() {
#ifdef PI 
    printf("%d\n", PI); 
#else						
#error PI not defined!					
#endif
}

当 PI 没有定义时,编译器编译以上代码会打印报错,提示该宏没有定义:

Executing task: D:\mingw-w64\mingw64\bin\gcc.exe -g e:\C_demo\Chong.c -o Chong.exe <
e:\C_demo\Chong.c: In function ‘test_6’:
e:\C_demo\Chong.c:66:2: error: #error PI not defined!
#error PI not defined!
^~~~~
The terminal process terminated with exit code: 1

# 用法

将 # 传入的参数变成字符串,如下代码

#define S1(s) #s
#define S2(s) #s" Friend"
#define S3(s) "Hello "#s

void test_8() {
    printf("%s\n", S1(Hello Friend));
    printf("%s\n", S2(Hello));
    printf("%s\n", S3(Friend));
}

执行以上代码,输出三个 “Hello Friend”,通过预编译后查看代码也能够看出,该宏会将参数转换成字符串

## 用法

将两个标识符连接在一起,合成一个标识符。还是一样举个例子说明

#define MERGE(a, b) a##b
#define PRINT1 printf("print one\n");
#define PRINT2 printf("print two\n"); 
void test_9() {
    MERGE(PRINT, 1);  // 宏替换后是 PRINT1;
    MERGE(PRINT, 2);
}
#undef MERGE

执行以上代码结果如下:

E:\C_demo>Chong.exe
print one
print two

#pragma 用法

  • #pragma 的作用: 通过#pragma指定的某些设置,然后通过这些设置告诉编译器在预编译或者编译时,完成某些特定的事情。
  • #pragma 的特点:预编译关键字都是在“预编译”阶段被处理的,预编译之后就看不到了,但是 #pragma 就不一定了,#pragma的大多数用法是在预编译阶段处理的,但是有些少数情况是在第二阶段“编译”时处理的。
  • #pragma 的使用格式:#pragma parameter
  • #pragma 的参数表:

*表示仅C++支持,其它的C/C++都支持
alloc_text | comment | init_seg* | optimize | auto_inline | component
inline_depth | pack | bss_seg | data_seg | inline_recursion | pointers_to_members*
check_stack | function | intrinsic | setlocale | code_seg | hdrstop
message | vtordisp* | const_seg | include_alias | once | warning

  • #pragma once:与#ifndef一样,可以用于防止头文件的重复包含
  • #pragma message(message string):在编译时打印提示信息,不是在预编译
  • #pragma pack:内存对齐
  • bss_seg、data_seg、code_seg、const_seg参数:
    1> bss_seg:修改和设置.bss节
    2> data_seg:修改和设置.data节
    3> const_seg:修改和设置.ro.data节
    4> code_seg:修改和设置.text节

关于一些常用的宏定义操作就先记录到此了,对于 #pragma 宏的使用,之后有接触更多用法再作更新…

推荐阅读