本文主要是对C语言里对象标识符的链接性进行总结。考虑下面的语句:
int a; /* 0. 简单临时定义 */
static int a; /* 1. 内部链接临时定义 */
extern int a; /* 2. 纯粹声明 */
int a = 1; /* 3. 块外定义 */
static int a = 1; /* 4. 块外定义(具有内部链接) */
extern int a = 1; /* 5. 块外定义 */
{
int a; /* 6. 块内定义 */
static int a; /* 7. 块内定义 */
extern int a; /* 8. 纯粹声明 */
int a = 1; /* 9. 块内定义 */
static int a = 1; /* a. 块内定义 */
}
位于块(block)内的extern int a = 1;语句是不合法的,所以没将其列进去。
在C标准里,语句0和语句1是tentative definition。语句0的链接性为外部,语句1的链接性为内部。但有的编译器(比如vc),对链接性的规定有所不同,语句0的链接性不一定是外部的,所以本文把语句0称为简单临时定义,把语句1称为内部链接临时定义。
带初值符的语句3,4,5是external definition。为了与链接性里的external区分,本文将其称为块外定义。注意,语句5这种方式是不推荐使用的,编译器一般都会有警告。
位于块内的,没有临时定义的概念,统一称为块内定义。
带extern的且没有初值符的,不管位于块外还是块内,都是一个纯粹的声明。
所有的定义都是声明。
通过将这11条语句进行两两组合,看是否能编译通过,可以得出C语言里对象标识符链接性的一些规律。
语句间的相互位置关系会影响链接性,所以这是一个排列(有121种可能)。针对每一个排列,产生一个C文件。因此,这个C文件应包含一个main函数和两条上述语句。把这11条语句分成两类,位于块外的和位于块内的。若两条语句都位于块外,则将它们放在main函数前面;若两条语句都位于块内,则将它们放在main函数里面;若一条位于块内,一条位于块外,则位于块内的放入main函数里,而位于块外的可以放在main函数前面或后面。
用于试验的C文件具有"file_nn.c"形式的文件名,其中n指示语句。
例如:
file_0a.c表明是语句0(简单临时定义)和语句a(块内定义)的组合,语句0位于前面,如:
int a;
int main(int argc, char *argv)
{
static int a = 1;
return 0;
}
file_a0.c表明是语句a(块内定义)和语句0(简单临时定义)的组合,语句a位于前面,如:
int main(int argc, char *argv)
{
static int a = 1;
return 0;
}
int a;
这次试验分别使用了gcc和vc进行编译。其中gcc平台是:windows7+mingw+gcc4.6.1,vc则是使用VS2010的命令行方式编译,其cl.exe的版本是16.00.40219.01。121个C文件中,gcc编译通过77个,vc编译通过83个。这里产生的C文件只包含变量定义和声明,并没有真正使用到该变量。如果加入使用变量的语句(比如printf("%d\n", a);),则编译通过的会更少。例如,file_22.c在gcc和vc下都是编译通过的,它只包含两条extern int a;语句,并没有真正定义该变量,如果加入使用变量a的语句,则编译通不过。
试验的结果如下图所示:
其中,白色块和黄色块是编译通过的,其余都是编译通不过的。比如,A方块,在左边是蓝色,在右边是白色,说明file_10.c,即下面的代码:
static int a;
int a;
int main(int argc, char *argv)
{
return 0;
}
在gcc下编译通不过,在vc下编译通过。
对某个对象标识符来说,应遵守下面5条规则:
规则1,在程序范围内,具有外部链接的块外定义不能超过一个。
规则2,在一个编译单元内,块外定义不能超过一个。
规则3,在相同的块作用域内,若某标识符无链接,则其声明不能超过一个。
规则4,在一个编译单元内,不能既有外部链接又有内部链接。
规则5,作用域独立。
gcc的链接性判定:
(1)在文件作用域内,带static的标识符具有内部链接性,不带存储类区分符(extern,static)的标识符具有外部链接性。
(2)若标识符带extern,则该标识符与当前可见的具有文件作用域的标识符具有相同的链接性;若这样的标识符不存在,则它具有外部链接性。
(3)在块作用域内,不带extern的标识符无链接性。
vc的链接性判定:
(1)extern与链接性的判定无关,它的作用只是增加某个标识符可见性(并成为其代理)。
(2)若文件作用域内第一个声明带static,则该标识符具有内部链接性,否则具有外部链接性。
(3)在文件作用域内,带static的标识符具有内部链接性。
(4)在块作用域内,不带extern的标识符无链接性。
根据上述5条规则和链接性的判定方法,可以对上面的结果进行分析:
左边gcc的结果:
绿色块,根据规则2,33-55总共9个是不允许的。
紫色块,根据规则3,66-aa总共24个是不允许的。注意,88并没有违反该规则。
蓝色块,根据规则4,这11个是不允许的。
黄色块,根据规则5,这58个是允许的。注意,81和84,虽然也是一个块内一个块外,但违反了规则4。
白色块,没有违反规则1-4,这19个是允许的。
右边vc的结果:
绿色块,根据规则2,33-55总共9个是不允许的。
紫色块,根据规则3,66-aa总共24个是不允许的。注意,88并没有违反该规则。
蓝色块,根据规则4,这5个是不允许的。
黄色块,根据规则5,这60个是允许的。
白色块,没有违反规则1-4,这23个是允许的。
vc的链接性判定与gcc的不同,导致了蓝黄白三个色块的不同。
/* A方块,file_10.c(vc通过) */
static int a; /* 内部链接 */
int a; /* gcc判定为外部链接,vc判定为内部链接 */
/* B方块,file_13.c(vc通过) */
static int a; /* 内部链接 */
int a = 1; /* gcc判定为外部链接,vc判定为内部链接(对a来说,值为1,链接性为内部) */
/* C方块,file_21.c(vc通过) */
extern int a; /* gcc判定为外部链接,vc下extern只是增加了标识符的可见性 */
static int a; /* 内部链接 */
/* D方块,file_24.c(vc通过,同C) */
extern int a;
static int a = 1;
/* E方块,file_40.c(vc通过) */
static int a = 1; /* 内部链接 */
int a; /* gcc判定为外部链接,vc判定为内部链接 */
/* F方块,file_41.c(gcc通过,vc通不过的原因未知) */
static int a = 1; /* 内部链接 */
static int a; /* 内部链接 */
/* G方块,file_81.c(vc通过) */
{
extern int a; /* gcc判定为外部链接,vc下的判定见后面说明 */
}
static int a; /* 内部链接 */
/* H方块,file_84.c(vc通过,同G) */
{
extern int a;
}
static int a = 1;
当对象标识符存在多个声明时,按下面所列顺序判定到底使用哪一个:
(1)本编译单元的块外定义。
(2)本编译单元的内部链接临时定义,其值为0。
(3)其他编译单元的具有外部链接的块外定义。
(4)简单临时定义(本单元或其他单元),其值为0。
有一点需要注意,vc里面的语句8,即位于块内的extern声明,不会引用当前不可见的具有内部链接的定义,这与语句2的处理方式不同。如下面代码所示(它们在gcc下都不能编译通过):
/* vc下编译通过,它增加了下面static定义的可见性 */
extern int a;
int main(int argc, char *argv[])
{
printf("%d\n", a);
return 0;
}
static int a;
/* vc下编译通不过,块内的extern声明,不会引用当前不可见的具有内部链接的定义 */
int main(int argc, char *argv[])
{
extern int a;
printf("%d\n", a);
return 0;
}
static int a
/* 综合上面两个,vc下编译通过 */
extern int a; /* 增加下面static定义的可见性 */
int main(int argc, char *argv[])
{
extern int a; /* 由于上面extern的存在,导致它也可以引用后面的static定义 */
printf("%d\n", a);
return 0;
}
static int a;
一些说明:
(1)为什么file_41.c(即F方块)在vc下编译通不过的原因未知,一般认为是vc没有遵守C标准。
(2)vc还有其他一些地方没有遵守C标准,比如对file_81.c(以及file_84.c)的处理。若其他编译单元存在a的具有外部链接的块外定义,则将导致在file_81.c这个编译单元内,a的标识符既有外部链接又有内部链接,违反了规则4,但vc还是编译通过了。
(3)从编译信息来看,并不是严格按照上述的规则一条条排除下来,虽然最终的结果是一致的。比如file_6a.c在vc下的编译出错信息是:"error C2370: "a": 重定义;不同的存储类"。这个"不同的存储类"给的有些奇怪,毕竟位于块内的语句6和语句a都是无链接性的。
(4)总体来说,gcc比较遵守C标准,vc则有一些自己的规则。