1.2 关键词所带来的差异(A Keyword Distinction)(2)
我第一次被我所谓的"关键词受难记"绊倒,是在大约1988年,当时我们测试小组中的一位成员对cfront发出一个"大难临头,即将完蛋"的"臭虫"报告。在cfront内部的类型层次结构的原始声明中,根节点(root node)和每一个派生下来的子类型(subtype)是以struct关键词来声明的,而在陆续修改的头文件(header files)中,某些派生子类型(derived subtypes)的前置声明(forward declaration)却使用了关键词class:
- // 不合法吗?不,只不过是不一致罢了
- class node;
- ...
- struct node { ... };
我们的测试员说这是一个粗野的错误,是一个cfront无法捕捉的问题,因为……呃……当然……cfront用来编译它自己。
真正的问题并不在于所有"使用者自定义类型"的声明是否必须使用相同的关键词,问题在于使用class或struct关键词是否可以给予"类型的内部声明"以某种承诺。也就是说,如果struct关键词的使用实现了C的数据抽象观念,而class关键词实现的是C++(www.cppentry.com)的ADT(Abstract Data Type)观念,那么当然"不一致性"是一种错误的语言用法。就好像下面这种错误,一个object被矛盾地声明为static和extern:
- // 不合法吗?是的。
- // 以下两个声明造成矛盾的存储空间
- static int foo;
- ...
- extern int foo;
这组声明对于foo的存储空间造成矛盾。然而,如你所见,struct和class这两个关键词并不会造成这样的矛盾。class的真正特性是由声明的本身(declarationbody)来决定的。"一致性的用法"只不过是一种风格上的问题而已。
我第二次触撞这个题目是在C++(www.cppentry.com) 3.0所引入的"parameter lists of template"上头。Steve Burof,我的另一位贝尔实验室同事,有一天走进我的办公室并指出以下程序代码被语意分析器(parser)视为不合法:
- // 最初始被标示为不合法的
- template < struct Type >
- struct mumble { ... };
然而下面的代码却是合法的:- // 没问题:它显式使用了class关键词
- template < class Type >
- struct mumble { ... };
"为什么?"他问道。
"为什么不?"我清楚地予以回击,然后详细说明templates并不打算与C兼容。我说让我们撇开struct不谈,然后再看看它做什么事。我想我大概一跃而过我的Sun 3/60机器并以最佳姿态挥舞鼠标--老实说我不记得了。不过我记得最终我更改了语意分析器(parser),使它同时接受两个关键词--在没有事先告知Bjarne和"少不更事"的ANSI C++(www.cppentry.com)委员会的情形下。这是这个语言用词的诞生由来。
你可能会争辩说,如果这个语言只支持一个关键词,可以省掉许多混淆与迷惑。但你要知道,如果C++(www.cppentry.com)要支持现存的C程序代码,它就不能不支持struct。好的,那么它需要引入新的关键词class吗?真的需要吗?不!但引入它的确非常令人满意,因为这个语言所引入的不只是关键词,还有它所支持的封装和继承的哲学。你不妨发挥一下想象力,想想当谈论到一个抽象的base struct(例如ZooAnimal struct层次结构)时,其中内含一个或更多virtual base struct的情形。
在前面的讨论中,我区分了"struct关键词的使用"和"一个struct声明的逻辑意义"。你也可以主张说这个关键词的使用伴随着一个public接口的声明,就好像在公开演讲中使用暗语或昵称一样。你甚至可以主张说它的用途只是为了方便C程序员迁徙至C++(www.cppentry.com)部落。
策略性正确的struct(The Politically Correct Struct)
C程序员的巧计有时候却成为C++(www.cppentry.com)程序员的陷阱。例如把单一元素的数组放在一个struct的尾端,于是每个struct objects可以拥有可变大小的数组:
- struct mumble {
- /* stuff */
- char pc[ 1 ];
- };
-
- // 从文件或标准输入装置中取得一个字符串,
- // 然后为 struct 本身和该字符串配置足够的内存
-
- struct mumble *pmumb1 = ( struct mumble* )
- malloc( sizeof( struct mumble ) + strlen( string ) + 1 );
-
- strcpy( &mumble.pc, string );
如果我们改用class来声明,而该class是:
指定多个access sections,内含数据;
从另一个class派生而来;
定义了一个或多个virtual functions。
那么或许可以顺利转化,但也许不行!
C++(www.cppentry.com)中凡处于同一个access section的数据,必定保证以其声明顺序出现在内存布局当中。然而被放置在多个access sections中的各笔数据,排列顺序就不一定了。在下面的声明中,前述的C伎俩或许可以有效运行,或许不能,需视protected data members被放在private data members的前面或后面而定(译注:放在前面才可以):
- class stumble {
- public:
- // operations ...
- protected:
- // protected stuff
- private:
- /* private stuff */
- char pc[ 1 ];
- };
同理,base classes和derived classes的data members的布局也未有谁先谁后的强制规定,因而也就不保证前述的C伎俩一定有效。Virtual functions的存在也会使前述伎俩的有效性成为一个问号。所以,最好的忠告就是:不要那么做(第3章会更详细地讨论相关的内存布局主题)。
如果一个程序员迫切需要一个相当复杂的C++(www.cppentry.com) class的某部分数据,使他拥有C声明的那种模样,那么那一部分最好抽取出来成为一个独立的struct声明。将C与C++(www.cppentry.com)(参考[KOENIG93])组合在一起的做法就是,从C struct中派生C++(www.cppentry.com)的部分:
- struct C_point { ... };
- class Point : public C_point { ... };
于是C和C++(www.cppentry.com)两种用法都可获得支持:- extern void draw_line( Point, Point );
- extern "C" void draw_rect( C_point, C_point );
-
- draw_line( Point( 0, 0 ), Point( 100, 100 ));
- draw_rect( Point( 0, 0 ), Point( 100, 100 ));
这种习惯用法现已不再被推荐,因为某些编译器(如Microsoft C++(www.cppentry.com))在支持virtual function的机制中对于class的继承布局做了一些变化(请看3.4节的讨论)。组合(composition),而非继承,才是把C和C++(www.cppentry.com)结合在一起的唯一可行方法(conversion 运算符提供了一个十分便利的萃取方法):- struct C_point { ... };
-
- class Point {
- public:
- operator C_point() { return _c_point; }
- // ...
- private:
- C_point _c_point;
- // ...
C struct在C++(www.cppentry.com)中的一个合理用途,是当你要传递"一个复杂的class object的全部或部分"到某个C函数去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。然而这项保证只在组合(composition)的情况下才存在。如果是"继承"而不是"组合",编译器会决定是否应该有额外的data members被安插到base struct subobject之中(再一次请你参考3.4节的讨论以及图3.2a和图3.2b)。