6.5.3 后缀运算符
TIP
这一节包含数组下标、函数调用、成员访问、后置自增自减和复合字面量。它们都属于高优先级表达式,但语义来源各不相同,不要把“后缀运算符”当成单一机制。
6.5.3.1 一般规定
- 后缀表达式包括:
- 基本表达式;
- 下标表达式
E1[E2]; - 函数调用
E(...); - 成员访问
E.id; - 指针成员访问
E->id; - 后置
++; - 后置
--; - 复合字面量。
6.5.3.2 数组下标
两个操作数中,一个必须是“指向完整对象类型的指针”,另一个必须是整数类型。
E1[E2]的定义等同于:
(*((E1) + (E2)))因而,只要一边是数组对象(或指向其首元素的指针),另一边是整数,结果就是该数组从
0开始计数的第E2个元素。多维数组的连续下标,按行主序解释。也就是说,最后一个下标变化最快。
标准示例的直观解释
int x[3][5];这里 x 是“3 个元素的数组”,每个元素又是“5 个 int 的数组”。因此:
x[i]得到第i个“长度为 5 的数组”x[i][j]才得到其中第j个int
IMPORTANT
a[b] 和 b[a] 在语义上是等价的,因为二者都只是 *((a) + (b))。只是前者符合人的阅读习惯。
6.5.3.3 函数调用
被调用表达式必须具有“指向函数的指针”类型,该函数返回
void或某种完整对象类型(但不能返回数组类型)。实参数量必须与形参数量一致。每个实参的类型,都应当允许其值赋给对应形参类型去限定后的对象。
后缀表达式后跟圆括号和可能为空的实参列表,就是函数调用表达式。前面的后缀表达式表示被调用函数。
实参可以是任意完整对象类型表达式。调用前会先对实参求值,然后把对应的值赋给形参。
若通过函数指针表达式调用,而该表达式所指函数类型与函数定义实际类型不兼容,则行为未定义。
在函数设计符和全部实参求值完成之后、真正进入被调函数之前,存在一个序列点。
递归调用必须被允许,无论是直接递归还是间接递归。
标准示例
(*pf[f1()]) (f2(), f3() + f4())在这个调用中,f1、f2、f3、f4 的调用顺序都可以不同;但它们的全部副作用,必须在最终调用 pf[f1()] 所指向函数之前完成。
6.5.3.4 结构体和联合体成员
.运算符左操作数必须是结构体或联合体类型,右操作数必须是该类型中的成员名。->运算符左操作数必须是“指向结构体或联合体的指针”,右操作数必须是所指类型中的成员名。E.id指代结构体或联合体对象中的某个成员。若E是左值,则结果也是左值。E->id指代E所指对象中的某个成员,结果是左值。若通过原子结构体或原子联合体对象访问成员,则行为未定义。
联合体有一个专门保证:如果它包含若干具有公共初始序列的结构体,并且当前存放的是其中某个结构体,那么只要该联合体完整类型在当前可见,就允许检查这些结构体共同的初始部分。
NOTE
-> 不是独立的“魔法访问”。它本质上就是“先解引用,再做成员访问”的语义捷径。
6.5.3.5 后置自增和后置自减
后置
++或--的操作数必须具有原子的、限定的或无限定的实数类型或指针类型,并且必须是可修改左值。后置
E++的结果是修改前的旧值;副作用则是在之后把对象值加一。结果值计算先于对象新值写回这一副作用。
对原子类型对象使用后置
++时,它是一个具有memory_order_seq_cst语义的读改写操作。后置
--与之类似,只是执行减一。
6.5.3.6 复合字面量
- 复合字面量的形式是:
( type-name ){ initializer-list }也可以带适用的存储类说明符。
其类型名必须指定完整对象类型,或者未知大小数组类型;但不能是变长数组类型。
复合字面量提供对一个无名对象的访问。这个无名对象的类型、初始化方式、存储期等,都仿佛你在对应作用域中写下了一个普通对象定义。
若它关联的是函数原型作用域,则不会创建对象;若它是复合字面量常量,会在翻译期求值,否则整体和内部初始化器都不求值。
若存储期是自动存储期,则该无名对象的生命周期就是外围块当前这次执行。
复合字面量表达式本身的值,是对应无名对象的左值。
字符串字面量以及
const限定的复合字面量,不要求一定对应不同对象;实现可以共享存储。
典型例子
int *p = (int[]){2, 4};这会让 p 指向一个无名 int 数组的首元素。
drawline((struct point){.x = 1, .y = 1},
(struct point){.x = 3, .y = 4});2
这说明复合字面量很适合临时构造结构体实参。