第一桶 从C到C++ 第七碗 陈老C演迭代开发 潘小P学渐进编程(之二)

     “在下一个版本里面我们不进行任何编码工作。”老C道。
     “哦?那么我们要做些什么?”小P不解道。
     “我们来重新组织一下我们的工程。”老C解释,“顺便再讨论一下文件结构的问题。”老C摸摸下巴,“你知道C语言的文件分为头文件和源文件吗?”
     “那是当然,就是.h和.c文件呗。”小P有些不以为然。
     “那你知道什么是translation unit 吗?”老C接着问。
     “唔……那个东东是什么?”小P疑惑道。
     “那么你可以区分清楚什么是declaration,什么是definition吗?”老C接着问。
     “一个是声明,一个是定义……”
     “……我是问这两个概念在C语言中的含义……”老C囧。
     “哦……”
     “看来我们需要进行一些基本的扫盲工作。”老C道,他让小P从教研室的角落拉出白板,在上面写下如下代码。

int a;
extern int a;
extern int a = 5;

int Func (void);

int Func(void)
{
}


     “看看这些声明有什么不同?”老C问道。
     “槑……”小P做沉思状,“好像……这个……”
     “呵呵,这些代码的不同之处在于是否分配存储空间。”老C解释道,“在我们对declaration和definition进行仔细讲解前,我们先看看如 下概念。”老C又在白板上写下如下文字,一边写一边说:“你要记住,概念是最重要的,技巧是微不足道的,因为一旦你掌握了概念,看懂技巧是水到渠成的事 情,甚至自己也可以根据概念发明出一些小技巧出来……”

source file(preprocessing file)
preprocessing translation unit
translation unit

     “很遗憾,一般概念都伴随一些晦涩的术语,而一般人则视术语如猛虎,认为术语只属于学术,在一般的实际开发中没有什么用处——然而我们组织逻辑的最基本单 位就是词汇,如果我们在思考、交流的时候连基本的词汇也无法理解,那么就根本谈不上什么思考和交流了——毕竟大多数人类还是靠语言进行思考的,除了少数天 才使用图形和符号进行思考;同时术语也简化了我们交流的复杂度,比如我说PID,只要你理解了什么是PID这个概念,那么我就不用画出框图并解释这个带有 比例、积分和微分环节的反馈系统,这样在行业内工作的人们可以方便的使用术语进行交流……罗嗦了这么多,只是希望你不要对这些晦涩的术语带有排斥的心理, 而是要慢慢习惯它们,接受它们……虽然有些术语名字起得的确有些脑残……”老C喋喋不休的说道。
     “呵呵……我明白了。”小P应道。
     “所谓source file,也可以说preprocessing file,就是程序中的源文件,一般就是我们写的.c文件,这个文件的后缀一般由编译器规定,但是行业内约定俗成的规定为.c,”老C指着source file开始解释,“而preprocessing translation unit指的是某个源文件以及在它的前面使用#include预处理指令包含的头文件和源文件。”说罢老C在白板上画了几个框框。
    
     “在这里,a.c就是preprocesing file,而a.c,a.h和b.c合起来称为preprocessing translation unit。”老C指着他画的框框,“哦,对了……这些框框表示文件……”
     “那么什么叫translation unit呢?”小P问。
     “就是经过preprocessing 后的preprocessing translation unit,你可以理解为a.c经过预处理后,在头部将a.h和b.c内容展开后的某个中间件……”老C解释道,“这样不正规的解释可以帮助你更快的理解……”
     “这些与declaration,definition有什么关系?”
     “当然有,在解释什么是declaration和definition时,我们需要用到translation unit的概念。”老C答道,“因为translation unit中包含有external definition……”
     “等等,什么是external definition?”小P追问。
     “哦,为了解释这些概念,我们先看看最初的那些例子,然后再熟悉一下这些术语。”老C指了指刚才在白板上随意写下的声明示例,然后又在旁边写下了以下文字。

declaration
definition

     “所谓definition者,引起内存分配的declaration也……”老C开始转文……
     “囧……请说地球话,反对火星语……”小P抗议。
     “呵呵,简单的说,declaration说明了一组标识符的含义和属性(A declaration specifies the interpretation and attributes of a set of identifiers),而definition就是引起内存分配的那些declaration——详细来说,如果对于对象,导致了内存分配的动作;对 于函数,包含了函数体;对于枚举常量或typedef名称,就是declaration本身。 ”
     “哦……有些晕……”小P有些不明白。
     “好吧,简单的说,definition是一些特殊的declaration,如果在声明一个对象时,引起了存储空间配分配于该对象,那么这个 declaration就是definition;如果在声明一个函数时,这个声明包含了函数体,那么这个declaration就是 definition;如果是枚举常量和typedef,那么这些declaration本身就是definition。这下可明白?”老C耐心的解释起 来。
     “嗯,就是说definition其实就是一些特殊的declaration,是吧?”小P有些理解,“但是在C里面怎么会有对象啊?”
     “哦,基本上可以这样理解。”老C回答小P的前一个问题,然后又开始回答下一个,“所谓对象,不过是统称,比如int a,a就是int的一个对象,如果你不习惯使用对象这个术语,我们可以用object来代替。”然后他指着上面的代码例子,“你来写写哪些是 declaration,哪些是definition吧。”
     “如果图省事,这些全部都是declaration……”小P自作聪明。
     “囧……对是对,可是……我说你就不能严肃一些吗?”
     “呵呵,开玩笑的,何必当真呢?”小P一边说,一边在旁边写下注释。

int a;              // definition
extern int a;       // declaration
extern int a = 5;   // ?

int Func (void);    // declaration

int Func(void)      // definition
{
}

     “有一个不知道是什么,所以我画了问号。”小P指着代码说道。
     “没有关系,我们先不管它到底是什么,我们再来看看其它几个概念。”老C没有着急给出小P答案,而是在白板上的一块空白地方又写下如下文字。

external linkage
internal linkage
none linkage

     “一个标识符(identifier),如果在不同的scope中被声明,或者在同一个scope中被多次声明,它总会被正确的指向同一个object或 者function,这一过程叫做linkage。”老C解释,“比如我有两个文件,a.c和b.c,一个函数FuncA()在a.c中定义,如果你想在 b.c中的FuncB()函数中使用函数FuncA(),在b.c中你可以这样写……”老C又开始在白板上涂抹。

a.c:
void FuncA(void)
{
}


b.c:
extern void FuncA (void);

void FuncB(void)
{
    FuncA();
}

     “喏,你只要在b.c中声明这个函数就可以了,你看,函数被声明了两次——注意定义是声明的特例——如果你的两个文件被正确的编链,那么C语言规范保证可 以找到正确的FuncA()。”老C在白板上指指点点,“同时要注意,这里说的是声明多次,可没有说定义多次,如果你把函数定义了超过一次,那么编链的时 候会报错的……”老C咽了一口唾沫,“这个就是external linkage的一个例子。而且根据C ISO/IEC 9899规范,我们甚至不用在b.c中FuncA()函数的声明前加exern,编链器一样可以正确的找到FuncA()的定义。”
     “哦?是吗?那么我到要试试……”小P有些好奇。
     “嗯,你等等再试。我再来说说internal linkage。”老C开始更改他在白板上写下的代码,“如果我在FuncA()的声明前加上static,那么其它的translation unit无论如何无法找到这个函数。”

a.c:
static void FuncA(void)
{
}

     “如果这个时候我们的代码还是b.c的样子,就会产生一个编链错误,告诉我们无法解析FuncA这个标识符。”老C道,“这个就是一个internal linkage的例子。”
     “那么none linkage呢?”小P追问。
     “……自己看看 ISO/IEC 9899规范吧……”老C觉得小P自己也得花些功夫了,“下面我们就来详细看看external definitions。这里之所以讲external,是因为这些definitons都在函数外部……什么?你不知道可以在函数内部定义和声明函 数?……这样也好,这是C语言的怪癖……我们不管那么多,先看看又有哪些概念需要了解的……”老C挠挠头,“哦,可能之前我们得先了解一下什么是 scope。”
     “scope?就是作用域吧?”小P问。
     “嘶……”老C抽了一口气,“我不知道怎么解释,在我理解作用域还包括了name spaces的概念,因此我更喜欢使用scope这个术语而不是很具有内涵的作用域这个术语。”
     “C语言也有name spaces吗?”小P不解。
     “有啊……自己去看吧。”老C不想多费口舌,“所谓scope,又分为以下几种……”他又在白板上涂抹起来。

function scope
file scope
block scope
function prototype scope

     “呵呵,”老C笑道,“file scope最好解释,如果一个标识符没有被声明到其它三个scope当中,那么它的scope就是file scope……至于其它三个scope的含义,我建议你……”
     “……去看规范……”小P囧。
     “哈哈……”老C突然觉得这是一个少费口舌的好办法,“其实简单的理解,file scope就是我们一般声明的全局变量和函数,因为规范是很严肃的东西,所以才写得那么罗嗦和晦涩,因为总有人喜欢找一些特殊的情况以显示自己对规则的藐 视,所以规范不得不那么面面俱到……好啦好啦,我也是胡说的,呵呵。你只要知道我们说的external definitions是在file scope中的定义就好了。在进入我们正式的议题前,我再磨蹭一下。”说完老C在白板上写下如下文字。

storage-class specifiers:
typedef
extern
static
auto
register

     “我们主要讨论extern和static,但是其它的你也要了解一下,所以……”
     “……看规范……”
     “呵呵,好了好了,我们现在来说说external definitions吧。”老C觉得小P真是善解人意啊,“这里你只要了解一些简单的规则就可以了。第一,function的规则与object不同; 第二,如果你没有将function或者object声明为static,那么它们自动的成为extern;第三,object的规则比较复杂一些,这 样,我来说你来写……”老C揉揉手,想偷懒一下,“这样你印象比较深刻……”
     “囧……好吧……”小P不情愿的回应,拿起彩笔一边听老C讲,一边在白板上写下如下内容。

1. 声明一个object,若它的scope是file scope,且它被初始化,那么它的声明就是一个external definition.
2. 声明一个object,若它的scope是file scope,且它没有被初始化,且它没有storage-class specifier,或者它的storage-class specifier是static,则此声明就被命名为tentative definition。如果一个translation unit中有一个或多个关于此一标识符的tentative definition,并且在此translation unit中没有关于此标识符的external definition,那么此标识符会被当作此translation unit中的一个file scope的一个declaration,其作用在整个file scope中,且有一个0初始化值。
3. 如果一个标识符的声明是tentative definition,并且有external linkage,则此被声明的类型不能是不完整的类型。
4. 如果一个变量其声明前带有extern storage-class specifier,则其是否是exernal或internal linkage要视其前面是否有在scope中可见的此相同变量的声明,如果有,则其跟随前一相同变量的声明,否则就是exernal linkage。
4. 同一个标识在一个translation unit当中即表现exernal linkage,又表现internal linkage,则其行为未定义。

     “唔……不是很好理解。”小P抱怨。
     “呵呵,我们来看几个例子好了。这些标识符都被声明在一个translation unit当中。”老C说道,“但是我想提醒一下,external definition与external linkage的external含义完全不同,不要搞混淆了。”然后他在小P写的话下面又增加了一组代码。

int i1 = 1;              // definition, external linkage
static int i2 = 2;       // definition, internal linkage
extern int i3 = 3;       // definition, external linkage
int i4;                  // tentative definition, external linkage
static int i5;           // tentative definition, internal linkage

int i1;                  // valid tentative definition, refers to previous
int i2;                  // undefined, linkage disagreement
int i3;                  // valid tentative definition, refers to pre vious
int i4;                  // valid tentative definition, refers to pre vious
int i5;                  // undefined, linkage disagreement

extern int i1;           // refers to previous, whose linkage is external
extern int i2;           // refers to previous, whose linkage is internal
extern int i3;           // refers to previous, whose linkage is external
extern int i4;           // refers to previous, whose linkage is external
extern int i5;           // refers to previous, whose linkage is internal

int i[];                 // the array i still has incomplete type, the implicit initializer causes it to have one element, which is set to
                         // zero on program startup.


     “我想提醒一下,这里只是做说明,在实际编码时我们可不要这么写。”老C强调,“那么现在你是否明白extern int a = 5 是declaration还是definition了吗?”
     小P仔细看了看老C写的示例代码,又把自己写的话念了几遍,说道:“嗯,这样看来这个语句应当是具有exernal linkage 的exernal definition。”
     “呵呵,不错,我再总结一下。你可以简单的理解为如果一个变量在声明时被初始化,那么这个声明就成为一个定义,而与storage-class specifier无关,如果其前面有storage-class specifier,那么只能说明其是否是internal linkage或external linkage;如果一个变量在声明时前面带有extern,且没有被初始化,那么它就是一个declaration,且其是否是external linkage要视前面是否有其它此相同变量的声明,如果有,则其跟随前面这一相同变量的声明,如果没有,则其为external linkage;声明总是倾向于exernal linkage,如果你不声明static;根据规则不能出现既是internal linkage又是external linkage的情况,否则其行为无定义。”老C觉得十分渴,找到茶杯大大的喝了一口水。    

     “好吧,我承认很复杂……可是这个和我们讨论的内容有什么关系呢?”小P有些云里雾里。
     “呵呵,只是一些理论基础。”老C答道,“根据规则我们可以使用各种各样的组合来管理我们的代码,设计我们的文件组织,但是在实际开发中自然有一定的规 则。如果你按照这种规则进行编码,那么基本上不用关注这些标准的细节,当然,出现错误的时候你还是要根据标准来查找可能出错的地方。”
     “哦?是吗?说来听听?”小P问。
     “好吧。”老C答道。“我们以前讨论过,我们人类对于复杂事物的处理能力是有限的,为了解决这些复杂问题,我们总是希望把它们分解成我们可以理解的规模。 通过信息隐藏的方式,我们可以将一个很大规模的问题分解分解再分解,直到我们的智力可以管理这些问题。而使用文件对代码进行划分,可以有效的帮助我们对问 题的规模进行控制——眼不见,心不乱嘛。”
     “哦,具体怎么做呢?能不能举个简单的例子?”小P问道。
     “可以啊。”老C答道,然后指挥小P将白板擦干净,又在上面开始比划,“一个比较简单的问题,求解一个方程。”他在白板上写下如下文字。

ax2 + bx + c = 0

     “我们可以这样来分解问题。”老解释,“设计一个函数,其返回值为实根的个数,0为没有实根,1为有两个相等的实根,2为有两个不等实根,3为有无穷多解,-1为无解。实根作为出口参数,设计为函数接口的一部分。我们把这个函数放到solve.c文件中。”

solve.c:
int Solve (float a, float b, float c, float* root1, float* root2);

int Solve(float a, float b, float c, float* root1, float* root2)
{
}

     “这样如果我们在某个项目中需要解一个二元一次方程,那么我们,比如在main.c中,就可以很简单的这样写。”老C接着在白板其它地方写道。

main.c:
extern int Solve (float a, float b, float c, float* root1, float* root2);

int main()
{
    float root1, root2;

    ...
    Solv(1, -1, 1, &root1, &root2);
    ...
}

     “只要我们将solve.c正确的添加到我们的工程中就可以了。”老C道。
     “这样写有什么好处呢?”小P问。
     “好处嘛,最明显的是……复用,而且就算是我们要自己写Solve()函数,现在它也与main()函数分开,人为的将两个关系比较远的模块分开,这样可 以强制的控制代码的规模。”老C点点头,“如果我们将extern 语句放入一个名叫solve.h的文件中,那么就更方便了。”

solve.h:
#if !defined(SOLVE_H_)
#define
SOLVE_H_

extern int Solve (float a, float b, float c, float* root1, float* root2);

#endif /*
SOLVE_H_ */

main.c:
#include "solve.h"

int main()
{
   
float root1, root2;

    ...
    Solve(1, -1, 1, &root1, &root2);
    ...
}

     “这样的好处呢?”小P问。
     “简单,减少冗余。如果我们solve.c中有很多可以让其它文件使用的函数,这样就不用在其它文件头部写出很多的extern...的声明,而只用在solve.h中写一次,在其它文件中#include就可以了。”老C补充道,“偷懒,是程序员的美德……”
     “这里为什么要用.h文件呢?我用一个.c文件,在里面写入extern...的声明不行吗?”小P接着问。
     “……没有什么不行,但,不符合行规……而且如果你使用automake工具的话,可能配置起来要麻烦一些……总之不要在这些地方释放你多余的创造力,别 人怎么做的你就怎么做,这个是行业内的规矩……”老C有些郁闷,心想这真是一个多动的家伙啊,“而且最好在.h文件中只出现声明而不要出现定义,这样你在 编译的时候链接错误会少很多很多。”
     “为什么在solve.c文件的前面要先写一个int Solve (float a, float b, float c, float* root1, float* root2)?”小P指着白板问。
     “函数原型,这个就叫做function prototype。”老C解释,“当然你也可以不用写,但是根据行业内许多经验的总结,这样写总有好处,因为据说这样在编译的时候可以让编译器在函数调 用点做全面的类型检查。”老C指着solve.c下面的代码说,“其实这里又出现一处冗余,因为在solve.c和solve.h文件中,Solve() 函数被声明了两次,这样在Solve()函数接口被修改的时候,我们不得不修改两处地方,而这是我们很讨厌的事情。”
     “那么有什么解决方法呢?”小P问。
     “我们可以在solve.c中包含solve.h,这样就可以了。”老C说,“然后我们可以进行解决问题的细节工作。”他又在白板上比划起来。

solve.c:
#include "solve.h"
#include <math.h>

#define EPSILON    0.000001F

static float Solve1stOrder (float b, float c);
static float Delta (float a, float b, float c);
static float DoSolve (float a, float b, float sqrtDelta);

int Solve(
float a, float b, float c, float* root1, float* root2)
{
    int   rootNum;
    float delta;
    float sqrtDelta;

  
    /* If a is 0, then the formula becomes 1st order. */
    if ((a < EPSILON) && (a > -EPSILON))
    {
        if ((b < EPSILON) && (b > -EPSILON))
        {/* b is 0 */
            if ((c < EPSILON) && (c > -EPSILON))
            {/* If c is 0, the formula has
infinite roots. */
                rootNum = 3;
               
                return rootNum;
            }
            else
            {/* If c is not 0, the formula has no root. */
                rootNum = -1;
               
                return rootNum;
            }
        }
        else       
        {/* b is not 0 */
            rootNum = 1;           
           
*root1 = *root2 = Solve1stOrder(b, c);

           
return rootNum;     
        }      
    }

    delta = Delta(a, b, c);

    /* If delta < 0, the formula has no real root. */
    if (delta < 0)
    {
        rootNum = 0;
       
        return rootNum;
    }

    /* If delta is 0, the formula has two equal real roots. */
    if ((delta < EPSILON) && (delta > -EPSILON))
    {
        rootNum = 1;
        *root1 = *root2 = (-b) / (2 * a);
   
        return rootNum;
    }

    /* If delta > 0, the formula has two different real roots. */
    if (delta > 0)
    {
        rootNum = 2;
        sqrtDelta = sqrt(delta);
        *root1 = DoSolve(a, b, sqrtDelta);
        *root2 = DoSolve(a, b, -sqrtDelta);

        return rootNum;
    }
}


static
float Solve1stOrder(float b, float c)
{
    return (-c) / b;
}

static float Delta(float a, float b, float c)
{
    return b * b - 4 * a * c;
}

static float DoSolve(float a, float b, float sqrtDelta)
{
    return (-b + sqrtDelta) / (2 * a);
}

     “看,一些具体的解题过程我们并不想暴露给其它文件,所以使用static将其声明为internal linkage,这样就相当于隐藏了信息;而Solve()函数是我们希望暴露给其它文件的,所以使用extern将其声明为external linkage——这样以文件为单位,我们组织了一个程序的模块,并向其它模块提供了接口,以供其它模块使用。”看到小P还在看代码,老C接着解释道,“ 哦,EPSILON这里只是一个需要注意的小技巧,因为你不能比较两个float数值是否相等,只能比较它们的差是否小于一个很小的数值,来判断它们是否 相等……原因?……与浮点数在内存中的存放格式有关系。总之你认为浮点数的最后几位总是随机的就可以了。”
     “哦,这样我就明白了。”小P点点头,“那么这个solve.h中的条件编译是怎么回事?”
     “哦,这也是一些小技巧,用于防止头文件被重复的包含而可能导致的递归。你只要认为#include是将其所引用的文件原封不动的放到引用点就可以理解 了,”老C挠挠头,“比如a.c包含a.h和b.h,而a.h包含c.h,b.h也包含c.h,那么c.h的这些条件编译可以防止c.h在a.c中被包含 两次。你可以自己在#include的包含处将文件展开看看就明白了。”
     “是么?”小P在纸上画了几下,“哦,这样我就清楚了。呵呵。但是……有没有包含.c文件的情况呢?”小P又开始发挥想象力。
     “唔……有的……”老C挠挠头,“在某些需要裁剪和定制的项目中也许会根据某个.h文件中的条件编译来选择是否包含某些.c文件,但……这些工作也可以由 makefile来完成,而且感觉大多数的做法都是采用脚本+makefile完成的……无论怎么样,你现在先不要使用包含.c文件的做法,等熟悉了以后 我们再慢慢研究……”他搓搓手,“好吧,废话说了这么多的一大堆,我们也去休息休息睡午觉吧,下午3点到教研室,我们接着聊。”老C有些乏力的说。
     “呵呵,好啊好啊。”两人一边说一边向门口走去……

(继续等待v0.03……)

posted on 2009-02-04 16:42 Anderson 阅读(1957) 评论(3)  编辑 收藏 引用

评论

# re: 第一桶 从C到C++ 第七碗 陈老C演迭代开发 潘小P学渐进编程(之二)[未登录] 2009-02-04 20:58 ypp

嘿嘿,看完,非常有帮组,继续等待楼主的佳作。  回复  更多评论   

# re: 第一桶 从C到C++ 第七碗 陈老C演迭代开发 潘小P学渐进编程(之二) 2009-02-04 22:28 岳阳

小P真幸福啊。遇到了名师。  回复  更多评论   

# re: 第一桶 从C到C++ 第七碗 陈老C演迭代开发 潘小P学渐进编程(之二) 2009-02-05 02:03 imnobody

其实,俄已经等好几天了^^  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2009年2月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
1234567

导航

统计

常用链接

留言簿(6)

随笔档案(21)

文章档案(1)

搜索

最新评论

阅读排行榜

评论排行榜