表达式、左值右值、执行顺序

一些表达式

常量表达式

  • 常量表达式在编译期间即可被记得算出来,不会生成相应的运行时代码。

  • 常量表达式不应该含有赋值,自增自减,函数调用,以及逗号操作符。(除非该表达式包含在不被计算的子表达式中,比如sizeof()、_Alignof()里的表达式)

如果一个表达式能够被赋值到全局静态变量中则其为常量表达式。

#include <stdio.h>
#include <stdint.h>

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(赋值表达式, 泛型关联列表)

  1. 赋值表达式:可以出现在赋值操作符(如=*=+=/=等)右边的表达式。
  2. 泛型关联列表:类似swich case的结构(类型名:赋值表达式,支持使用default

很多人将C语言与C++相比较,都会指出C语言在面向对象和模板这两方面的欠缺。

但是C11的泛型选择表达式可以稍微弥补一下C语言在模板方面的不足。

_Generic()可根据赋值表达式的类型,从泛型关联列表中返回相应的值。

可以借此实现一种C++中template类模板的感觉。

#include <stdio.h>

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 <assert.h>
#include <stdbool.h>

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语言的左值做了明确的定义:

  1. 一个左值表达式能隐式地用来表示一个对象;
  2. 如果一个左值在计算时无法用来表示一个对象, 那么行为是未定义的。
  3. 一个可修改的左值不能是一个数组类型, 不能是一个不完整类型, 不能有const 限定符修饰; 并且如果该左值是一个结构体或联合体类型的话, 其任一成员也不能有const 限定符修饰。
  4. 当一个左值作为&++--的操作数, 或成员访问操作符.=的左操作数时, 整个表达式就不具备左值特性了, 这在C语言中称为左值转换。

或者说从内存的角度来看,左值是存储在内存中、能在内存中定位(located)的值。

#include <stdio.h>

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)。

顺序点:

At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.

在执行到该时间点之前的所有程序的副作用都已发挥完,之后的程序还未发挥作用。

执行顺序就是编译器对顺序点的处理,编译器和C语言规范将决定到底哪个顺序点应该先执行,那个后执行(这就是执行顺序)。

  1. 在一个函数调用中,函数指派符(可以想成是函数名)和实参的计算与函数实实际调用之间。
  2. 以下操作符的操作数的计算之间:逻辑与(&&)逻辑或(||)逗号(,)条件操作符(? :)。
  3. 在一条完整声明符的结尾。
  4. 在对一条完整表达式的计算,与下一条要被计算的完整表达式之间。
  5. 紧放在一个库函数返回之前。
  6. 与每个格式化的输入输出函数转换说明符(比如%d %s)相关的行为(进行输入输出)之后。

几种执行顺序:

  1. 执行在前的次序关系(sequenced before)

  2. 执行在后的次序关系(sequenced after)

这两个很好理解,一般的表达式都存在这个问题,大部分由运算符就优先级决定。

a = b + array[2];
c = a++ + b++

这里的+++=[]都有明显的先后次序。

  1. 无执行先后次序的次序关系(unsequenced)

两个表达式的计算可以交错甚至并行执行,没有执行的先后次序。

尤其在支持指令级并行的芯片上,这种情况就会发生

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里就会明显看出指令级并行的痕迹。

  1. 不确定的次序关系(indeterminately sequenced)

两个表达式,有明显的先后顺序不能交错、并行,但是其先后顺序并不确定,可能会由具体编译器决定。

比如:

#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

2.1 在函数调用之前在函数调用时,未指定实参和实参中的子表达式的求值顺序,但在函数调用之前有一个序列点。例:函数调用func(a++,a);两个实参的执行顺序未指定,由编译器做主,这就有可能先计算a++,后计算a,或者相反。在计算实参时,有个副作用,就是变量a自增1,该副作用会在实际调用函数之前完成,具体在何处完成,就看编译器如何处理。不管编译器如何处理副作用它都遵守了C标准,但在计算函数实参中存在未定义行为。因为,实参的求值未指定顺序,如果将实参a++的值记为Va++,它的副作用记为Sa++,因此处的++是后缀运行符,所以要求副作用Sa++在Va++之后发生;将参数a的值记为Va,实际的函数调用记为Cf,则整个表达式的求值顺序可能是(假定变量a的初始值为1)Va++(1)→Va(1)→Sa++(2)→Cf   也可能是Va++(1)→Sa++(2)→Va(2)→Cf按第一个求值顺序计算,两个实参值分别是1,1。按第二个求值顺序计算,两个实参分别是1,2。还会有其他的执行顺序,至于选择哪一种顺序处理,取决于C的实现,只要保证Cf发生在Va++、Sa++、Va之后、Sa++发生在Va++之后即可。很显然,实参求值顺序不同,传递给函数func的实参也不同,这可能会影响到函数的返回值,即整个表达式的值,这就说明上述实参的计算中存在未定义行为。2.2 在下列运算符的第一个操作对象处理结束后序列点在下列运算符的第一个操作对象处理结束后:
逻辑与运算符—— &&逻辑或运算符—— ||条件运算符 ?:中的——?逗号运算符——,当在表达式中遇到这4个运算符时,处理完它们的第一个操作数即是序列点。比如&&,当第一个操作对象的值是0时,就不需要处理第二个操作对象,逻辑或||,当第一个操作对象的值是非0时,就不需要处理第二个操作对象等等。2.3 在完整声明符的末尾完整声明符(full declarator)首先是一个声明符,但它自身是独立和完整的,不是其他声明符的组成部分。例如:int a=3, b=a++, c=++b+1, y[++c];在初始化对象b时,对象a的初始化已经完成,且a++的副作用也已经完成;在初始化对象c时,对象b的初始化已经完成,且++b的副作用也已完成;在初始化对象y时,对象c的初始化已经完成,且++c的副作用也已完成。分析下列程序的运行结果。#include <stdio.h>int main(void) { int a=3, b=a++, c=++b+1, y[++c]; printf(“%d %d %d %d\n”,a,b,c, sizeof y); return 0; }2.4 在完整表达式末尾有序列点(1)初始化变量末尾,例
int z[4][3] = { { 1 }, { 2 }, { 3 }, { 4 } };(2)表达式语句中的表达式(3)选择语句(if或switch)的控制表达式(4)while或do语句的控制表达式(5)for语句的每个表达式(6)return语句中的表达式2.5 在库函数返回之前有序列点2.6 在与每个格式化的输入/输出函数转换说明符关联的操作之后有序列点。2.7 在查找和排序等函数中,每次调用比较函数之前和之后,以及在调用比较函数和作为参数传递给该调用的对象的任何移动之间有序列点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值