• 2004-06-18

    大头随笔(一)

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://www.blogbus.com/dreamhead-logs/227609.html

    学过C语言的人大概都知道,编写C语言的程序时候,除了程序文件,多半还要包括头文件,也就是我们印象中的.h文件。头文件里到底该放些什么呢?

    刚开始学编程的那会儿,就知道头文件里放的东西应该是在多个程序里用到的,那时对一个程序可以实现在多个文件里还没有什么感觉,于是按照自己的“复用逻辑”,把一大堆的东西都塞进了头文件。记得以前曾经编写一个DOS程序,因为程序里用到鼠标,于是写了一个头文件,把所有鼠标相关的函数放了进去。那个程序只有一个程序文件,于是程序运行得很好,我也没有多想,头文件里到底应该放些什么进去。

    真正让我开始考虑这个问题的是一次课程设计。那次准备编一个小游戏,用的是VC。由于VC下在一个project下管理多个程序文件比较容易,于是我采用了多程序文件,麻烦开始了。采用了和以前同样的思维习惯,我把一堆东西塞到了头文件中。编那个程序的时候,我把自己学来的防止重编译的手法用上了:
      #ifndef __XXX_H
      #define __XXX_H
      ……
      #endif

    曾为此得意洋洋,以为自己的手法很酷!

    但结果是,编译不通过。给出的提示:redefine?重定义?我没有啊?我不是防止重编译了吗?怎么还会出这种错?(当时,我根本没弄清楚重定义和重编译的区别)

    为此,挠头了好长时间,幸而在网上求得高人帮助顺利解决。后来才知道,关于这个问题,我曾在《C++编程思想》中看到过相关的论述,只因为字太少,没有引起自己足够的重视。于是再读一遍,豁然开朗。

    那C语言的头文件中到底该放些什么呢?

    说白了,很简单,就是声明(declaration),而在程序文件里放的应该是定义(define)。声明和定义的区别何在?就差一个空间分配,也就是说,声明是不分配空间的,而定义是要分配空间的。胡涂了吧!那我就以自己的错误为反面教材解释一下吧!

    我当时犯下的错误就是将许多定义放到了头文件中,比如在头文件中这么写:
      char ch;

    这就是一个定义。别喊,我知道这个叫声明语句,可它实际上就是一个定义,它之所以称为“声明”,我以为完全是历史上的冤假错案。

    让我们看一下C语言的编译过程,如果你在Unix/Linux下编写过程序,你一定知道Makefile。Makefile里经常会有这种东西:
    exe : a.o b.o
        gcc -o exe a.o b.o

    a.o : a.c
        gcc -c a.o a.c

    b.o : b.c
        gcc -c b.o b.c

    这就大概反映了一个编译过程,就是由a.c生成a.o,b.c生成b.o,然后由a.o和b.o一起来生成exe。

    现在回到我们的问题上,我们在头文件里定义了一个变量。而这个头文件有恰好被a.c和b.c所包含,也就是说两个文件都有这么一句。
      #include “header.h”(假设该文件中就包含上面出现的定义)

    在C的编译过程,预处理指令是在编译进行前先行处理的。Include预处理指令就是让header.h在a.c和b.c中被展开,也就是说,在预处理完的两个程序中,都包含了
      char ch;

    你不信的话,可以自己试一下。
      gcc –E a.c > a.i(a.i是自己起的名字)

    编译行动继续往下走,二者都生成了自己对应的.o文件,准备最后的连接了。这是编译器发现,在a.o中有一个变量叫ch,在b.o中也有一个变量叫ch,都要自己的空间,给a,b不同意,给b,a也不同意。于是它搞不定了,最后决定向你报告,结果就出现了一个连接错误:重定义。

    现在知道问题是怎么出来的了吧!知道了哪里有问题,就要想办法解决问题。

    我们的问题是在头文件中放置了定义,而在多个程序文件中包含了这个文件。实际上,我们之所以把这个变量放入头文件,就是因为我们可能在多个地方用到它,这也就是我们对头文件的最基本的认识。我又要多个地方用到,又不能在头文件中定义,那该如何解决呢?

    声明是最好的回答。

    我知道,从刚才你就想骂我,因为,我把声明语句说成了定义,那声明跑哪去了?真正的声明在C语言中应该这么写:
      extern char ch;

    都是初学C语言时用了谭老师的那本C语言教材的错,我们都忽略了extern,就知道了个“外部的”,具体怎么用都不清楚。(别废话,说正题!)

    这样,我们就声明了一个变量,这才是真正的“声明”。

    声明的作用就是告诉计算机介绍一个名字。编译器看到它,就知道有个变量叫ch,但并不为它分配空间,当然,如果真正用到了ch,还是要给它分配空间的,在哪分配?都说不能在头文件了,当然只能在程序文件里了。通常一个实现文件对应着一个目标文件,而生成可执行文件的时候,目标文件们是多对一,于是就不可能出现多个定义了。

    上面基本上都是在说变量,其实对于函数也存在着同样的问题,说过了变量问题,函数问题就简单了,你只要知道,如何声明、如何定义就够了。我们通常写函数就是定义,那声明呢?就是我们常说的函数原型。

    如果有兴趣,你可以打开C语言一个库文件看看,现在知道为什么里面基本上都是函数原型了吧!

    当然,事事没有绝对,我并没有说头文件中绝对不能放“定义”,放也可以,没人会反对你,前提是,你确实知道自己在做什么。

    再解释一下,前面提到过的防止重编译的手法。

    相信只要写得正规点的头文件中,在文件的开头结尾,你会看到这样的东西:
      #ifndef __XXX_H
      #define __XXX_H
      ……
      #endif

    为什么这么写呢?

    先来看看,上面的语意吧!

    如果没有定义__XXX_H,就定义它。

    文件编译的时候,如果同一个文件被多次包含,第一次遇到这个语句的时候,它显然还没有定义__XXX_H,于是定义它,继续下面的编译。后面再遇到的时候,因为已经定义过__XXX_H了,就直接跳到#endif。中间的东西就不会被再次编译。

    你或许想问,我为什么要在一个文件中包含多个头文件,你通常是不会这么干,但你知道你包含的头文件都干了些什么吗?

    我的意思是,有可能有这种情况:

    你写了两个头文件a.h和b.h,而b.h中包含了a.h。

    当你编程的时候,如果同时用到a.h和b.h,所以就这么写了:
      #include “a.h”
      #include “b.h”

    上面已经说过,include指令会在当前位置将程序展开,展开b.h的时候,a.h需要在b.h里展开,于是,a.h被展开两次。

    你的本意并不是把a.h处理两次,而实际上如果你不处理一下的话,它就会被处理两次。这种情况在使用标准库的时候会变得非常频繁,因为你通常不会注意每个头文件中还包含了那些文件。

    防止重编译的最大的好处是省时,当然,你若不在乎那点时间,别人也没办法:)

    如果你的程序还没有解决好声明与定义的问题,把定义写进了头文件,又忘了防止重编译,哈哈,两个麻烦就会一起找到你,具体的情形,自己分析去吧!

    参考:
    《C++编程思想》(第一版)第2章,2.1节

    分享到:

    历史上的今天:

    Hello, Modernizr 2011-06-18
    苍天作弄 2008-06-18
    引用地址:

    评论

  • 我用C,在三个文件里有char ch;没什么问题,不解.
  • 很多人,特别是善于被驯服的人,会认为服从约定是更简单的。