玩儿转C语言:宣言和定义(2)

玩儿转C语言:声明和定义(2)

1、如何解释声明和定义,它们有何区别?

        在C语言中,(某一模块内)数据对象(变量或函数)只能有一个定义,但它可以有多个extern声明。定义是一种特殊的声明,它负责创建一个对象并分配空间;声明只是简单地说明这个数据对象在其他文件里边被定义了,你不需要再定义,可以直接使用这个数据。它们的本质区别是:是否分配内存空间,定义需要分配空间,声明不需要。

2、为什么要明确区分声明和定义,他们有什么用处?

       声明相当于普通的声明,它所说明的并非自身,而是其他地方创建的对象;定义相当于特殊的声明,他为对象分配内存。这里以多个文件的模块为例:文件A中定义了数据a,文件B中定义了数据b,如果文件B想用到数据a,则需要在B中对a进行声明才可以使用数据a,如果不声明数据a,则编译器会报错“数据a没有定义”。

       下面讲一下内部原理,这就涉及到编译原理的知识,以linux为例,编译器对文件的转换过程为:预处理、编译、汇编、链接(详见文章:【1.2】系统漫游——“程序”被其他程序翻译成不同格式)。以文件A、B为例,当“汇编”完成后每个源文件(*.c)会生成对应目标文件(*.o),文件B中对数据a的引用并没有转换成具体地址(因为此时编译器不知道a到底在哪定义的),而是把这个“异常”存到“符号表”中,当进行“链接”时从各个目标文件中寻找a的定义,这里能从A中找到,则所有关于数据a的引用都转换成具体地址,如果没找到则报错“数据a没有定义”。

定义语句:int test  = 0;

       这是test定义,如果在函数之外就是“外部变量”。如果外部变量没有初始化,则运行时系统默认为0(存在于bss段中),但我们仍不能指望编译器帮我们初始化,所以对于外部变量无论何值都要手动初始化。在函数内部就是“内部变量”,如果内部变量没有初始化,则其值是随机的(存在于栈中),所以内部变量也要手动初始化,尤其是指针变量,否则后果很严重,老板很生气!

声明语句:extern int test ;

      这是test声明,显式地说明test的存储空间在程序其地方分配的,这个声明就是对外部变量a的引用。这两个语句是配套的,可以在一个文件中,也可以在不同文件中,声明可以有多个嘛!

       如果有2个或多个相同的外部对象定义,则结果是未定义的,一般情况系统会报错,因为链接时会找到多个地址空间,系统会晕的。严格说来,外部变量只允许定义一次,严禁多次定义。(其实,这句话是不全面的)

3、在一个大型系统中多人协同开发,如何保证不会出现命名冲突?

      首先要有一套完整有效的变量命名规则,其次要利用static关键字,static可以改变外部变量的链接属性,制外部变量的作用域在一个源文件内,对于其他源文件该变量不可见,也就是明确告诉编译器在链接时static修饰的外部变量不能被其他文件使用。如果static修饰函数名,则效果类似。所以,对于外部变量和函数如果仅在本文件有效,则都应该加上static,最大限度避免命名冲突。对比:如果static修饰内部变量,则改变内部变量的生存期,内部变量在函数第一次运行时就分配好空间,当函数退出该变量数据仍然保留,以备下次使用。

4、使用对象时,要注意哪些与声明定义相关的内容?

      首先对于每个对象包括函数和变量,在使用之前必须先进行声明;声明的格式与实际定义格式要严格一致,否则错误状态未定义,可自行试验;因为定义也是一种类型的声明,所以如果在使用之前进行了定义,相当于进行了声明。

对于严格一致,哪怕是指针和数组也不能混淆。例如下面代码是有问题的:

char test[] = "I am the best one"; //文件A中

extern char *test; //文件B中

       第一个定义中指明test是一个字符数组的名称,test的类型是“字符数组”,而不是“字符指针”,尽管引用test值得到的是数组首元素的地址。第二个声明中指明test是一个指针,本质是一个变量,它俩对存储空间的利用方式是不同的。而且数组名类似于一个常量指针,数组名test本身的值是不能更改的,但是对于指针test的值可以随意修改。即便是某种巧合,这么做出现了争取的结果,但是用指针的方式就人为的隐藏掉了数组名称的常量特性,对于维护者也是一个陷阱。所以为了现在和将来,耶稣的归耶稣,上帝的归上帝吧,把他们分开。

5、头文件是否可以起到什么作用?

      一个好的方法是:只在头文件中对外部对象(数据和函数)进行统一声明。当其他模块要用到本模块的数据时,直接包含本模块的头文件就行;本模块也要包含自己的头文件(这么做是合法的),方便模块内部函数调用关系。