外部定义(一)
一、概述
如果一个声明同时为标识符命名的对象或者函数预留了存储空间,那么这个声明就是一个定义(definition)。外部定义(external definition)是一种外部声明(external declaration),同时也是函数(内联定义(inline definition)除外。)或者对象的定义。外部声明之所以称为外部,是因为它出现在任何函数之外;因此外部声明声明的标识符具有文件作用域。
ISO/IEC 9899:2024标准对外部声明作了一些限制:
-- 存储类说明符register不能出现在外部声明的声明说明符中。
register int func(void); //非法。 register int global; //如果global具有文件作用域,则非法。
如果存储类说明符register用于函数形参的声明或者具有块作用域对象的声明则是合法的。
int func(register int); //合法。 ... register int local; //如果local具有块作用域,则合法。
-- 存储类说明符auto仅用于类型推断时才可以出现在外部声明的声明说明符中。
//假设i1、i2具有文件作用域。 auto int i1 = 0; //非法。 auto i2 = 0; //合法。 ... //假设i3、i4具有块作用域。 auto int i3 = 0; //合法。 auto i4 = 0; //合法。
-- 同一编译单元内,对于具有内部链接的标识符,其外部定义最多不超过一个;此外如果具有内部链接的标识符在表达式中使用,那么在同一编译单元内,该标识符必须存在一个外部定义,以下情况除外:
① 作为sizeof运算符操作数的一部分,其结果是一个整数常量。
② 作为alignof运算符操作数的一部分,其结果是一个整数常量。
③ 作为泛型选择(generic selection)控制表达式的一部分。
④ 作为泛型关联(generic association)表达式的一部分,并且该表达式不是泛型选择的结果表达式。
⑤ 作为typeof类运算符操作数的一部分,并且该运算符的结果类型不是可变修改类型(variably modified type)。
如果具有外部链接的标识符在表达式中使用,那么在整个程序中,该标识符必须存在一个外部定义(以下①、②、③、④、⑤的情况除外。);否则标识符的外部定义不能超过一个。如果具有外部链接的标识符未在表达式中使用,该标识符可以不存在外部定义。
① 作为sizeof运算符操作数的一部分,其结果是一个整数常量。
extern int global; ... size_t st = sizeof(global); //global不会被评估,所以可以没有外部定义。
② 作为alignof运算符操作数的一部分,其结果是一个整数常量。
extern int global; ... printf("%u\n", alignof(typeof(global))); //global不会被评估,所以可以没有外部定义。
③ 作为泛型选择(generic selection)控制表达式的一部分。
extern int global; #define TYPE(x) _Generic((x), \ int: "int type.", \ default: "unknown type." \ ) ... printf("%s\n", TYPE(global)); //global不会被评估,所以可以没有外部定义。
④ 作为泛型关联(generic association)表达式的一部分,并且该表达式不是泛型选择的结果表达式。
float func_f(float); double func_d(double); long double func_l(long double); #define FUNC(x) _Generic((x), \ float: func_f, \ long double: func_l, \ default: func_d \ )(x) ... //本例中func_f和func_l不会被评估,所以可以没有外部定义; //func_d会被评估,所以应存在外部定义。 printf("%f\n", FUNC(3.14)); ... double func_d(double d) { return d; }
⑤ 作为typeof类运算符操作数的一部分,并且该运算符的结果类型不是可变修改类型(variably modified type)。
extern int m; extern int n; int n = 3; ... typeof(m) i; //m不会被评估,所以可以没有外部定义。 typeof(int [n]) arr; //n会被评估,所以应存在外部定义。
二、外部对象定义
如果一个对象标识符的声明具有文件作用域并且存在初始化器,或者具有文件作用域并且使用存储类说明符thread_local,则该声明构成该标识符的外部定义。
//假设global和thi都具有文件作用域。 int global = 0; //声明构成外部定义。 thread_local int thi; //声明构成外部定义。
具有文件作用域但不存在初始化器、并且未使用存储类说明符extern或者thread_local的对象标识符的声明构成暂定定义(tentative definition)。
//假设i1和i2都具有文件作用域。 int i1; //声明构成暂定定义。 static int i2; //声明构成暂定定义。
暂定定义允许在同一编译单元内对同一对象标识符进行多次声明;但最终最多只能存在一个定义。编译时遇到暂定定义,编译器会先记录,但不会立即分配存储空间(这与外部定义不同。),直至编译单元结束才分配存储空间。
//假设a、b、c、d、e、f、g、h均具有文件作用域。 int a; int a; //合法。 int b; int b = 3; //合法。 int c = 5; int c; //合法。 int d = 6; int d = 8; //非法,d重定义。 static int e; //内部链接。 int e; //非法,外部链接,链接属性不一致。 static int f = 0; //内部链接。 int f; //非法,外部链接,链接属性不一致。 extern int g; //外部链接。 static int g; //非法,内部链接,链接属性不一致。 static int h; //内部链接。 extern int h; //合法,引用前面声明的h,因此也是内部链接,链接属性一致。
如果编译单元包含某个标识符的一个或者多个暂定定义,并且该编译单元不包含该标识符的外部定义,则该标识符的行为等同于该编译单元包含该标识符的文件作用域声明,并且该声明具有空初始化器,其类型按以下规则确定:
-- 如果编译单元结束时复合类型(composite type)是未知大小数组(例如:double arr[]),则视为具有复合元素类型、大小为1的数组(double arr[1]);
-- 否则最终类型是编译单元结束时该标识符的复合类型。
int arr[]; int arr[5] = {0}; //数组arr的类型是int [5]类型。
(注:如果一个标识符在同一编译单元内多次声明(包括暂定定义。),编译器会将所有这些声明合并成一个统一的类型视图,这就是复合类型。这样确保了所有声明的类型兼容性。)
如果对象标识符的声明是暂定定义,并且具有内部链接,则其声明类型应是完整类型。
static int a1[]; //非法,是不完整类型。 static int a2[5]; //合法,是完整类型。