“在这里,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……)