常量表达式在编译期间即可被记得算出来,不会生成相应的运行时代码。
常量表达式不应该含有赋值,自增自减,函数调用,以及逗号操作符。(除非该表达式包含在不被计算的子表达式中,比如sizeof()、_Alignof()里的表达式)
如果一个表达式能够被赋值到全局静态变量中则其为常量表达式。
#include
#include
int fun(int);
static int a = 100 + 10;
static int g = a;
/* error: expression must have a constant value */
static int b = fun(1);
/* error: function call is not allowed in a constant expression */
static const int c = (sizeof(a) / 2 + 2);
static int d = c;
static int e = (sizeof(a) > sizeof(b) ? 2 : 1);
int* p = &e;
int main() {
printf("a = %d, b = %d, c = %d, d = %d", a, b, c, d);
return 0;
}
int fun(int i) {
return i ++;
}
上述第12行
static int d = c;
并不是在所有编译器里都允许(比如visual studio的默认编译器MSVC)
GCC与Clang支持将一个被const修饰的常量作为一个常量表达式。MSVC不允许。
C11新引入的语法规则
_Generic(赋值表达式, 泛型关联列表)
- 赋值表达式:可以出现在赋值操作符(如
=
、*=
、+=
、/=
等)右边的表达式。- 泛型关联列表:类似
swich case
的结构(类型名:赋值表达式
,支持使用default
)
很多人将C语言与C++相比较,都会指出C语言在面向对象和模板这两方面的欠缺。
但是C11的泛型选择表达式可以稍微弥补一下C语言在模板方面的不足。
_Generic()
可根据赋值表达式的类型,从泛型关联列表中返回相应的值。
可以借此实现一种C++中template
类模板的感觉。
#include
typedef struct {
int i;
int j;
}atype;
typedef struct {
int i;
int j;
}btype;
int main() {
const char* p = "none";
int a = _Generic(100, float:1.1, int:2, default:0);
printf("%d \n", a); // 2
p = _Generic((a++, a + 1.5f), \
int : "int", \
float : "float", \
default : "none");
printf("%s \n", p); // float
p = _Generic("abc", \
const char* : "const char *", \
char* : "char*", \
const char[4] : "const char array", \
char[4] : "char array");
printf("%s \n", p); // char*
struct point {int x, y;};
struct size {int w, h;};
struct point po = {};
struct size si = {};
p = _Generic(si, \
struct point : "point", \
struct size : "size", \
default : "none");
printf("%s \n", p); // size
atype st = {};
p = _Generic(st, \
atype : "atype", \
btype : "btype", \
default : "none");
printf("%s \n", p); // atype
return 0;
}
这里如果你查看一下编译出来的汇编代码可以发现,这些类型选择的操作都是在编译阶段由编译器完成的。
也是C11新引入的语法规则
_Static_assert(整数常量表达式, 字符串字面量)
或者static_assert(整数常量表达式, 字符串字面量)
它的作用是,如果常量表达式的值为false
(或者0、空指针)那么断言失败,程序在编译的时候输出一段诊断信息,该诊断信息就是字符串字面量。
#include
#include
int i = 10;
int main() {
static_assert(true, "this is true! \n");
static_assert(false, "this is false! \n");
// error: static assertion failed with "this is false!
int* pointer = NULL;
static_assert(pointer, "this is false \n!");
// error: expression must have a constant value
static_assert(&i, "this is true! \n");
// error: this operator is not allowed in an integral constant expression
static_assert(sizeof(i), "this is true! \n");
return 0;
}
这个也是在编译的时候就已经确定。
不同于C++的移动语义、右值引用、将亡值、纯右值等等,C的左值右值机制比较简单。
一般来说左值就是赋值操作符左侧的数。
C11标准给C语言的左值做了明确的定义:
&
、++
、--
的操作数, 或成员访问操作符.
和=
的左操作数时, 整个表达式就不具备左值特性了, 这在C语言中称为左值转换。或者说从内存的角度来看,左值是存储在内存中、能在内存中定位(located)的值。
#include
int main() {
int a = 10;
int* p;
a += 5; // 这里a就是个左值
p = &a; // 这里p就是个左值
a++ = 0; // 这里的(a++)就是右值了,会报错
int array[5] = {1,2,3};
array = (int[]) {4,5,6}; // 这里array不是左值
array[3] ++;
main = NULL; // 好吧main也不是左值
int (*pfun)(int, const char**);
pfun = $main; //pfun是左值,可以被赋值
(a = 10) = 100; // (a = 10)不是左值
return 0;
}
C语言中的右值很好判断,一般来说只要不是左值,那他就是右值。
有一种很特殊的右值,叫复合字面量(compound literal)。C语言中我们把匿名结构体、匿名联合体、匿名数组统称为复合字面量。
int main() {
int(*p)[3] = &(int[]){1, 2, 3};
(int[]){1, 2, 3}[2] ++;
struct test {int a, b};
(struct test){10, 20}.a ++;
return 0;
}
上面这些看似离谱的操作实际上都是可行的,只不过像第三行的这些值会被丢弃在内存中无法访问。
C语言中有四种次序关系:执行在前的次序关系(sequenced before),执行在后的次序关系(sequenced after),无执行先后次序的次序关系(unsequenced),不确定的次序关系(indeterminately sequenced)。
在执行到该时间点之前的所有程序都已发挥完作用,之后的程序还未发挥作用。
执行顺序就是编译器对顺序点的处理,编译器和C语言规范将决定到底哪个顺序点应该先执行,那个后执行。
- 在一个函数调用中,函数指派符(可以想成是函数名)和实参的计算与函数实实际调用之间。
- 以下操作符的操作数的计算之间:逻辑与(
&&
)逻辑或(||
)逗号(,
)条件操作符(? :
)。- 在一条完整声明符的结尾。
- 在对一条完整表达式的计算,与下一条要被计算的完整表达式之间。
- 紧放在一个库函数返回之前。
- 与每个格式化的输入输出函数转换说明符(比如
%d %s
)相关的行为(进行输入输出)之后。
执行在前的次序关系(sequenced before)
执行在后的次序关系(sequenced after)
这个很好理解,一般的表达式都存在这个问题,大部分由运算符就优先级决定。
a = b + array[2];
c = a++ + b++
这里的++
、+
、=
、[]
都有明显的先后次序。
两个表达式的计算可以交错甚至并行执行,没有执行的先后次序。
尤其在支持指令级并行的芯片上,这种情况就会发生
mov r1, #1
mov r2, #1
mov r3, #1
mov r4, #1
mov r5, #1
mov r6, #1
mov r7, #1
mov r8, #1
比如这样的操作在stm32里就会明显看出指令级并行的痕迹。
两个表达式,有明显的先后顺序不能交错、并行,但是其先后顺序并不确定,可能会由具体编译器决定。
比如:
#incldue <stdio.h>
int fun1(int a, int b) {
printf("fun1 is called \n");
return a + b;
}
int fun2(int a, int b) {
printf("fun2 is called \n");
return a - b;
}
int main() {
int a = 0;
a = (a++) + (a++);
/* a是先被赋值,还是先被自增 */
fun1(a++, a--);
/* a先自减,还是先自增 */
fun1(fun1(1,2), fun2(2,1));
/* fun1和fun2谁先呗调用 */
return 0;
}
------ By Flier
2024.2.11