ISO/IEC 9899:2011 条款6.5.2——后缀操作符
6.5.2 后缀操作符
语法
1、postfix-expression:
primary-expression
postfix-expression [ expression ]
postfix-expression ( argument-expression-listopt )
postfix-expression . identifier
postfix-expression -> identifier
postfix-expression ++
postfix-expression --
( type-name ) { initializer-list }
( type-name ) { initializer-list , }
argument-expression-list:
assignment-expression
argument-expression-list , assignment-expression
6.5.2.1 数组下标
约束
1、[译者注:(关于数组下标的表达式)]其中一个表达式应该具有“指向完整对象类型type的指针”类型,而另一个表达式应该具有整数类型,并且结果具有“type”的类型。
语义
2、后面跟着中括号 [ ] 的一个后缀表达式是对一个数组对象的一个元素的下标指定。下标操作符 [ ] 的定义为 E1 [E2] 等价为 (* ( (E1) + (E2) ) )。因为应用于双目操作符 + 的转换规则,所以如果E1是一个数组对象(等价于指向一个数组对象起始元素的一个指针)并且E2是一个整数,那么 E1[E2] 表示为 E1 的 第 E2 个元素(计数从零开始)。
3、连续的下标操作符指派了一个多维数组对象的一个元素。如果 E 是一个n维数组(n ≧ 2),维度为i × j × ... × k,那么 E (用作为左值意外的值)被转换为一个指向(n - 1)维数组的一个指针,其维度为j × ... × k。如果单目 * 操作符被显式地应用于这个指针,或者作为下标的一个结果而被隐式地应用,那么结果被引用为(n - 1)维数组,其自身被转换为一个指针,如果用作为一个左值之外的其它值。数组以行为主的次序被存放,这个协定要遵从。[译者注:这个协定意味着,像int x[N][M]; 表示x有N个int[M]类型的元素。]
4、例 考虑以下定义的数组对象
int x[3][5];
这里,x是int类型的一个3 × 5数组;更精确地说,x是三个元素对象的一个数组,每个元素是五个int类型的一个数组。在表达式 x[i] 中,这等价于 (*((x) + (i))),x首先被转换为五个int类型的初始数组。然后,i根据x的类型进行调整,这在概念上表示为i乘以指针所指向的对象的大小,命名为五个int对象的一个数组。结果然后被相加,并且应用间接操作来产生一个五个int对象的一个数组。当以表达式 x[i][j] 来使用时,该数组再依次被转换为指向int数组的起始元素的指针,这样,x[i][j] 产生一个int对象。
6.5.2.2 函数调用
限制
1、表示函数调用的表达式[注:大部分情况下,这是转换一个标识符的结果,该标识符是一个函数指派符。]应该具有指向返回void,或返回一个除数组类型以外的完整对象类型的函数的指针类型。
2、如果指明被调函数的表达式具有一个类型,该类型包含了一个原型,那么实参的个数应该与形参个数一致。每个实参应该具有一个类型,以至于它的值可以被赋给具有其相应形参类型的非限定版本的一个对象。[译者注:这里是说,如果一个实际参数含有一个限定符,那么把它传给形式参数时,形参可不带该限定符。比如:
void func(int a, int *p) { } int main(void) { const int a = 0; int* const p = NULL; func(a, p); // 这里尽管实参a与p都含有const限定,但是可直接传给func中非限定版本的形参 }
]
语义
3、后面跟着圆括号 () 的一个后缀表达式,其包含了一个可能为空的,用逗号分隔的表达式列表,这个表达式则是一个函数调用。此后缀表达式指明了被调函数。表达式列表指定了函数的实参。
4、一个实参可以是任一完整对象类型的一个表达式。在准备调用一个函数时,实参被计算,并且每个形参被赋值为相应实参的值。[注:一个函数可以改变其形参的值,但这些改变不能影响实参的值。另一方面,可以传递一个指向某一对象的指针,这样函数就可以改变所指向对象的值。一个声明为具有数组或函数类型的一个形参被调整为具有在6.9.1中所描述的一个指针类型。]
5、如果表达式指明了被调函数具有指向返回一个对象类型的函数的指针类型,那么该函数调用表达式具有与那个对象类型相同的类型,并且具有由6.8.6.4中所指定能确定的值。否则,函数调用具有void类型。
6、如果表达式指明了被调函数具有不包含一个原型的类型,那么要对每个实参执行整数晋升,并且具有float类型的实参被晋升为double。这被称为默认的实参晋升。如果实参个数与形参个数不相等,并且原型要么用省略号结尾 (, ...) ,要么在晋升之后实参的类型与形参的类型不兼容,那么行为是是未定义的。如果函数用一个不包含一个原型的类型定义,并且实参的类型在晋升之后与晋升后的形参的类型不兼容,那么行为是未定义的,除了以下几种情况:
——一个晋升类型是一个带符号整数类型,而另一个晋升类型是相应的无符号整数类型,那么值可同时用这两种类型表示;
——两种类型都是指向限定或非限定版本的字符类型或void的指针。
7、如果表达式指明了被调函数的表达式具有不包括一个原型的类型,那么实参就好像通过赋值而被隐式转换为相应实参的类型,取每个形参的类型为其所声明类型的非限定版本。在一个函数原型声明符中的省略号使得实参类型转换在最后所声明的形参之后停止。默认的实参晋升在尾随实参上执行。
8、没有其它转换被隐式执行;特别地,在一个函数定义中实参的个数与类型不被形参的个数与类型进行比较,如果该函数不包含一个函数原型声明符的话。
9、如果函数以一个与通过指明该被调函数的表达式所指向的(该表达式的)类型不兼容的类型而被定义,那么行为是未定义的。
10、在函数指派符与实际实参之后,但在实际调用之前,有一个顺序点。在调用函数中的每个计算(包括对其它函数的调用),它没有被指定在被调函数体的执行之前或之后,在被调函数执行相关的顺序上是不确定的。[注:换句话说,函数执行并不相互“交错”]。
11、递归的函数调用应该被允许,既要支持直接递归,也要支持通过任一对其它函数的链式调用的间接递归。
12、例 在以下函数调用中
(*pf[f1()]) (f2(), f3() + f4())
函数f1、f2、f3和f4可以以任一次序被调用。所有副作用必须在pf[f1()]所指向的函数被调用之前而被完成。
6.5.2.3 结构体与联合体成员
约束
1、. 操作符的第一个操作数应该具有一个原子的、限定的,或非限定的结构体或联合体类型,而第二个操作数应该命名该类型的一个成员。
2、 -> 操作符的第一个操作数应该具有类型“指向原子的、限定的,或非限定结构体的指针”或是“指向原子的、限定的,或非限定联合体的指针”,而第二个操作数应该命名所指向类型的一个成员。
语义
3、后面跟着 . 操作符的一个后缀表达式与一个标识符指派了一个结构体或联合体对象的一个成员。该值是该类型的命名成员,[注:如果用于读取联合体对象内容的成员与最后用于把一个值存储到该对象中的成员不相同,那么该值的对象表示的适当部分被重新解释为在6.2.6中所描述的新类型的一个对象表示(这是一个有时被称为“类型双关”的过程)。这可能是一个陷阱表示。],并且如果第一个表达式是一个左值,那么该成员值为一个左值。如果第一个表达式具有限定类型,那么结果具有所指派成员的类型的该限定版本。
4、后面跟着 -> 操作符的一个后缀表达式与一个标识符指派了一个结构体或联合体的成员。该值为第一个表达式所指向对象的命名成员的值,并且是一个左值。[注:如果 &E 是一个有效的指针表达式(这里, & 是“取地址”操作符,它生成了一个指向其操作数的指针),那么表达式 (&E) -> MOS 与 E.MOS 相同。]如果第一个表达式是指向一个限定类型的指针,那么结果具有所指派成员的类型的该限定版本。
5、访问一个原子的结构体或联合体对象的一个成员导致未定义行为。[注:例如,如果在一个线程中访问整个结构体或联合体,与从另一个线程访问一个成员发生冲突,这里至少有一次访问是一次修改操作,那么就会发生数据竞争。成员可以使用一个非原子对象而被安全访问,而这可以从原子对象获取或存储。]
6、为了简化对联合体的使用,要做一个特殊的保证:如果一个联合体包含了若干共享一个公共初始序列的结构体(见下面),并且如果该联合体对象当前包含这些结构体的其中之一,那么它被允许检查它们之间任何一个的公共初始部分,该联合体的完整类型的一个声明在此部分可见。如果两个结构体相应成员具有兼容类型(以及,对于位域,具有相同带宽)对于一个或多个初始成员的一个序列,那么这两个结构体共享一个公共初始序列。
7、例1 如果f是一个返回一个结构体或联合体的一个函数,并且x是那个结构体或联合体的一个成员,那么 f().x 是一个有效的后缀表达式,但不是一个左值。
8、例2 在以下代码片段中:
struct s { int i; const int ci; }; struct s s; const struct s cs; volatile struct s vs;
各个成员具有如下类型:
s.i int
s.ci const int
cs.i const int
cs.ci const int
vs.i volatile int
vs.ci volatile const int
9、例3 以下代码片段是有效的:
union { struct { int alltypes; } n; struct { int type; int intnode; } ni; struct { int type; double doublenode; } nf; } u; u.nf.type = 1; u.nf.doublenode = 3.14; /* ... */ if (u.n.alltypes == 1) if (sin(u.nf.doublenode) == 0.0) /* ... */
以下代码片段不是有效的(因为联合体类型在函数f内不可见):
struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 *p1, struct t2 *p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); }
6.5.2.4 后缀递增与递减操作符
约束
1、后缀递增或递减操作符的操作数应该具有原子的、限定的、或非限定的实数或指针类型,并且应该具有一个可修改的左值。
语义
2、后缀 ++ 操作符的结果是操作数的值。作为一个副作用,操作数对象的值被递增(即,适当类型的值1被加到上面)。见加法操作符的讨论以及对约束、类型以及转换和对指针操作的副作用的复合赋值。值计算的结果顺序在更新操作数所存储的值的前面。关于一个不确定顺序的函数调用,后缀 ++ 操作是一单个计算。在一个具有原子类型对象上的后缀 ++ 操作是一个具有memory_order_seq_cst存储器次序语义的读-修改-写操作。[注:这里,一个指向原子对象的指针可以被形成,并且E具有整数类型,E++等价于以下代码次序,这里T是E的类型:
T *addr = &E; T old = *addr; T new; do { new = old + 1; } while (!atomic_compare_exchange_strong(addr, &old, new));
old作为操作的结果。
如果E具有浮点类型,那么需要做特别的注意;见6.5.16.2。
]
3、后缀 -- 操作符类似于后缀 ++ 操作符,除了操作数的值是递减的(即,适当类型的值1从该操作数上减去)。
6.5.2.5 复合字面量
约束
1、类型名应该指定一个完整的对象类型或一个未知大小的数组,但不能是一个变长数组类型。
2、所有罗列在6.7.9中的初始化器的约束也应用于复合字面量。
语义
3、由一个带圆括号类型名,后面跟着一个花括号初始化器列表的一个后缀表达式是一个复合字面量。它提供了一个未命名的对象,其值由初始化器列表给出。[注:注意,这与一个投射表达式不同。比如,一个投射表达式仅指定了转换到一个标量类型或void,并且一个投射表达式的结果不是一个左值。]
4、如果类型名指定了一个未知大小的数组,那么该大小通过在6.7.9中所指定的初始化器列表确定,并且该复合字面量的类型为完整数组类型。否则,(当类型名指定了一个对象类型),复合字面量的类型为该类型名所指定的类型。在任一种情况下,结果都是一个左值。
5、复合字面量的值为由初始化器列表所初始化的一个未命名对象。如果复合字面量在一个函数体外部发生,那么对象具有静态存储周期;否则,它具有与包围的语句块相关联的存储周期。
6、对于在6.7.9中的初始化器列表的所有语义规则也应用于复合字面量。[注:比如,没有显式初始化器的子对象被初始化为零。]
7、字符串字面量与const限定类型的复合字面量,不需要指派独立的对象。[注:这允许实现对字符串字面量与带有相同或跌交的表示的常量复合字面量共享存储空间。]
8、例1 文件作用域定义
int *p = (int[]){2, 4};
将p初始化为指向带有两个int元素的数组的第一个元素,第一个元素具有值2,第二个值为4。在此复合字面量中的表达式要求是常量。未命名对象具有静态存储周期。
9、例2 对比之下
void f(void) { int *p; /* ... */ p = (int[2]){ *p }; /* ... */ }
这里,p被赋值为一个带有两个int元素数组的第一个元素的地址,第一个具有先前由p所指的值,而第二个为零。在此复合字面量中的表达式不需要是常量。未命名对象具有自动存储周期。
10、例3 带有指派的初始化器可以与复合字面量结合。使用复合字面量创建的结构体对象可以被传递给函数,而无需依赖于成员次序:
drawline((struct point){ .x = 1, .y = 1 }, (struct point){ .x = 3, .y = 4 });
或者,如果drawline需要指向struct point的指针的话:
drawline(&(struct poing){.x = 1, .y = 1}, &(struct point){.x = 3, .y = 4});
11、例4 一个只读复合字面量可以通过类似以下构造来指定:
(const float[]){1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6}
12、例5 下列三个表达式具有不同意思:
"/tmp/fileXXXXXX" (char[]){"/tmp/fileXXXXXX"} (const char[]){"/tmp/fileXXXXXX"}
第一个总是具有静态存储周期,并且具有char数组类型,但不需要可修改的;最后两个当它们在一个函数体内时具有自动存储周期,并且中间那个是可被修改的。
13、例6 跟字符串字面量一样,const限定的复合字面量可以被存放在只读存储器,并且甚至可被共享。比如:
(const char[]){"abc"} == "abc"
可能产生1,如果字面量存储空间被共享的话。
14、例7 由于复合字面量是未命名的,因而一单个复合字面量不能指定一个循环连接的对象。比如,对于下列例子,没有方法写一个自引用的复合字面量,该复合字面量要能被用作为函数的实参,放在endless_zeros命名对象中:
struct int_list { int car; struct int_list *cdr; }; struct int_list endless_zeros = { 0, &endless_zeros }; eval(endless_zeros);
15、例8 每个复合字面量在一给定的作用域仅创建一单个对象:
struct s { int i; }; int f(void) { struct s *p = 0, *q; int j = 0; again: q = p; p = &((struct s){ j++ }); if(j < 2) goto again; return p == q && q->i == 1; }
函数f()总是返回1值。
16、注意,如果使用一个迭代语句来代替goto与一个标签语句,那么未命名对象的生命周期将仅仅为该循环体,出了此循环体之后p的值将是一个不确定的值,而这将会导致未定义行为。