转:从编译链接过程解析static函数的用法
关于static函数的用法
就像我们熟知的那样,变量可以分全局的和局部的,函数也可以分全局的和局部的。
比如说,在一个工程的common.h中定义了一个全局变量 int test;那么在整个工程的作用范围内,该变量都是存在的,在编译的时候会将其保存在整个工程全局的变量表中,文件(.h或.cpp)只要使用声明extern int test;就可使用该变量,而不用包含该变量的头文件common.h,因为该变量的作用域是全局的。
和全局变量一样,大多数的非类成员函数基本上都定义为全局的。我们可以分两种情况讨论,第一种情况,void func();被声明在commom.h中,实现体在common.cpp中。这种情况和全局变量的情况一样,整个工程内的任何程序都可以通过extern voud func();来使用该函数,而不需要包含头文件common.h。当然了,还可以通过包含头文件的方式使用func();。工程内的任何文件都可以包含common.h而不会出现名字冲突问题,因为common.h中只包含了函数的声明而没有实现体,任何包含了common.h的文件因此也就只包含了函数的声明,因此编译是没有问题的。至于函数的实现体,会在链接的时候自动到对应的obj(由对应的cpp生成)文件中寻找。
当把一个全局函数的声明和实现都放在common.h中时,如果有2个以上文件都包含了common.h,在编译的时候就无法通过,因为每包含一次common.h,就会在全局空间内保存一个func()实现体(最终会保存在obj固定的全局区域),如此一来,就会有多个个func()实现体保存在全局空间中(分别保存在多个obj中,在链接的时候会整合成一个总的exe),从而导致命名冲突。所以,一般都会使用class来封装函数,可以避免很多的命名冲突问题。
还有一种方法可以避免全局函数的命名冲突,可以将函数的声明和实现都放在common.h中,但是要加上static声明,即static void func(){};加上static就表明该函数只在本文件(common.h)中可用,在包含该头文件的obj全局空间内不会保存func()的实现体,任何文件要想使用func(),只能通过包含头文件(或者说是包含该函数的作用域)的方式来使用它,不能通过extern void func();来直接使用,因为此时的func()以不在整个工程全局空间的控制范围内。
还有一种情况,如果将函数的声明放在头文件中,将函数的实现放在对应的cpp文件中,并且将实现体加上了static声明,那么此时的static没有太大作用,因为既然将函数体放在了cpp中,别人一般不会包含你的cpp文件,也就不存在命名冲突问题了,除非有几个文件非要包含你的cpp文件不可,那么此时的static就派上用场了。
其实只要对编译的过程理解了,这些问题都很容易搞懂。下面简单讲一下编译的原理。
程序写好之后会先进行编译,在编译阶段,编译器会为每个cpp文件生产一个.obj文件,该文件就保存了每个文件中的全局变量或者全局函数的标识符,如果一个文件使用了另外一个文件中的全局函数并且没有包含其头文件,在编译的时候,编译器首先会寻找是否使用了extern声明,如果找到了,此时编译器就放心了,它知道这个函数是在别的地方定义了,在链接的时候会自动找到的,所以编译器就编译通过了。
编译完之后就会对每个.obj文件进行整合链接,最终生成可执行的exe文件。在链接阶段,此时的所有相关的.obj文件都被放到了整个工程的全局空间内,链接器会检查所有的.obj文件是否存在全局变量或者函数的命名冲突问题,如果多个obj文件都包含了某个全局函数的实现体,此时编译器就会报命名冲突错误,因为每个obj包含的不是函数的声明,而是结结实实的函数体,大家都知道,函数可以被声明多次,但是函数体只能定义一次,那么此时的函数体被每个obj都定义了一次,肯定会导致命名冲突问题。
解决的办法上面也说了,只能将函数体移到cpp文件中,头文件中只包含函数的声明,由此只有该cpp文件对应的obj文件中存在函数体,那么链接的时候果断不会有问题了;或者将函数体声明为static,此时的每个obj文件中就不会有该函数的定义了,该函数在编译阶段已经被整合到obj代码中了,不会出现在obj的全局空间中,所以在链接的时候不会有问题。
链接器的作用就是将每个单独的obj文件按照代码中的依赖关系进行整合(其中就包括对全局空间变量或者函数进行整合),最终生成exe文件。
补充一下,工程中的每一个cpp文件都会生成一个对应的obj文件。 关于static基本的用法就这么多,以后如有更深入的研究再继续补充