表达式
在 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
- (TypeName)exp
- 条件表达式:两个表达式 + 条件操作符结构
- >, ≥, <, ≤, ==, ≠
- 运算表达式:两个表达式 + 运算操作符结构(包括位运算操作符、逻辑运算操作符)
- +, -, *, /, %
- <<, >>, &, ^, |
- &&, ||, exp1 ? exp2 : exp3
- 赋值表达式:两个表达式 + 赋值运算操作符
- =、*=、/=、%=、+=、-=、<<=、 >>=、&=、^=、|=
- 逗号运算表达式
- exp1, exp2, …, exp_n
- 从左至右按将逗号分隔的表达式依次运算,整个表达式返回值和类型是最后一个表达式的返回值和类型
- exp1, exp2, …, exp_n
当然,多个表达式也可以组合成新的表达式。做分解的时候,我们总是从基础表达式开始分解。
左值(lvalue)
左值是一个表达式,且这个表达式能够定位一个对象:
(只有这些可以)
因此就像之前讲过的那样,左值并不是 left value 而是 locator value。之所以叫左值是因为大多数情况下它都在等号(赋值语句)左边,但只有 Modifiable lvalue 才能被放到等号左边。Modifiable lvalue 的条件是,如果一个 lvalue 定位到的对象内存满足以下条件:
- 变量类型不是数组变量类型
- 变量类型不是不完全类型
- 没有被 const 修饰
- 如果该对象类型是 struct / union,则成员的变量类型也没有被 const 修饰
则说明该 lvalue 是一个 Modifiable lvalue,可以放到等号左边。能被放到等号左边的含义是能够被赋值。
观察 lvalue
如果一个左值表达式在赋值表达式的等号左边:
- 若该左值是 Modifiable 的,则等待赋值;
- 若该左值是非 Modifiable 的,则编译出错。
其他情况下,lvalue 表达式的观察方法如下:假设该左值定位的对象为 Obj(对应一个内存六元组,下面要用),则:
- 如果 lvalue 跟 & 结合,则返回值:<Address, Obj_T*>
- 如果 lvalue 跟 sizeof 结合,则返回值:<Size, size_t>
- 其他情况,返回值为:<Value, Value_Type>
再谈 *exp 形式
之前在定位内存的部分讲过 * 运算符如何定位内存,这里稍作补充。
- *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 可以是数字常量,也可以是一个返回值为合法的整数类型的表达式
- ⭐ 显然,通过这个规则可以递归地完成数组元素的定位(即将数组元素转换为通过 * 运算符定位内存的过程)
- 这里放上这节课的课堂小测题,熟练了这条规则之后这些题就很容易了。
- 趣闻
- 当 n 为返回值为合法整数类型的表达式时,我们可以将这个形式化定义式子写成 E1[E2] <==> *((E1) + (E2)),我们并不会对 E1、E2 的对应次序做出要求,此时会产生一个很有意思的现象:a[1] <==> 1[a]!这是可以正确通过编译的!
- 实际操作的时候不要这么写,会被打 O(∩_∩)O
- 当 n 为返回值为合法整数类型的表达式时,我们可以将这个形式化定义式子写成 E1[E2] <==> *((E1) + (E2)),我们并不会对 E1、E2 的对应次序做出要求,此时会产生一个很有意思的现象:a[1] <==> 1[a]!这是可以正确通过编译的!
- 两个指针相减
表达式的值
表达式的值可以形式化定义为: <V, V_T> 。
若观察到表达式为非数组变量类型,那么就可以按照观察 lvalue 的规则得到返回值;
若观察到表达式为数组类型,则返回值是数组第一个元素的首地址,返回值类型是元素变量类型对应的指针类型。