ISO/IEC 9899:2011 条款6.7.6——声明符
6.7.6 声明符
语法
1、declarator:
pointeropt direct-declarator
direct-declarator:
identifier
( declarator )
direct-declarator [ type-qualifier-listopt assignment-expressionopt ]
direct-declarator [ static type-qualifier-listopt assignment-expressionopt ]
direct-declarator [ type-qualifier-listopt static assignment-expressionopt ]
direct-declarator [ type-qualifier-listopt * ]
direct-declarator ( parameter-type-list )
direct-declarator ( identifier-listopt )
pointer:
* type-qualifier-listopt
* type-qualifier-listopt pointer
type-qualifier-list:
type-qualifier
type-qualifier-list type-qualifier
parameter-type-list:
parameter-list
parameter-list , ...
parameter-list:
parameter-declaration
parameter-list , parameter-declaration
parameter-declaration:
declaration-specifier declarator
declaration-specifier abstract-declaratoropt
identifier-list:
identifier
identifier-list , identifier
语义
2、每个声明符声明了一个标识符,并且断言,当一个操作数与声明符的形式相同,并出现在一个表达式中时,该操作数指派了一个函数或对象,由声明说明符所指示的作用域、存储周期和类型。
3、一个完整声明符是这么一个声明符,它不是另一个声明符的一部分。一个完整声明符的结尾是一个顺序点。如果,在一个完整声明符中的嵌套声明符序列中,有一个指定可变长度数组类型的声明符,那么由该完整声明符所指定的类型被称为可变修改的。此外,任何从一个可变修改类型由声明符类型源所派生得到的类型,是其自身可变修改的。
4、在下列子条款中,考虑一个声明:T D1
这里,T包含了指定一个类型T的声明说明符(比如int),而D1是一个声明符,包含一个标识符ident。在声明符的各种形式中,为标识符ident而指定的类型,使用这个说明来归纳描述。
5、如果,在上述声明中,D1具有形式:identifier
那么,为ident而指定的类型是T。
6、如果在声明“T D1”中,D1具有这种形式——( D )
那么,ident具有由声明“T D”所指定的类型。从而,在圆括号中的一个声明符与不使用圆括号的声明符是同一个,但是复杂声明符的绑定可以用圆括号替换。
实现限制
7、正如5.2.4.1中所讨论的,一个实现可以限制指针、数组以及函数声明符的个数,这些声明符修饰了一个算术、结构体、联合体或void类型,要么直接修饰,要么通过一个或多个typedef来修饰。
6.7.6.1 指针声明符
语义
1、如果在声明“T D”中,D1具有这种形式——
* type-qualifier-listopt D
并且为ident而指定的类型在“T D”声明中是“derived-declarator-type-list T”,那么为ident而指定的类型是“指向T的derived-declarator-type-list type-qualifier-list指针”。对于列表中的每个限定符,ident为如此限定的指针。
2、对于两种兼容的指针类型,两者都应该是同样被限定的,并且应该为指向相兼容类型的指针。
3、例 下列一对声明描述了在一个“指向一个常量值的变量指针”与一个“指向一个变量值的常量指针”之间的区别。
const int *ptr_to_constant; int * const constant_ptr;
由ptr_to_constant所指向的任一对象的内容不应该通过该指针修改,但是ptr_to_constant其本身可以被改变为指向另一个对象。类似地,由constant_ptr指向的int的内容可以被修改,但是constant_ptr其本身应该总是指向同一个位置。
4、常量指针constant_ptr的声明可以通过包含对类型“指向int的指针”类型的一个定义来变得更清晰。
typedef int *int_ptr; const int_ptr constant_ptr;
将constant_ptr声明为一个对象,该对象具有类型“指向int的const限定指针”的类型。
6.7.6.2 数组声明符
约束
1、除了可选的类型限定符以及关键字static之外,[ 和 ] 也可以对一个表达式或 * 划定界限。如果它们对一个表达式划定界限(这指定了一个数组的大小),那么该表达式应该具有一个整数类型。如果该表达式是一个常量表达式,那么它应该具有一个大于零的值。元素类型不应该是一个不完整类型或一个函数类型。可选的类型限定符以及关键字static应该仅出现在具有一个数组类型的函数形参的声明中,并且随后仅在最外部的数组类型派生中。
2、如果一个标识符被声明为具有一个可变修改的类型,那么它应该是一个普通的标识符(正如6.2.3中定义的),不具有连接,并且要么具有语句块作用域,要么具有函数原型作用域。如果一个标识符被声明为带有静态或线程存储周期的一个对象,那么它不应该具有一个可变长度数组类型。
语义
3、在声明“T D1”中,如果D1具有以下形式之一:
D [ type-qualifier-listopt assignment-expressionopt ]
D [ static type-qualifier-listopt assignment-expression ]
D [ type-qualifier-list static assignment-expression ]
D [ type-qualifier-listopt * ]
并且在声明 "T D"中的为ident而指定的类型是“derived-declarator-type-list T”,那么为ident而指定的类型是“T的derived-declarator-type-list数组”。[注:当若干“数组的”指定毗邻时,则声明了一个多维数组。](见6.7.6.3获悉可选的类型限定符以及关键字static的意义。)
4、如果数组类型没有大小,那么该数组类型是一个不完整类型。如果大小是 * 而不是是一个表达式,那么数组类型是一个未指定大小的可变长度数组类型,它只能用在带有函数原型作用域的声明或类型名中;[注:从而,* 只能用在不作为定义的函数声明中(见6.7.6.3)。]这样的数组即便如此也是完整类型。如果大小是一个整数常量表达式并且元素类型是一个已知常量大小,那么数组类型不是一个可变长度的数组类型;否则,数组类型是一个可变长度数组类型。(可变长度数组是一个可选特征,实现不需要一定支持;见6.10.8.3。)
5、如果大小是一个表达式,且该表达式不是一个整数常量表达式:如果它发生在函数原型作用域处的一个声明中,那么它被看作为用 * 替代;否则,在每次它被计算时,它应该要有一个大于零的值。一个可变长度数组类型的每个实例的大小,在其生命周期期间不会改变。一个大小表达式作为一个sizeof操作符的操作数一部分,并改变该大小表达式的值的地方将不影响操作符的结果,大小表达式是否被计算是未指定的。[译者注:
// 声明一个函数ArrayTest,这里可以指定数组a的大小为* static void ArrayTest(int a[*]); // 定义函数ArrayTest时,不能用a[*],但可用a[]或者以下形式 static void ArrayTest(int a[static 100]) { int len = 10; int vla[len + 6]; printf("The size is of a: %zu\n", sizeof(a)); // 相当于sizeof(int*) printf("The size of vla is: %zu\n", sizeof(vla)); // 输出64,即sizeof(int[16]) printf("The size is: %zu\n", sizeof(vla[++len])); // 在Clang编译器中,++len没有被计算 printf("len = %d\n", len); // len仍然为10 }
]
6、对于两个相互兼容的数组类型,两者都应该具有兼容的元素类型,并且如果两者大小说明符存在,并且都是整数常量表达式,那么两者大小说明符都应该具有相同的常量值。如果两个数组类型用在一个上下文中,该上下文要求它们兼容,那么如果这两个大小说明符计算为不相等的值,行为是未定义的。
7、例1
float fa[11], *afp[17];
声明了一个float成员的数组以及指向float指针成员的数组。
8、例2 注意声明之间的区别
extern int *x; extern int y[];
第一个声明将x声明为指向int的一个指针;第二个将y声明为未指定大小的int的一个数组(一个不完整类型),其存储在其它地方定义。
9、例3 下列声明描述了对于可变修改类型的兼容性规则。
extern int n; extern int m; void fcompat(void) { int a[n][6][m]; int (*p)[4][n+1]; int c[n][n][6][m]; int (*r)[n][n][n+1]; p = a; // 无效:因为4 != 6,所以不兼容 r = c; // 兼容,但是只有当n == 6且m == n+1时才是定义的行为 }
10、例4 可变修改(VM)类型的所有声明必须要么在语句块作用域,要么在函数原型作用域。用_Thread_local,static,或extern存储类说明符声明的数组对象不能具有一个可变长度数组(VLA)类型。然而,用static存储类说明符声明的一个对象可以具有一个VM类型(即,一个指向VLA类型的指针)。[译者注:
static void ArrayTest(int a[static 100]) { int len = 10; int vla[len + 6]; static int (*sp)[len + 6]; // OK // static int sp2[len]; 无效 sp = &vla; }
]最后,所有用一个VM类型所声明的标识符必须是普通标识符,并且从而不能作为结构体或联合体的成员。
extern int n; int A[n]; // 无效:文件作用域VLA extern int (*p2)[n]; // 无效:文件作用域VM int B[100]; // 有效:文件作用域,但不是VM void fula(int m, int C[m][m]); // 有效:带有原型作用域的VLA void fula(int m, int C[m][m]) // 有效:调整为指向VLA的指针 { typedef int VLA[m][m]; // 有效:语句块作用域typedef VLA struct tag { int (*y)[n]; // 无效:y不是普通标识符 int z[n]; // 无效:z不是普通标识符 }; int D[m]; // 有效:自动的VLA变量 static int E[m]; // 无效:静态作用域的VLA extern int F[m]; // 无效:F具有连接,而且是VLA int (*s)[m]; // 有效:指向VLA的自动指针变量 extern int (*r)[m]; // 无效:r具有连接,且是指向VLA的指针 static int (*q)[m] = &B; // 有效:q是一个指向VLA的静态语句块指针 }
6.7.6.3 函数声明符(包含原型)
约束
1、一个函数声明符不应该指定一个返回类型,该返回类型是一个函数类型或一个数组类型。
2、可以存在于一个形参声明中的仅有的存储类说明符是register。
3、在一个函数声明符中(该函数声明符不是此函数定义的一部分)的一个标识符列表应该为空(empty)。
4、在调整之后,一个函数声明符中的一个形参类型列表中的形参(这些形参作为该函数定义的一部分)不应该具有不完整类型。
语义
5、如果在“T D1”声明中,D1具有以下形式
D ( parameter-type-list )
或
D ( identifier-listopt )
并且在“T D1”声明中用于指定ident的类型是“derived-declarator-type-list T”,那么用于指定ident的类型是“返回T的derived-declarator-type-list函数”。
6、一个形参类型列表指定了函数形参的类型,并且可以声明函数形参的标识符。
7、作为“type的数组”的一个形参的声明应该被调整为“指向type的限定的指针”,这里类型限定符(如果存在的话)是在数组类型派生的[ 和 ]内所指定的。如果关键字static也出现在数组类型派生的[ 和 ]内,那么对于对该函数的每一次调用,相应实际参数的值应该提供对一个数组的第一个元素的访问,而且该数组具有与大小表达式所指定的同样多的元素个数。
8、一个形参作为“返回type的函数”的一个声明,应该被调整为“指向返回type的函数的指针”,如6.3.2.1中所描述的。
9、如果列表以一个省略号(, ...)终结,那么在逗号后面形参的个数以及类型都不会提供任何信息。[注:定义在<stdarg.h>头文件(7.16)中的宏可以被用来访问省略号相应的实参。]
10、一个未命名void类型的形参作为列表中唯一项的特殊情况,指定了该函数不具有形参。
11、在一个形参声明中,如果一个标识符可以被看作为一个typedef名,或者是一个形参名,那么它应该被看作为一个typedef名。[译者注:
typedef int my_int; // 在Clang编译器中,以下函数声明中,my_int为typedef名,a是形参名 static void MyFunc2(const my_int, const a);
]
12、如果函数声明符不作为该函数定义的一部分,那么形参可以具有不完整类型,并且可以在这些参数的声明符指定符序列中使用 [ * ] 记号来指定可变长度数组类型。
13、在声明说明符中用于修饰一个形参声明的存储类说明符,如果存在,那么它被忽略,除非所声明的形参是用于一个函数定义的形参类型列表的其中一个成员。
14、一个标识符列表仅仅声明了函数形参的标识符。一个函数声明符(该声明符不作为函数定义的一部分)中的空列表指定了函数没有形参。此空列表不指定所提供形参的类型和个数相关的任何信息。[注:见“未来语言方向”(6.11.6)。][译者注:
// 用空参数列表声明MyFunc3 static void MyFunc3(); // 用带有一个int a的形参列表来定义MyFunc3 static void MyFunc3(int a) { }
]
15、对于两个相兼容的函数类型,两者都应该指定相兼容的返回类型。[注:如果两个函数类型都是“旧的类型”,那么形参类型不被比较。]此外,如果两个形参类型列表都存在,那么应该满足形参个数以及省略号终结符的使用;相应的形参应该具有相兼容的类型。如果其中一个类型具有一个形参类型列表,而另一个类型通过一个函数声明符(该声明符不作为函数定义的一部分)来指定,且含有一个空的标识符列表,那么形参列表不应该具有一个省略号终结符,并且每个形参的类型应该相互兼容(这些类型能从应用程序的默认实参晋升得到)。如果一个类型具有一个形参类型列表,且另一个通过函数定义来指定(该函数定义可能包含空的标识符列表),那么两者都应该满足形参个数,并且每个原型形参的类型都应该相互兼容,这些类型从应用程序的相应默认实参晋升到相应标识符类型而得到。(对类型兼容性以及复合类型的兼容性的判定中,用函数或数组类型声明的每个形参被取为具有调整后的类型,并且用限定类型声明的每个形参被取为具有其声明类型的非限定版本。[译者注:
static void MyFunc4(void func(void), int a[3], const int n); // 被自动调整为 static void MyFunc4(void (*func)(void), int *a, int n) { // 与上述声明的MyFunc4,在类型上完全兼容 }
])
16、例1 以下声明
int f(void), *fip(), (*pfi)();
声明了一个函数f,不带任何形参,返回一个int;一个函数fip,不带任何形参,返回一个指向int的指针;一个指针pfi,指向不带任何形参并返回一个int的函数。对于比较后两者尤其有用。*fip()的结合是*(fip()),以至于这个声明揭示了,在一个表达式中的同样的构造要求调用一个fip函数,然后通过指针结果的间接操作产生一个int。在声明符(*pfi)()中,额外的圆括号是必要的以指示通过一个指向函数的指针的间接操作来产生一个函数指派符,该指派符然后可以用于调用此函数;它返回一个int。
17、对于上述声明,如果这些声明发生在任一函数外面,那么标识符具有文件作用域和外部连接。如果声明发生在一个函数内部,那么函数f和fip具有语句块作用域,并且具有内部或外部连接(依赖于这些标识符的声明对什么文件作用域可见),而指针pfi的标识符具有语句块作用域且没有连接。
18、例2 对于以下声明
int (*apfi[3])(int *x, int *y);
声明了一个数组apfi,它具有三个指向返回类型为int的函数的指针。这些函数的每一个具有两个指向int的指针形参。标识符x和y的声明仅用于描述目的,并且其作用域到apfi的声明结束为止。
19、例3 以下声明
int (*fpfi(int (*)(long), int)) (int, ...);
声明了一个函数fpfi,返回一个指向返回为int函数的指针。函数fpfi具有两个形参:一个指向返回一个int的函数(带有一个long int类型的形参)的指针,和一个int。由fpfi所返回的指针指向一个函数,它具有一个int形参并接受零个或多个额外的任意类型的实参。
20、例4 以下原型具有一个可变修改的形参。
void addscalar(int n, int m, double a[n][n*m+300], double x); int main() { double b[4][308]; addscalar(4, 2, b, 2.17); return 0; } void addscalar(int n, int m, double a[n][n*m+300], double x) { for(int i = 0; i < n; i++) for(int j = 0, k = n*m+300; j < k; j++) // a是一个指向带有n*m+300个元素的VLA的指针 a[i][j] += x; }
21 例5 以下都是兼容的函数原型声明。
double maximum(int n, int m, double a[n][m]); double maximum(int n, int m, double a[*][*]); double maximum(int n, int m, double a[][*]); double maximum(int n, int m, double a[][m]); void f(double (* restrict a)[5]); void f(double a[restrict][5]); void f(double a[restrict 3][5]); void f(double a[restrict static 3][5]);
(注意,最后的声明也指定了在对f的任一调用中相应于a的实参,必须是一个指向至少3个具有5个double元素的数组的非空指针,而不能是其它。)