声明分为引用性声明和定义性声明。一般而言(本文中也是如此),“声明”指引用性声明 ,“定义”指定义性声明。本文中示例代码的运行结果均为gcc环境下所得。
1 声明和定义的语法
1.1 对于变量
只要不用extern 修饰即为定义。
int a; //定义 弱符号
int b = 1; //定义并初始化 强符号
使用extern 修饰的变量,如果做了初始化则为定义,否则是一个声明。 但使用extern 修饰的变量做初始化,编译时会产生警告。
extern int a; //声明
extern int b = 1; //定义
编译结果:
warning: 'b' initialized and declared 'extern'
声明或定义中如果没有类型则采用默认类型 ,并产生警告。当然这取决于你的编译选项设置。
a = 1; //定义
编译结果:
warning: data definition has no type or storage class
a = 1;
^
warning: type defaults to 'int' in declaration of 'a' [-Wimplicit-int]
1.2 对于函数
通过是否带有函数体区分定义或声明。
void test1()
{//带有函数体 为定义
}
void test2(); //没有函数体 为声明
2 scope和linkage
2.1 scope
C99 6.2.1 规定了四个scope :
blockscope
局部代码块范围,即{ }内。
function scope
标签(Labels)是唯一一类拥有function scope的名字。可以在它所出现的函数中的任何位置被使用(通过goto语句)。
function prototype scope
函数原型中声明的名字只在该原型结束前可见。比如下面的原型声明了3个名字(strDestination, numberOfElements 和 strSource);在原型结束处它们离开作用域。
errno_t strcpy_s( char *strDestination, size_t numberOfElements, const char *strSource );
file (global)scope
全局范围。
2.2 linkage
对象和函数名在编译单元之间的共享方式被称为linkage。
linkage | 说明 |
---|---|
external linkage | 标识符在其他编译单元1可见 |
internal linkage | 标识符仅在本编译单元可见 |
no linkage | 标识符仅在它们的作用域内可见 |
internal linkage
- 显式声明为static的对象、引用、函数或函数模板。
- 使用了说明符const,但没有显式使用extern或提前声明为external linkage的对象或引用。
- 匿名 union 中的数据成员。
- 匿名命名空间中声明的标识符。
external linkage - file scope中,没有使用static声明对象和函数的标识符。
no linkage - 函数参数
- block scope中未声明为 extern 或 static 的名字。
- typedef语句中声明的名字。
2.3 static 用法总结
- 全局变量定义时加上static修饰,表示该变量为静态全局变量。作用域为当前文件。
- 任意函数的定义或声明中包含了static修饰,表示该函数为静态函数,只能在本文件中被调用。
- 局部变量定义时,带有static关键字,表示其为静态局部变量,只被初始化一次,之后每次调用函数时,该变量值为上次函数退出时的值。即,改变量的生存周期被扩展到整个程序运行时间段内。
2.4 枚举,结构体,联合体的类型定义可以在其他文件中声明么?
似乎没有相应的语法,只能在其他文件中重新定义相同的类型。
多个文件使用相同的类型定义时,可以定义在.h文件中,然后在.c文件中include 。如果你定义的类型只在本.c文件中使用那么直接写在本.c文件中就好。
3 强符号和弱符号
在编程中会发现,多个文件中重复定义同一全局变量,在链接时不一定会报错。这是为何呢? 在《程序员的自我修养-链接、装载与库》的强符号与弱符号一节中可以找到这个问题的原因。
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。也可以通过GCC的“attribute((weak))””来决定任何一个强符号为弱符号。强符号和弱符号是针对符号的定义来说的,不针对付符号的引用。
引用也可以分为强引用和弱引用。对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们需要被正确决议,如果没有找到改符号的定义,链接器会报符号未定义错误,这种被称为强引用。而对于弱引用,如果未定义,链接器默认其为0或者一特殊值,以便程序代码能够识别。
3.1 链接器处理规则
针对强弱符号的概念,链接器按如下规则处理多次定义的全局符号:
- 不允许强符号多次定义,如果存在则报符号重复定义错误。
- 如果一个符号在某个目标文件2中是强符号,在其他文件中是弱符号,那么选强符号
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
3.2 如何避免重复定义
- 尽量不使用全局变量。
- 定义成static的,并封装函数,外部文件通过调用函数来使用。
- 定义时初始化,使其成为强符号。
GCC提供了一个选项,可以检查这类错误:-fno-common。
3.3 弱符号、弱引用和链接器的COMMON块
弱符号和弱引用主要用于库的链接过程。与连接器COMMON块概念联系紧密。。。。。。
待补充
4 初始化
全局变量或局部静态变量,如果没有初始化或初始化为 0,则编译后会被存放在目标文件的未初始化段(.bss段)。如果定义中有初始化,则被放在数据段(.data)或只读数据段(.rodata)。它们都是在程序运行之前被初始化的。
局部变量则在程序运行时才会进行初始化。在初始化前的值是什么,c语言标准中说由具体实现决定。一般而言下是这个变量地址之前储存的值。
不过即使你在定义过程中做了初始化也可能会出现一些奇怪的问题。比如以下例子:
int s= 1;
void test()
{
switch(s){
int a = 1;
case 0 :
printf(" here !");
int b = 1;
break;
case 1 :
printf("a is %d b is %d\n",a,b);
break;
default :
break;
}
return ;
}
变量a
的作用域是在整个swtich{}
块中,它被分配在栈上。由于switch在执行时是直接跳转到case
执行的,变量a
的初始化未被执行。所以a
的值是一个不确定的值。
类似的在case 0:
中定义并初始化 变量。由于case 0:
语句不会被执行到,变量b
的值也是不确定的。
另外 case 是一个标签,在case :
后边紧跟变量声明是会报错的。
int s = 1;
void test()
{
switch(s){
case 0 :
int a = 1;
break;
default :
break;
}
return ;
}
编译结果:
error:a label can only be part of a statement and a declaration is not a statement
5 函数隐式声明
引用外部文件中的全局变量 必须显示声明该变量。否则编译器会报错。
error: 'xxx' undeclared (first use in this function)
对于函数则只会产生警告。
implicit declaration of function 'xxxx' [-Wimplicit-function-declaration]
在C89中,函数在调用前不一定非要声明。如果没有声明,那么编译器会自动按照一种隐式声明的规则去处理。较新的C规范(C99、C11)是不允许不声明直接用的。只是GCC默认还允许implicit function declaration功能。
如果调用外部文件的函数而没有声明,则gcc编译器会默认函数返回值得类型为int 型。如果函数原型的返回值不是int型。gcc编译器可能会产生类型不匹配的警告。不过对于标准库内建函数,gcc编译器会使用正确的返回值类型。
关于函数隐式声明的详细讨论可参考我之间转载的文章- C语言中的隐式函数声明