预处理指令(二)
二、条件包含
#if指令和#elif指令统称为条件表达式包含预处理指令(conditional expression inclusion preprocessing directives)。#if指令、#elif指令、#ifdef指令、#ifndef指令、#elifdef指令和#elifndef指令统称为条件包含预处理指令(conditional inclusion preprocessing directives)。
ISO/IEC 9899:2024标准对条件包含预处理指令作了一些限制:
-- 除非另有明确说明,否则控制条件包含的表达式应是整数常量表达式;表达式可以包含0个或者多个defined宏表达式、has_include表达式、has_embed表达式、 has_c_attribute表达式。
#if 5>3 //合法。 ... #endif #if 'A' == 65 //合法。 ... #endif #if true //合法,等价于#if 1。 ... #endif #if false //合法,等价于#if 0。 ... #endif
由于常量控制表达式在编译阶段步骤4评估,因此所有标识符要么是宏名,要么不是宏名;不存在关键词、枚举常量等概念。
-- defined宏表达式具有以下两种语法格式:
defined identifier defined ( identifier )
如果标识符(identifier)当前已定义为宏名(可以是预定义,也可以通过#define预处理指令定义。),则defined宏表达式值为1;否则值为0。
#define SIZE 10 //定义宏SIZE。 #if defined(SIZE) //defined(SIZE)值为1。 ... #endif #undef SIZE //取消宏SIZE定义。 #if defined(SIZE) //defined(SIZE)值为0。 ... #endif #if defined(false) //defined(false)值为1,false是预定义宏。 ... #endif
-- has_include表达式具有以下两种语法格式:
__has_include( header-name ) __has_include( header-name-tokens )
has_embed表达式具有以下两种语法格式:
__has_embed( header-name embed-parameter-sequenceopt ) __has_embed( header-name-tokens pp-balanced-token-sequenceopt )
has_include表达式和has_embed表达式的第二种格式只有在第一种格式不匹配时才被考虑,这种情况下预处理标记像在普通文本中一样处理。
-- has_include表达式括号内的预处理标记序列所标识的头文件或者源文件会按照如同这些预处理标记是#include指令中预处理标记的方式搜索,但不会进一步宏扩展(即不会使用文件内容替换预处理指令。)。这样的指令需满足#include指令的语法要求。如果搜索文件成功,has_include表达式值为1;如果搜索失败,值为0。
#if __has_include( <stdio.h> ) ... #endif #if __has_include( "gch.h" ) ... #endif
has_include表达式与#include指令具有相同的文件搜索规则和语法要求。
-- has_embed表达式中使用头文件名预处理标记序列标识的资源会按照如同这些预处理标记是#embed指令中预处理标记的方式搜索,但不会进一步宏扩展(即不会使用文件内容替换预处理指令。)。这样的指令需满足#embed指令的语法要求。
#if __has_embed( "gch.bin" )
has_embed表达式与#embed指令具有相同的文件搜索规则和语法要求。
has_embed表达式与以下宏具有相同值:
宏__STDC_EMBED_NOT_FOUND__:如果资源搜索失败,或者实现不支持#embed指令指定的嵌入形参序列中的形参,has_embed表达式值为宏__STDC_EMBED_NOT_FOUND__。
宏__STDC_EMBED_FOUND__:如果资源搜索成功,并且实现支持#embed指令指定的嵌入形参序列中的所有形参,并且资源不为空,has_embed表达式值为宏__STDC_EMBED_FOUND__。
宏__STDC_EMBED_EMPTY__:如果资源搜索成功,并且实现支持#embed指令指定的嵌入形参序列中的所有形参,但资源为空,has_embed表达式值为宏__STDC_EMBED_EMPTY__。
-- 如果实现支持has_c_attribute表达式中预处理标记指定的属性,has_c_attribute表达式会替换成非0的预处理数(匹配整数常量格式。);否则替换成0。预处理标记应匹配属性标记格式。
//检查编译器是否支持ISO/IEC 9899:2024标准中的nodiscard属性。 #if __has_c_attribute(nodiscard) == 202311L #define SUPPORT true #else #define SUPPORT false #endif
-- 宏替换后剩余的预处理标记(在将成为控制表达式的预处理标记列表中。)应符合标记的词法形式。
(注:标记包括:关键词、标识符、常量、字符串字面量、标点符号。)
#ifdef指令、#ifndef指令、#elifdef指令、#elifndef指令以及defined条件包含运算符应将__has_include、__has_embed和__has_c_attribute视为已定义的宏。
#ifdef __has_include //真。 ... #endif #ifndef __has_include //假。 ... #endif #if defined(__has_include) //真。 ... #endif
#if指令、#elif指令具有以下语法格式:
#if constant-expression new-line groupopt #elif constant-expression new-line groupopt
if、elif是指令名;constant-expression是常量控制表达式;new-line是换行符;group是代码组,代码组可以包含普通的C代码,也可以包含预处理指令。
如果控制表达式的值等于0,#if指令或者#elif指令控制的代码组将被忽略;如果控制表达式的值不等于0,#if指令或者#elif指令控制的代码组将被处理。
//检测代码运行平台。 #if defined(_WIN32) || defined(_WIN64) puts("Windows operating system."); #elif defined(__unix__) puts("Unix operating system."); #else puts("Unknown operating system."); #endif
常量控制表达式评估前,常量控制表达式中的预处理标记序列会像普通文本(normal text)一样进行宏替换;但defined宏表达式中的宏不会进行宏替换。
#define SIZE 10 //defined(SIZE)中的SIZE不会进行宏替换。 //SIZE >= 10中的SIZE会进行宏替换。 #if defined(SIZE) && SIZE >= 10 ... #endif
如果标记defined通过宏替换生成或者defined一元运算符不匹配defined宏表达式语法格式,其行为是未定义的。
(注:defined一元运算符的两种语法格式:① defined identifier,② defined(identifier)。)
#define DEF defined //DEF(__STDC__)表达式中defined通过宏替换生成,因此是未定义行为。 #if DEF(__STDC__) ... #endif
在完成所有宏扩展(macro expansion)以及defined宏表达式、has_include表达式、has_embed表达式、has_c_attribute表达式的评估后,所有剩余标识符(标识符true除外。)都会替换成预处理数0,标识符true会替换成预处理数1;然后每个预处理标记转换成标记。
#if false //等价于#if 0。 ... #elif true //等价于#elif 1。 ... #else ... #endif
(注:从ISO/IEC 9899:2024标准起,true和false是C语言关键词;ISO/IEC 9899:2024标准之前true和false是<stdbool.h>头文件中定义的宏。)
经过上述预处理后得到的结果标记构成了常量控制表达式,该表达式按照常量表达式的规则进行评估。为进行标记的转换和评估,所有有符号整数类型视为具有与intmax_t类型相同的表示形式;所有无符号整数类型视为具有与uintmax_t类型相同的表示形式。这样可避免因平台差异导致的预处理阶段整数溢出以及精度问题。
//对于16位系统,INT_MAX = 0x7FFF, UINT_MAX = 0xFFFF。 //对于16位系统,intmax_t类型可能是32位。 //预处理阶段有符号整数类型视为具有与intmax_t类型相同的表示形式。 //预处理阶段0x8000可以使用intmax_t类型表示,因此是有符号整数类型。 #if 0x8000 < INTMAX_MAX puts("0x8000 is a signed integer in the preprocessing phase."); #else puts("0x8000 is a unsigned integer in the preprocessing phase."); #endif //编译阶段无后缀十六进制整数常量类型确定的先后顺序: //int、unsigned、long、unsigned long、long long、unsigned long long。 //0x8000大于INT_MAX,并且小于UINT_MAX,因此具有unsigned int类型。 if( 0x8000 > INT_MAX ) puts("0x8000 is a unsigned integer in the compilation phase."); else puts("0x8000 is a signed integer in the compilation phase.");
上述代码可能输出:
0x8000 is a signed integer in the preprocessing phase. 0x8000 is a unsigned integer in the compilation phase.
解释字符常量时可能涉及将转义序列转换成执行字符集成员。对于字符常量值,在#if指令和#elif指令中与#if指令和#elif指令外相同字符常量是否具有相同值将由实现定义。
//下面两个表达式'A' == 65结果有可能不同。 //#if 'A' == 65使用的是预处理阶段字符常量A的值。 //if('A' == 65)使用的是编译阶段字符常量A的值。 //预处理器和编译器可能使用不同的字符集, //因此对于同一字符常量,在预处理阶段和编译阶段可能具有不同值。 #if 'A' == 65 ... #endif if('A' == 65)
单字符字符常量是否具有负值将由实现定义。
//如果char类型与signed char类型兼容,'\xFF'值为-1; //如果char类型与unsigned char类型兼容,'\xFF'值为255。 #if '\xFF' ... #endif
#ifdef指令、#ifndef指令、#elifdef指令、#elifndef指令具有以下语法格式:
#ifdef identifier new-line groupopt #ifndef identifier new-line groupopt #elifdef identifier new-line groupopt #elifndef identifier new-line groupopt
ifdef、ifndef、elifdef、elifndef是指令名;identifier是标识符;new-line是换行符;group是代码组,代码组可以包含普通的C代码,也可以包含预处理指令。
上述指令等价于:
#if defined identifier new-line groupopt #if !defined identifier new-line groupopt #elif defined identifier new-line groupopt #elif !defined identifier new-line groupopt
如果当前标识符未被定义为宏名,指令控制的代码组将被忽略;如果当前标识符定义为宏名,指令控制的代码组将被处理。
//检测代码运行平台。 #ifdef _WIN32 puts("Windows operating system."); #elifdef _WIN64 puts("Windows operating system."); #elifdef __unix__ puts("Unix operating system."); #else puts("Unknown operating system."); #endif
条件包含预处理指令的处理:
① 条件包含预处理指令中的控制条件将按顺序检查。
#if defined(_WIN32) || defined(_WIN64) puts("Windows operating system."); #elif defined(__unix__) puts("Unix operating system."); #else puts("Unknown operating system."); #endif
先检查#if指令中的控制条件,如果为真,检查结束;如果为假,将检查#elif指令中的控制条件。对于#if指令中控制条件的检查,先检查defined(_WIN32),如果为真,检查结束;如果为假,将检查defined(_WIN64)。
② 如果控制条件评估为假(0),该指令控制的代码组会被跳过。
代码组被跳过并不是简单地跳过整个代码组,具体做法是:通过对指令名的处理,确定嵌套条件语句的层级关系;指令中的其它预处理标记会和代码组中的其它预处理标记一样被忽略。
#if 0 #ifdef __STDC__ ... #endif //与#ifdef __STDC__匹配。 #else ... #endif //与#if 0匹配。
预处理器跳过时会识别出if、endif等指令名,确定它们的嵌套关系。遇到内层if指令名,会增加嵌套层数;遇到内层endif指令名,会减少嵌套层数。这样才能找到与最外层#if 0指令匹配的#else指令和#endif指令。
③ 只有第一个控制条件评估为真(非0)的代码组会被处理;其后所有的代码组都会跳过,并且它们的控制指令会像处于被跳过的代码组中一样处理。
//假设__STDC_VERSION__ == 202311L为假, //并且__STDC_VERSION__ == 201710L为真, //以下绿色背景代码会被跳过。 #if __STDC_VERSION__ == 202311L puts("Support ISO/IEC 9899:2024 standard."); #elif __STDC_VERSION__ == 201710L puts("Support ISO/IEC 9899:2018 standard."); #else puts("Support other ISO standards."); #endif
④ 如果没有条件评估结果为真,并且存在#else指令,则#else指令控制的代码组将被处理;如果不存在#else指令,所有代码组都被跳过直至#endif指令。
//假设__STDC_VERSION__ == 202311L为假, //并且__STDC_VERSION__ == 201710L为假, //以下绿色背景代码会被跳过。 #if __STDC_VERSION__ == 202311L puts("Support ISO/IEC 9899:2024 standard."); #elif __STDC_VERSION__ == 201710L puts("Support ISO/IEC 9899:2018 standard."); #else puts("Support other ISO standards."); #endif
#else指令或者#endif指令后,换行符前不能存在任何预处理标记,但可以存在注释;也就是说#else指令和#endif指令应单独占一行,后面最多只能跟注释。
//以下两个#endif指令合法。 #endif ... #endif // 0