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

文章目录

  • 一些表达式
    • 常量表达式
    • 泛型选择表达式
    • 静态断言
  • 左值右值
    • 左值
    • 右值
  • 求值顺序
    • 顺序点:
    • 几种执行顺序:

一些表达式

常量表达式

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

  • 常量表达式不应该含有赋值,自增自减,函数调用,以及逗号操作符。(除非该表达式包含在不被计算的子表达式中,比如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(赋值表达式, 泛型关联列表)

  1. 赋值表达式:可以出现在赋值操作符(如=*=+=/=等)右边的表达式。
  2. 泛型关联列表:类似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语言的左值做了明确的定义:

  1. 一个左值表达式能隐式地用来表示一个对象;
  2. 如果一个左值在计算时无法用来表示一个对象, 那么行为是未定义的。
  3. 一个可修改的左值不能是一个数组类型, 不能是一个不完整类型, 不能有const 限定符修饰; 并且如果该左值是一个结构体或联合体类型的话, 其任一成员也不能有const 限定符修饰。
  4. 当一个左值作为&++--的操作数, 或成员访问操作符.=的左操作数时, 整个表达式就不具备左值特性了, 这在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语言规范将决定到底哪个顺序点应该先执行,那个后执行。

  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

你可能感兴趣的:(#,C语言,c++,算法,数据结构)