C语言中的常量表达式
常量表达式包括命名常量(named constant)、复合字面常量(compound literal constant)、算术常量表达式(arithmetic constant expression)、空指针常量(null pointer constant)、地址常量(address constant)、完整对象类型的地址常量加减整数常量表达式。
常量表达式(constant expressions)可以在任何可以使用常量的地方使用。常量表达式应评估为其类型可表示值域范围内的常量。
3.14f //合法的常量表达式。 -3U //非法的常量表达式。
常量表达式不能包含赋值运算符、自增运算符、自减运算符、函数调用运算符或者逗号运算符,除非这些运算符包含在不被评估的子表达式中(typeof、sizeof和alignof运算符的操作数通常不被评估。)。
2 || (3, 5) //是常量表达式,逗号表达式不会被评估。 0 && (4, 6) //是常量表达式,逗号表达式不会被评估。 0 || (3, 5) //不是常量表达式,逗号表达式会被评估。 2 && (4, 6) //不是常量表达式,逗号表达式会被评估。
一、命名常量
如果一个标识符是枚举常量、预定义常量或者声明时使用了存储类说明符constexpr的对象标识符,那么该标识符是命名常量,后缀表达式使用成员访问运算符(.)访问结构或者联合类型的命名常量得到的也是命名常量,这种关系可以递归应用。
struct s { int i; double d; }; enum color {black, white}; //black, white是命名常量。 constexpr unsigned week = 7; //week是命名常量。 constexpr struct s ss = {5, 3.14}; //ss、ss.i和ss.d是命名常量。
结构常量或者联合常量是具有结构类型或者联合类型的命名常量或者复合字面常量。对于结构常量或者联合常量,使用成员访问运算符(.)可生成命名常量或者复合字面常量。如果使用成员访问运算符(.)访问联合常量成员,被访问成员应与联合常量初始化器初始化的成员相同。
对于枚举常量,其值和类型遵守枚举常量的相关规则。对于constexpr对象,其值和类型与声明对象相同。
预定义常量是ISO/IEC 9899:2024标准新增内容,包括false、true、nullptr。false和true是bool类型常量,值分别是0和1;nullptr是空指针常量。
(注:在ISO/IEC 9899:2018标准中,false和true是<stdbool.h>头文件中定义的宏。)
二、复合字面常量
存在存储类说明符constexpr的复合字面量是复合字面常量;后缀表达式使用成员访问运算符(.)访问结构或者联合类型的复合字面常量得到的也是复合字面常量,这种关系可以递归应用。
struct s{ int i; double d; }; ... (constexpr struct s){5, 3.14}; //复合字面常量。 (constexpr struct s){2, 1.23}.d; //复合字面常量。
复合字面常量是常量表达式,具有未命名对象的类型和值。
三、算术常量表达式
算术常量表达式应具有算术类型,并且操作数只能是整数常量、浮点常量、算术类型的命名常量、算术类型的复合字面常量、字符常量、结果为整数常量的sizeof表达式或者alignof表达式。算术常量表达式中的强制类型转换运算符只能将算术类型转换为算术类型,但作为typeof运算符、sizeof运算符、alignof运算符操作数的一部分除外。
1、整数常量表达式
整数常量表达式应具有整数类型,并且操作数只能是整数常量、整数类型的命名常量、整数类型的复合字面常量、字符常量、结果为整数常量的sizeof表达式、alignof表达式、以及作为强制类型转换直接操作数的浮点常量、命名常量或者具有算术类型的复合字面常量,例如:-5、3U、'A'、sizeof(int)、alignof(double)、(int)3.14。
整数常量表达式中的强制类型转换运算符只能将算术类型转换为整数类型,但作为typeof运算符、sizeof运算符、alignof运算符操作数的一部分除外。
以下情况要求表达式是整数常量表达式:
-- 结构中位字段成员大小。
-- 枚举常量值。
-- 非变长数组大小。
-- switch语句中的case标签。
-- 整数类型到指针类型的隐式类型转换,例如:int *ptr = 0;,右操作数应为值为0的整数常量表达式。
-- 数组下标值。
-- 函数式宏alignas的整数实参。
struct s{ int i; alignas(16) double d; };
(注:在ISO/IEC 9899:2024标准中alignas是C语言关键词;在ISO/IEC 9899:2018标准中alignas是<stdalign.h>头文件中定义的函数式宏。)
-- 函数式宏static_assert的第一个实参。
static_assert(sizeof(int) == 4, "Bytes of integer type is not equal to 4.");
-- 预处理指令中控制条件包含的表达式。
#define STATE 0 #if defined STATE //表达式defined STATE的值为1。
-- 位精确整数类型的位数,例如:_BitInt(N)。
2、浮点常量表达式
浮点表达式在编译环境中评估时,其算术范围和精度应不小于执行环境中评估时的范围和精度。由宏FLT_EVAL_METHOD和宏DEC_EVAL_METHOD确定的评估格式同样适用于编译环境中浮点表达式的评估。
| 宏FLT_EVAL_METHOD值 | 评估格式 |
| -1 | 评估格式不明确。 |
| 0 | 根据类型的范围和精度评估所有操作和常量。 |
| 1 | 根据double类型的范围和精度评估所有float类型和double类型的操作和常量;根据long double类型的范围和精度评估所有long double类型的操作和常量。 |
| 2 | 根据long double类型的范围和精度评估所有操作和常量。 |
| 宏DEC_EVAL_METHOD值 | 评估格式 |
| -1 | 评估格式不明确。 |
| 0 | 根据类型的范围和精度评估所有操作和常量。 |
| 1 | 根据_Decimal64类型的范围和精度评估所有_Decimal32类型和_Decimal64类型的操作和常量;根据_Decimal128类型的范围和精度评估所有_Decimal128类型的操作和常量。 |
| 2 | 根据_Decimal128类型的范围和精度评估所有操作和常量。 |
四、空指针常量
转换为void *类型的、值为0的整数常量表达式或者预定义常量nullptr称为空指针常量。宏NULL也被定义为空指针常量。
五、地址常量
地址常量可以是空指针,指向表示静态存储期限对象的左值的指针或者指向函数指示符的指针。地址常量应使用地址运算符(&)或者整数常量强制转换成指针类型显式创建,或者使用数组类型或者函数类型的表达式隐式创建。
int func(int); //func是地址常量。 static int i; //&i是地址常量。 int *ptr = NULL; //ptr是地址常量。
值为0的整数类型命名常量或者复合字面常量是空指针常量。值为空的指针类型命名常量或者复合字面常量是空指针,但不是空指针常量;只有在其类型隐式地转换为目标类型时,才能用于初始化指针对象。
// ptr是空指针。 // nullptr是空指针常量。 int *ptr = nullptr;
算术类型的命名常量或者复合字面常量(包括constexpr对象名。),只要它们出现的表达式构成整数常量表达式,在偏移计算(例如:数组下标)或者指针的强制类型转换中都是有效的;相反其它对象名(甚至包括使用const类型限定符限定的、具有静态存储期限的对象名。)都是无效的。
六、完整对象类型的地址常量加上或者减去一个整数常量表达式
static int i; int *ptr = &i + 1; //表达式&i + 1是常量表达式。
实现可能接受其它形式的常量表达式,称为扩展常量表达式(extended constant expressions)。扩展常量表达式是否可以与ISO标准定义的常量表达式以同样方式使用(包括扩展整数常量表达式是否视为整数常量表达式。)将由实现定义。
常量表达式评估的语义规则与非常量表达式评估的语义规则相同。
static int i = (0 && (4, 6)); //表达式(0 && (4, 6))是值为0的整数常量表达式。
大多数情况下常量表达式的值在编译时确定;但地址常量值及其派生值可能在链接或者程序启动时确定。具有静态存储期限的对象在程序启动前初始化。存在constexpr存储类说明符的对象的初始化器在程序编译时评估。
主要参考资料:
3、cppreference.com : Constant expressions
4、devgem.io : Understanding Address Constants and Static Initializers in C
5、open-std.org : Chasing Ghosts I: constant expressions