初级程序员面试不靠谱指南(四)

三、改变出生的static和extern

1.程序是怎样炼成的?IDE的发展带来的一个好处就是在写程序更多的可以关注在写这件事情上了,比如在vc里面,当你酣畅淋漓的写完一段程序之后,点击一下debug就可以享受到程序运行起来的好感,但是IDE带来的一个负面影响就是对ASCII码写成的代码如何转换成二进制的计算机程序掩盖的越来越多,虽然说不懂这些在绝大多数情况下对写程序也没什么影响,但是面试的时候如果你能有这方面的知识那绝对是加很多分的,而且现在来说很多时候面试这也成了一个基础的内容,特别是面试linux下开发的时候。先用一副图说明下点击debug/run以后都大概干了什么,我找了很多图,确实难找,这玩意儿,最后找了一个差不多的但是是linux下的过程的先用用:

     这个图中看起来步骤也挺多的,如果实在是记不住这么多的过程,你可以忽略源文件、预处理、纯C、编译器、汇编器这些玩意儿,就记住源文件在编译以后会先转换成为.o(windows下是.obj),这些被称为目标文件,其实就是一个个的编译单元,也就是在(一)里面提到的translation unit,然后这些编译单元和一些库文件(windows下是.lib/.dll)进行连接产生可执行文件。

     这个整个过程可以这样理解,某位领导需要在某个大会上发言,他自己首先要写一些自己想发言的关键点,然后交给秘书写成完整可用的发言稿,领导是高级职位(高级语言),他可以很简略的列出一些要点和开会需要的文件,然后交给秘书,秘书首先要看懂这些简略的要点并寻找这些文件(预处理),在这些都收集妥了的时候,秘书开始根据这些东西写成正式的发言稿,首先他要将领导列出的要点一个个补充完整(汇编语言,汇编器),尽量让语言更通俗易懂,并按照单独整理出每个要点(目标文件),然后再去寻找需要参考的文件(库文件),将这些要点和(库文件)相结合,整理写成可以用来发言的正式的发言稿(可执行文件)。

    说了这么多,展示下vc产生的obj文件更直观一点,我建了一个纯c的工程(cpp,c这个无所谓,为了切入更关键的部分,采用c展示),在随意写一些测试内容之后,点击debug,转到相应的文件夹,可以看到.obj文件。

   

     为了初窥下obj里面到底有啥,我们采用记事本打开这个obj文件看看,可以看到内容如下:

          虽然不能完全懂这里面有啥,但是可以大概从这些东西看到都是一些编译信息啥的,比如里面的_main,这就是main函数,这一次不是要叙述IDE的研究,这些只是为了后面说明static和extern之间做一些准备工作。

2.声明与定义声明和定义在英语中有点接近(declaration和definition),而且这个两个词又经常一起出现,搞得我最开始看英文书籍的时候到后面就怀疑自己哪个是哪个了,这两个概念在程序之中有巨大的区别,也是很多面试的时候会涉及的地方。首先得弄明白啥是声明,啥是定义。从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。有些书本里会说,int a;这是声明,int b=1;这是定义,但是如果你去main里面试试发现并不是这样的,因为a和b都被分配了内存。在这里要说的是有的书里面把第一个称为定义性声明,它是会被分配内存并且有一个随机分配的垃圾值,而把真正的纯声明叫做引用性声明,必须用extern。所以有的时候,为了叙述方便,把建立存储空间的声明称为定义,而把不需要建立存储空间的声明称为声明。显然这里指的声明是狭义的,即非定义性声明。最终归结下来的核心问题就是声明是不会分配内存的,而定义需要,还有一个需要记住的就是对同一个变量或函数的声明可以有多次,而定义只能有一次!

    声明和定义和连接是分不开,有时候程序出现的连接错误就是和声明与定义有关系,这个后面具体进行描述。

3.由内而外的extern。extern可以改变变量的连接属性,可以将一个变量的连接属性由internal改变成为external,也就是说由extern标示的变量不仅可以在本模块中被使用,在该工程的所有模块中都可以被使用,不会出现找不到的错误。而且用extern标示的一定是一个声明,没有定义。下面结合前面的.obj文件来对这句话进行解释。

   在上面的mainfile.h中添加一个全局的变量如下:

#ifndef MAINFILE_H
#define MAINFILE_H

extern char *roger_str; //仅仅是做了声明,没有定义。

#endif

   然后在mainfile中包含这个头文件并且给这个字符串赋值(这时才进行了定义),大概类似如下所示,这里我们不需要main函数,只是为了掩饰连接属性。编译一下。

#include "MainFile.h"

char* roger_str="Roger Zhu";//定义

void MainFileFun()
{
    printf("%s\r\n",roger_str);
}

    找到debug文件夹里面相应的mainfile.obj文件,用记事本打开,可以看到里面有Roger Zhu,roger_str之类的内容。

    下面我们再在这个工程内添加一个新的文件,叫做LinkFile.c,在这个文件中,我们仅仅引用MainFile.h,但同时也使用roger_str,但我们没有再次定义这个变量,编译一下能够通过,运行也不会报连接错误,只是会说找不到main,因为我们根本没有定义main。其内容如下所示:

#include "MainFile.h"
#include <stdio.h>

void LinkFileFun()
{
	printf("%s\r\n",roger_str);
}

    然后找到LinkFile.obj,查看一下其内容,发现其中有roger_str的内容,但是没有Roger Zhu字符串,但是在该文件中声明一个main函数再调用LinkFileFun函数,会发现,输出的是正确的Roger Zhu。在obj文件中只有一份的原因是因为这是一个全局变量,在内存中只能有一个。

  

    如果你在LinkFile中同样也定义一次全局变量roger_str会发生什么呢?程序可以通过编译,但是会曝出类似如下的连接错误: LinkFile.obj : error LNK2005: _roger_str already defined in MainFile.obj。这也和第二点里面的对同一个变量可以声明多次但是只能定义一次的阐述进行了验证。

    还有需要注意一点的是,如果使用的是extern char *roger_str=“Roger Zhu”;这相当于没有使用过extern,所以不小心也会造成连接错误,关于这一点你可以自己试一下。


4.由外而内的static。static也是一个经常被考的内容,最常见的一个答案是static变量时在栈上进行分配的,它具有全局作用域(其实我觉得这个说法不恰当,应该是模块作用域或者文件作用域比较好),可以作为一个全局计数器,其原因是因为static是在全局数据区进行分配的,所以其可以保持这样的特性。比如像下面这样:

int LinkFileFun2()
{
   static int i=0;
   i++;
   return i;
}

void main()
{
   int count=0;
   int k=0;
   for(;k<10;k++) count=LinkFileFun2();
   printf("%d\r\n",count);
}

     static另外的一个作用就是将连接属性从external改变成internal,static声明的变量只能在自己的编译单元中是可见的,同样采用3里面的例子,不同的是将头文件中的extern改变成为static,可以看到同样可以通过编译和运行,但是,如果在LinkFile.c中调用LinkFileFunc会发现,输出的字符串是null,也就是说该编译单元并不能找到另外一个编译单元的roger_str,为了显示在.obj文件中的不同,我们在LinkFileFunc中也定义一个roger_str为Roger Zhu 2,进行编译,运行,不会再报连接错误而且可以看到输出的是Roger Zhu 2,mainfile.obj并没有什么不同,但是linkfile.obj会多出Roger Zhu 2这样的字符串,这是因为static具有模块作用域,它不和其余的模块发生关系,所包含的变量也仅仅是自己模块中可见。如果你要是在MainFile中输出roger_str,会发现输出结果是Roger Zhu,也再一次证明了作用域是模块的。

5.extern的static变量的位置。是不是extern和static变量只能在头文件中声明而在源文件中定义呢?这个看起来不经意的问题其实也蕴含了很多的知识点的,比如说extern标记的变量,我们在头文件中对roger_str进行定义而不是声明,也就是用在头文件全局的地方使用 char *roger_str=“Roger Zhu”;注释掉MainFile.c中的对roger_str的定义。进行编译,运行,这是会出现连接错误LinkFile.obj : error LNK2005: _roger_str already defined in MainFile.obj,因为你把全局变量roger_str在头文件中进行定义了以后 ,MainFile.c这个模块包含了头文件,根据预编译的原理,头文件会在这里被展开,于是定义了一次roger_str,同理LinkFile.c又包含了一次头文件,又定义了一次roger_str,这和2里面所描述的问题是相同的。如果想要改正这个错误,只有在LinkFile.c不要包含.h文件,那么编译器怎么知道这个roger_str是怎么来的呢?如果什么也不加,很明显只能在本模块中可见,这时要把internal连接属性,转换为external的,就需要使用extern,也就是需要在LinkFile.c的全局处声明extern char *roger_str,这时再编译程序回去其他编译单元寻找这个变量,编译连接都可以通过。但这里出现了一个问题,如果不能包含头文件意味着头文件里面的所有内容想使用的话都需要用extern进行标记,这个对于编程和维护都是极为不便的,所以,带有extern的变量应该在头文件中声明,在源文件中进行定义。这样就可以避免上述问题。

     关于static,尝试跟踪调试上面的代码,结果你发现在两个源文件中的roger_str的内存地址是相同的,这难道说明这是一个全局的作用变量?因为如果地址是相同的,那么无论你怎么修改,肯定修改的是一个变量,但是这和static本身的意义不一样啊?其实这是编译器为了优化做的一个调整,当它发现在两个模块中,对于两个static变量并没有做写的操作,那么它会将其安排在一个内存,这样更节省内存。想证明这个很简单,只要在其中的一个模块static变量进行调整,这时候再跟踪内存会发现两个的内存地址并不相同。所以在使用static的时候,为了避免造成这样的干扰,一般放在源文件中,这样可以避免给其他模块的static全局变量造成不必要的干扰。

     还有一些是关于类中使用extern和static的内容,我将在后面详述,还有就是和const的结合,已经在(一)中进行了叙述,这里就不累述了。

     另外:感谢 zjwzcnjsy的评论,“查看符号信息windows可以使用dumpbin.exe查看,linux的可以使用objdump查看。”有兴趣的话你可以去搜一下这两个tool的用法,这是正规的做法。这篇要这样写,也是为了能够更加少的涉及到其他玩意儿,另外,也是我自己的一点点“哦,这玩意儿里面原来是这样”的装逼感~哈哈

 

posted on 2013-05-29 20:58  一心一怿  阅读(1519)  评论(3编辑  收藏  举报

导航