关键词所带来的差异
如果不是为了努力维护与C之间的兼容性,C++远可以比现在更简单些。举个例子,如果没有8种整数需要支持的话,overloaded function的解决方式将会简单得多。同样道理,如果C++丢掉C的声明语法,就不需要花脑筋去判断下面这一行其实是pf的一个函数调用操作(invocation)而不是其声明:
// 不知道下面是个 declaration 还是 invocation
// 直到看到整数常量 1024 才能决定
int ( *pf )(
1024 );
而在下面这个声明中,像上面那样的“向前预览(lookahead)”甚至起不了作用:
//
meta-language rule :
// pq 的一个 declaration,而不是 invocation
int ( *pq )(
);
当语言无法区分那是一个声明还是一个表达式(expression)时,我们需要一个超越语言范围的规则,而该规则会将上述式子判断为一个“声明”。
同样地,如果C++并不需要支持C原有的struct,那么class的观念可以借由关键词class来支持。但绝对令你惊讶的是,从C迁徙到C++,除了效率,另一个最常被程序员询问的问题就是:什么时候一个人应该在C++程序中以struct取代class?
如果是1986年,我的答案毫不拖泥带水:“绝不”!在我的C++ Primer第一版和第二版中(译注:第三版已于1998年5月出版),关键词struct并未出现在书籍本文,只出现在附录C中,而附录C用来讨论C语言。那时候,这是一个人非万不得已不会指出的一些小小哲学问题中的一个。然而如果能够指出这个问题,你可以获得一些小小(一般公认非常小)的满足。通常问题会被这样指出:“嘿,你知道吗,struct那个关键词,其实没什么用……”。但就像贝尔实验室(译注:Bell Lab.,C++发源地)的一位同事婉转对我说的,即使是最小的哲学问题也有人需要解答。如果一个C程序员渴望学习C++,当他发现我的书中没有提到struct,一定会相当苦恼。很显然把这个主题含入,可以提供语言移转时的救生索,让程序员攀上高峰时少点折磨。呵,多么哲学啊!
关键词的困扰
那么,让我重新问一次:“什么时候一个人应该使用struct取代class?”答案之一是:当它让一个人感觉比较好的时候。
虽然这个答案并没有达到高技术水平,但它的确指出了一个重要的特性:关键词struct本身并不一定要象征其后随之声明的任何东西。我们可以使用struct代替class,但仍然声明public、protected、private等等存取区段与一个完全public的接口,以及virtual functions和单一继承、多重继承、虚拟继承……早期,似乎每个人都得在一小时的C++简介中花费整整10分钟看清楚以下两者的相同:
class
cplus_plus_keyword {
public:
// mumble ...
};
和其C对等品:
struct
c_keyword {
// the same mumble
};
当人们以及在教科书中说到struct时,他们的意思是一个数据集合体,没有private data,也没有data的相应操作(译注:指member
function)。亦即纯然的C用法。这种用途应该和C++的“使用者自定义类型”(user-defined type)用法区别开来。在C这一边,这个关键词的设计理由因其用法而存在;而在C++那一边,选择struct或class“作为关键词,并用以导入ADT”的理由,是希望从此比较健全。这远比讨论“函数需不需要一个大括号”,或是“要不要在变量名称和类型名称中使用下画线(例如IsRight或is_right)”更具精神层次。
在C所支持的struct和C++所支持的class之间,有一个观念上的重要差异。我的重点很简单:关键词本身并不提供这种差异。也就是说,如果一个人拥有下面的C++使用者自定义类型,他可以说“喔,那是一个class”:
// struct 名称(或 class 名称)暂时省略
{
public:
operator int()
virtual void foo();
// ...
protected:
static int object_count;
// mumble
};
事实上你可以说上面那东西是个struct,也可以说它是个class。这两种声明的观念上的意义取决于对“声明”本身的检验。
举个例子,在cfront(译注:第一个C++实现品,由Lippman完成)之中,上述两个关键词在语意分析器(parser)中是以共享的“AGGR”替换的。而在Foundation项目中,Rob Murray的ALF层次结构保留了程序员真正使用的关键词。然而这份信息并未在更内层的编译器中被使用,倒是可以被一个“unparser”工具用来还原程序的ASCII面貌。啊,是的,如果程序经过“unparser”工具处理过后,无法还原原本使用的关键词,程序员一定会很郁闷——即使程序在其他方面是相等的。
我第一次被我所谓的“关键词受难记”绊倒,是在大约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++的ADT(Abstract Data Type)观念,那么当然“不一致性”是一种错误的语言用法。就好像下面这种错误,一个object被矛盾地声明为static和extern:
// 不合法吗?是的。
// 以下两个声明造成矛盾的存储空间
static int
foo;
...
extern int
foo;
这组声明对于foo的存储空间造成矛盾。然而,如你所见,struct和class这两个关键词并不会造成这样的矛盾。class的真正特性是由声明的本身(declarationbody)来决定的。“一致性的用法”只不过是一种风格上的问题而已。
我第二次触撞这个题目是在C++ 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++委员会的情形下。这是这个语言用词的诞生由来。
你可能会争辩说,如果这个语言只支持一个关键词,可以省掉许多混淆与迷惑。但你要知道,如果C++要支持现存的C程序代码,它就不能不支持struct。好的,那么它需要引入新的关键词class吗?真的需要吗?不!但引入它的确非常令人满意,因为这个语言所引入的不只是关键词,还有它所支持的封装和继承的哲学。你不妨发挥一下想象力,想想当谈论到一个抽象的base struct(例如ZooAnimal struct层次结构)时,其中内含一个或更多virtual base struct的情形。
在前面的讨论中,我区分了“struct关键词的使用”和“一个struct声明的逻辑意义”。你也可以主张说这个关键词的使用伴随着一个public接口的声明,就好像在公开演讲中使用暗语或昵称一样。你甚至可以主张说它的用途只是为了方便C程序员迁徙至C++部落。
策略性正确的struct(The Politically Correct Struct)
C程序员的巧计有时候却成为C++程序员的陷阱。例如把单一元素的数组放在一个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是:
n 指定多个access sections,内含数据;
n 从另一个class派生而来;
n 定义了一个或多个virtual functions。
那么或许可以顺利转化,但也许不行!
C++中凡处于同一个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++ class的某部分数据,使他拥有C声明的那种模样,那么那一部分最好抽取出来成为一个独立的struct声明。将C与C++(参考[KOENIG93])组合在一起的做法就是,从C struct中派生C++的部分:
struct
C_point { ... };
class Point
: public C_point { ... };
于是C和C++两种用法都可获得支持:
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++)在支持virtual function的机制中对于class的继承布局做了一些变化(请看3.4节的讨论)。组合(composition),而非继承,才是把C和C++结合在一起的唯一可行方法(conversion 运算符提供了一个十分便利的萃取方法):
struct
C_point { ... };
class Point
{
public:
operator C_point() { return _c_point; }
// ...
private:
C_point _c_point;
// ...
C struct在C++中的一个合理用途,是当你要传递“一个复杂的class object的全部或部分”到某个C函数去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。然而这项保证只在组合(composition)的情况下才存在。如果是“继承”而不是“组合”,编译器会决定是否应该有额外的data members被安插到base struct subobject之中(再一次请你参考3.4节的讨论以及图
——本段文字节选自《深度探索C++对象模型》
图书详细信息:
http://www.cnblogs.com/broadview/archive/2012/02/13/2349766.html