Gwok HiujinGwok Hiujin

The Bird of Hermes is my name, eating my wings to make me tame.

Oct 19, 2022C语言系统级编程1428 words in 7 min


『C系统级编程』C语言左值与表达式


表达式

在 C 语言中,由一系列操作符操作对象组成的一个序列被称之为表达式(Exp)。它们具有如下的分类:

  • 基础表达式
    • 标识符(Identifier):变量名或函数名
    • 常量(Constant)
    • 字符串
    • 括号:也就是说,(Exp) == Exp
    • 泛型选择表达式
      • 在程序设计中,泛型编程(generic programming)指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。C11 新增了泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是 int、double 还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作 #define 宏定义的一部分。
      • 示例:_Generic(x, int: 0, float: 1, double: 2, default: 3)
  • 后缀表达式:给定一个表达式 exp,其与后缀操作符结合构成的仍然是一个表达式
    • exp1[exp2]
    • exp.identifier
    • exp→identifier
    • exp++ / exp–
    • (TypeName){Initializer-list}
      • (int){2}, {int[3]}{1, 2, 3}
    • exp(argument list)
      • func(1, 2)
  • 一元表达式:给定一个表达式 exp,其与一元操作符结构构成的仍然是一个表达式
    • ++exp / --exp
    • &exp
    • *exp
    • +exp / -exp
    • ~exp / !exp
    • sieof(exp) / sizeof exp / _Alignof(exp)
  • Cast表达式
    • (TypeName)exp
      • (float)a
  • 条件表达式:两个表达式 + 条件操作符结构
    • >, ≥, <, ≤, ==, ≠
  • 运算表达式:两个表达式 + 运算操作符结构(包括位运算操作符、逻辑运算操作符)
    • +, -, *, /, %
    • <<, >>, &, ^, |
    • &&, ||, exp1 ? exp2 : exp3
  • 赋值表达式:两个表达式 + 赋值运算操作符
    • =、*=、/=、%=、+=、-=、<<=、 >>=、&=、^=、|=
  • 逗号运算表达式
    • exp1, exp2, …, exp_n
      • 从左至右按将逗号分隔的表达式依次运算,整个表达式返回值和类型是最后一个表达式的返回值和类型

当然,多个表达式也可以组合成新的表达式。做分解的时候,我们总是从基础表达式开始分解。

p9yylTI.jpg

左值(lvalue)

左值是一个表达式,且这个表达式能够定位一个对象

p9yy3kt.jpg

(只有这些可以)

因此就像之前讲过的那样,左值并不是 left value 而是 locator value。之所以叫左值是因为大多数情况下它都在等号(赋值语句)左边,但只有 Modifiable lvalue 才能被放到等号左边。Modifiable lvalue 的条件是,如果一个 lvalue 定位到的对象内存满足以下条件:

  1. 变量类型不是数组变量类型
  2. 变量类型不是不完全类型
  3. 没有被 const 修饰
  4. 如果该对象类型是 struct / union,则成员的变量类型也没有被 const 修饰

则说明该 lvalue 是一个 Modifiable lvalue,可以放到等号左边。能被放到等号左边的含义是能够被赋值。

观察 lvalue

如果一个左值表达式在赋值表达式的等号左边:

  1. 若该左值是 Modifiable 的,则等待赋值;
  2. 若该左值是非 Modifiable 的,则编译出错。

其他情况下,lvalue 表达式的观察方法如下:假设该左值定位的对象为 Obj(对应一个内存六元组,下面要用),则:

  1. 如果 lvalue 跟 & 结合,则返回值:<Address, Obj_T*>
  2. 如果 lvalue 跟 sizeof 结合,则返回值:<Size, size_t>
  3. 其他情况,返回值为:<Value, Value_Type>
再谈 *exp 形式

之前在定位内存的部分讲过 * 运算符如何定位内存,这里稍作补充。

p9yyQ0A.jpg

  • *p 的时候并不知道这块内存的 Name,只要直接定位以该编号开头的内存就好了。
  • 只有指针类型的值能用 * 操作符进行间接定位。
  • 指针变量 p + n
    • p + n 的 Value = p.value + sizeof(*p)*n
    • p + n 的 Value_Type 不变,还是 p.Value_Type
    • 注意这个 n 可以是数字常量,也可以是一个返回值为合法的整数类型的表达式
  • *(exp + n) <==> exp[n]
    • 要求 exp 的返回值类型是有效的指针类型
    • 可见:任何一个返回值为指针的表达式,蕴含着指向的那块内存为一个数组,元素为指针变量类型对应的变量类型,但大小未知
      • 这意味着可能会发生溢出
    • 注意这个 n 可以是数字常量,也可以是一个返回值为合法的整数类型的表达式
    • ⭐ 显然,通过这个规则可以递归地完成数组元素的定位(即将数组元素转换为通过 * 运算符定位内存的过程)
      • 这里放上这节课的课堂小测题,熟练了这条规则之后这些题就很容易了。p9yy8tP.jpg
    • 趣闻
      • 当 n 为返回值为合法整数类型的表达式时,我们可以将这个形式化定义式子写成 E1[E2] <==> *((E1) + (E2)),我们并不会对 E1、E2 的对应次序做出要求,此时会产生一个很有意思的现象:a[1] <==> 1[a]!这是可以正确通过编译的!
        • 实际操作的时候不要这么写,会被打 O(∩_∩)O
  • 两个指针相减
    • p9yyMmd.jpg

表达式的值

表达式的值可以形式化定义为: <V, V_T> 。

若观察到表达式为非数组变量类型,那么就可以按照观察 lvalue 的规则得到返回值;

若观察到表达式为数组类型,则返回值是数组第一个元素的首地址,返回值类型是元素变量类型对应的指针类型。

Buy me a cup of coffee ☕.