返回信息流昨晚看到头文件和作用域规则困惑了好一阵,后来读完了下面这段文字才豁然开朗,贴在这儿给和我一样有困惑的人分享,同时也可以作为对比C++和其它语言的设计的一个参考
ps:《 程序设计语言——实践之路》确实是本好书,可惜没有时间读了
《 程序设计语言——实践之路》
3.6.2 分别编译
Separate Compilation
由于大部分的大程序都需要逐步地构造和测试,也由于编译一个很大的程序可能需要许多个小时,任何设计时就想支持大型程序的语言,都必须提供某种分别编译功能。由于模块的设计就是为了封装和提供较窄的界面,因此也就成为许多程序设计语言里“编译单位” 的自然选择。例如,Modula-3和Ada里分离的模块头部和模块体的意图显然是为了支持分别编译,反映了从其他一些语言里一些更基本的机制中取得的经验。
C的最初版本是20世纪60年代在贝尔实验室里设计的,在随后的25-30年里有了很大变化,但直到C9X(在本书撰写时还在发展中,见 [Int98a])标准之前,其作用域规则和分别编译机制基本上没有变。1990年ANSI标准C的主要规则可以总结如下(忽略了其中的一些细节和特殊情况)。
¨ 如果一个名字是在某个块里声明的(块是任何由 {…} 界定的结构,包括函数体或者复合语句),那么它的作用域从声明的位置直到这个块的结束(除去可能的空洞)。但如果是一个标号,那么作用域是它所在的整个函数体。如果一个名字是在所有函数之外声明的,那么它的作用域就从声明的位置一直延伸到所在的文件结束。
¨ 如果一个变量的声明中包含关键字extern,或者声明出现在所有的块之外而且又不包含关键字static,那么这个变量将被连接到本程序的任何文件里同一外部变量的所有声明。换句话说,如果在同一个程序的不同文件里包含相互匹配的外部变量声明,它们实际上是共享同一个变量。
¨ 如果一个函数的声明不包含关键字static,那么它就连接到这个程序的任何文件里同一个函数的所有其他(非static)声明。(函数声明可以只包含头部,见下。)
¨ 一个具有外部连接的(变量或函数)对象必须在整个程序的恰好一个文件有一个定义。变量被定义的形式可以是给它初始化,或者位于所有的块之外而又没有extern关键字。函数的定义就是给出它的体(代码)。
C语言的许多实现放松了最后一条规则,允许具有外部连接的变量可以有零个或者一个定义。在这些实现里,连接器(一个程序,它的工作是将分别编译的文件装配成一个完整程序)将在需要时为这种变量创建一个定义。
ANSI C的“连接”规则为不同文件里的名字建立关联提供了一种方式,要理解这些规则,最好是看看它们的实现。大部分独立于语言的连接器的设计就是处理各种符号,也就是机器语言程序里一些位置的字符串名字。连接器在最终程序里给每个符号赋一个位置,并把相关的地址嵌入引用各个符号的机器语言指令里。为了完成这一工作,连接器必须知道哪些符号可以用于解析其他文件里未约束的
引用,哪些符号只是本文件内部的东西。C的规则足以提供这些信息。当然,对于程序员而言,在这里根本没有正式定义的界面,也不存在使一个名字能够为一些文件所见,而不是在所有文件里可见的机制。进一步说,也没有任何机制来保证所找到的在其他文件里声明的对象符合需要。举例来说,完全可能在一个文件里把一个外部变量声明为具有多个域的记录,而在另一文件里将其声明为一个浮点数。语言并不要求编译器去捕捉这类错误,结果代码里的这类错误将很难发现。
幸运的是,C程序员开发出了一套使用外部声明的惯用形式,以便在实际中尽可能减少上述错误。这些惯用形式依赖于宏预处理器的文件包含功能。程序员以成对的方式创建文件,使它们大致对应于模块的界面和实现。界面文件都采用 .h结尾的名字,对应实现文件以 .c结尾。所有在某个 .c文件里定义的对象都在对应的 .h文件里声明。在 .c文件的开始处程序员都加入了一个命令行。对于编译器而言,这种命令行就像是特殊形式的注释,但它能导致预处理器把相应 .h文件的整个拷贝包含进来。这种包含操作还能起另一种作用:将模块里所有对象的“向前”声明放在实现文件最前面,与文件中后面定义的任何不一致都会导致编译器报告错误。程序员还在每个 .c文件的前部指挥预处理器,要求包含本文件所依赖的所有模块的 .h文件。只要预处理器把同样的 .h文件包含到了所有使用它的模块的 .c文件里,任何不一致的情况就都逃不过检查了。当然,所有的全局名字都必须互不相同,因为在这里并不存在超越独立文件之上的作用域规则。为了解决这一问题,许多程序员就把模块名嵌入到有关的外部对象的名字中(例如,scanner_nextSym)。当某个 .h文件被修改之后,人们很容易忘记去重新编译那几个相关的 .c文件,并因此造成许多很微妙的程序错误。有些工具(如Unix的make)通过追踪模块间的依赖关系的方式,能够大大减少这方面的错误。
Java的分别编译功能解决了C语言相应功能的许多问题。特别的,Java引进了正式的模块概念,称为包。每个编译单位(可能是一个文件,在某些实现里也可能是一个数据库记录)属于唯一的一个包,但一个包可以由多个编译单位组成,在每个编译单位的开始都指明了它所属的包:
package foo;
public class foo_type_1 { ...
除非特别把Java的类声明为public,否则它就只在同一个包的各个编译单位里可见。
如果Java的一个编译单位里需要使用其他包里的类,那么可以通过完全限定名的方式,或者通过显式导入它们的方式使用:
foo.foo_type_1 my_first_obj;
或者
import foo.foo_type_1;
...
foo_type_1 my_first_obj;
或者
import foo.*; // import everything from foo
...
foo_type_1 my_first_obj;
究竟哪些包可供导入使用,依赖于安装Java的那个宿主系统里的约定。基于Internet地址的编程约定鼓励程序员创建在世界范围里具有唯一性的包名字。
标准C++提供了一种更复杂的名字空间概念,它推广了已经由类和函数提供的作用域机制,打破了模块与编译单位之间的紧密联系,并把由一个里模块导出的名字聚集成一种可以清晰标识的界面。在一个namespace里可以声明任意的一组名字:
namespace foo {
class foo_type_1; // declaration
...
}
foo里的对象的定义可以出现在任何文件里:
class foo::foo_type_1 { ... // full definition
如果需要,也完全可以在一个文件里声明属于不同名字空间的对象。
如在Java里一样,C++程序员可以采用完全限定的名字,采用一个个导入对象的方式,或者一下导入整个名字空间的方式,使用在一个名字空间里定义的对象:
foo.foo_type_1 my_first_obj;
或者
using foo.foo_type_1;
...
foo_type_1 my_first_obj;
或者
using namespace foo; // import everything from foo
...
foo_type_1 my_first_obj;
C语言的C9X版本里也可能会有类似于C++的机制。
在Modula和Ada里,程序员可以通过词法嵌套的方式,在一个编译单位里创建起形成某种层次结构的一些模块(例如,将模块C声明在模块B内部,而模
块B又是在模块A内部声明的)。按照类似的方式,Java和Ada 95程序员可以通过多组分名字的方式,创建起由一些分别编译模块形成的层次结构:
package A.B;
class B_type_1 { ... // Java
package A.B is ... -- Ada 95
在上面例子里,包A.B称为包A的子包。Ada 95里的子包大致就像是在其父包的内部声明的,因此其父包里的所有名字都自动成为可见的(Ada里嵌套的包具有开作用域)。Java的情况正相反,多组分名字只是作为一种方便,在A和A.B之间并没有任何特殊的关系。如果A.B需要引用A里的名字,那么A必须导出它们而A.B必须导入它们。Ada 95里的子包使人想到C++里的派生类,但它们支持的是一种模块作为管理器风格的抽象,而不是以模块作为类型的风格。我们将在10.2.3节进一步考察 Ada 95的这方面机制。
这是一条镜像帖。来源:北邮人论坛 / cpp / #2243同步于 2008/2/3
CPP机器人发帖
[转载]分别编译
UnitTest
2008/2/3镜像同步0 回复
订阅后,新回复会通过你的通知中心匿名送达。
0 条回复
暂无回复 · 你可以订阅本帖等待新回复。