程序代码也有风格,这算不得什么新鲜事。早在20世纪80年代, C语言程序员就必须在K&R风格和ANSI风格之间择善而从。但平心而论,我确实没有见过哪一种语言能像C++这样,在代码风格方面表现得如此诡谲和难以捉摸:谁也说不清C++代码究竟能衍生出多少种迥异的风格,但我知道,有许多C++初学者在面对不同风格的C++代码时,经常会误以为自己看到的是好几种完全不同的编程语言——仅此一点就足以提醒我们,研究和廓清C++语言风格的演化和发展规律已是当务之急了。
和文体学家们研究历朝历代文体变迁的工作相仿,研究C++语言风格的流变史也没有什么捷径可走。我们只能依据刘勰在《文心雕龙》中提倡的“原始以表末”[1]的研究方法,循着历史的脉络,推求代码风格的来源,探寻风格演化的内因,并借以阐明技术发展的趋势和规律。
1. 带类的C——对C语言风格的因袭 | ||
![]() |
在1983年12月Bjarne Stroustrup采纳Rick Mascitti的建议,将其发明的新语言命名为“C++”之前,人们一直用“带类的C(C with Classes)”来称呼这种脱胎于C语言的,带有数据抽象机制的“方言”。虽然带类的C在本质上仅仅是一种可以被预处理程序Cpre转换为传统C语言代码(这类似于我们在Oracle中见到的Pro*C语言的预处理过程)的扩展性语言,但它的确在风格上奠定了后来所有C++代码的基础。
class stack { char s[SIZE]; char* min; char* top; char* max; void new(); public: void push(char); char pop(); };
这段“带类的C”代码录自Stroustrup所著的《C++语言的设计和演化》。代码中的new()其实是类stack的构造函数,这与后来的C++语言有很大的不同。
显而易见,带类的C在风格上几乎完整地承袭了C语言的衣钵。代码中的声明语句看上去与C语言一模一样,class的结构也与C语言中struct的结构大致相仿,这些迹象反映出C++语言来源于C又尽量与C保持兼容的设计思想——这种设计思想既为C++的迅速普及提供了便利(C++语言的顺利推广显然得益于C语言已有的庞大用户群),也在C++的语言风格中深深地烙上了C语言的印记,以至于在若干年后,当C++语言已经基本具备了“独立人格”的时候,Stroustrup还不得不时常提醒人们要尽量抛开C语言的思维方式。
另一方面,Stroustrup从Simula语言借用的类、派生、访问控制等面向对象概念在带类的C中牢牢地扎下了根。据Stroustrup介绍,他为C语言引入面向对象机制的本意在于寻找一种“合适的工具”[2],以便实现分布式系统或解决类似的复杂问题。但无论怎样,Stroustrup将C的高效和Simula的优雅捆绑在一起的做法都在事实上为C++语言埋下了“双重性格”的种子——很难说这不是C++语言风格多样化的直接诱因。
2. I/O流——C++的新形象 | ||
![]() |
如果说C++语言的生身父母分别是C语言和Simula语言的话,那么,1984年出现的,借助操作符重载实现的I/O流技术就是C++这个幼童甩开父母的庇护,向新的代码风格迈出的第一步了。
ostream& operator<<(ostream&s, const complex& z) { return s << '(' << z.real() << ',' << z.imag() << ')'; }
上面几行代码来自Stroustrup所著《C++程序设计语言》中的示例程序。注意那一行由“<<”连接的代码,I/O流、变量、字符常量在代码中被巧妙地串联在一起。从技术角度看,这种全新语法的引入弥补了C语言中printf()函数族缺乏类型安全机制和扩展能力的弱点。从代码风格上说,“<<”等通俗易懂的运算符大大改变了程序员对C++语言的第一印象。我自己第一次接触C++ I/O流库时,就曾清晰地感觉到,一个试图摆脱C语言风格束缚的C++精灵正顺着“<<”和“>>”组成的溪水“流淌”而来——这种行云流水般的代码风格在十几年前就已经显示出了C++语言在塑造新形象、引进新观念方面的决心和勇气。
3. OWL和MFC——窗口环境下的风格变异 | ||
![]() |
20世纪80年代末到90年代初,X Window、Mac OS、Windows等窗口环境的先后出现为程序设计提出了新的课题,而C++语言兼顾面向对象和传统开发方法的特性无疑使其成为了窗口环境下编程语言的最佳选择。一批基于C++语言的窗口框架不仅在商业上取得了成功,也在很大程度上改变了C++语言本身的风格特点。
最早在窗口开发中赢得大多数程序员青睐的C++框架是Borland公司于1992年内置在Borland C++ 3.1中的OWL(Object Windows Library)框架库。下面这段代码取自Borland C++ 3.1的示例程序:
class TGDIDemoWindow : public TMDIFrame { public: TGDIDemoWindow( LPSTR ATitle, LPSTR MenuName ) : TMDIFrame(ATitle, MenuName) {}; virtual void SetupWindow(); virtual void ArtyDemo( TMessage& ) = [CM_FIRST + ArtyDemoID]; virtual void Quit( TMessage& ) = [CM_FIRST + QuitID]; virtual void WMTimer( TMessage& ) = [WM_FIRST + WM_TIMER]; virtual void WMDestroy( TMessage& ) = [WM_FIRST + WM_DESTROY]; };
为了解决窗口编程中最关键的消息映射问题,OWL的设计者为C++语言的成员函数引入了“=[…]”的古怪语法,这是许多用过Borland C++的程序员至今都无法忘怀的一种语言风格。我承认,Borland公司在C++语言的发展初期为我们提供了最好的编译器和最出色的集成开发环境(IDE),但Borland通过OWL框架为C++引入的另类语言风格的确让人不敢恭维(客观地讲,这笔账也不应全算在Borland头上,因为OWL的前身是Borland从White Water公司购买的框架代码)。
就在Borland C++ 3.1统治市场两年以后,Microsoft凭借其当仁不让的霸气和著名的Visual C++系列产品逐渐夺回了Windows开发工具市场的主导权。与Borland不同的是,Visual C++中的MFC(Microsoft Foundation Class)框架库没有向OWL那样肆意篡改C++的语法,而是采用了下面这样的方式来实现消息映射(代码取自MSDN示例程序):
// Example for BEGIN_MESSAGE_MAP BEGIN_MESSAGE_MAP( CMyWindow, CFrameWnd ) ON_WM_PAINT() ON_COMMAND( IDM_ABOUT, OnAbout ) END_MESSAGE_MAP( )
事实上,用MFC框架编写的C++代码在大量使用宏定义等预编译指令的同时,还把WIN32平台下常见的匈牙利风格(有关标识符大小写和前缀的书写规范)发挥到了极限。这一点用不着我多费口舌,许多程序员仅从代码的大小写特征上就能百分之百地确定代码中是否使用了MFC框架。
很遗憾,MFC为C++打造的语言风格并没有得到C++专家们的首肯。例如,包括Stroustrup在内的许多学者都建议我们尽量少用甚至不用宏定义等预处理指令。在这一点上,MFC的做法显然和专家们的论调背道而驰。应当说,是Microsoft的霸气造就了MFC的巨大成功;但从纯粹的语言学角度看,MFC在语言风格上的贡献远不如它在窗口框架技术方面的贡献大。
4. 模板——现代C++风格的基础 | ||
![]() |
Stroustrup于1988年首次公布了与模板(template)有关的语法设计。毫无疑问,这是一项对现代C++的语言风格影响最大的技术改进。模板的概念来自Clu语言,并综合了Smalltalk和Ada语言中相关技术的优点。1991年后,包含模板机制的开发环境(DEC C++、IBM C++、Borland C++等)陆续问世。但直到1995年STL(Standard Template Library)模板库逐渐发展成熟以后,模板技术才在程序员中迅速普及开来。
下面的例子取自SGI STL的示例代码,它基本反映了使用模板技术后C++代码的整体风格:
template <class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; }
在这样的C++代码中,除了少数几个关键字和操作符以外,我们几乎找不到多少C语言的痕迹了。模板技术兼顾了类型安全和编码灵活性的双重需求,但它同时也为C++语言引入了一种更加精妙但也较难理解(相对于没有模板的代码而言)的代码风格。许多传统的C语言拥护者讨厌这种风格的代码,但更多的新生代程序员对其钟爱有加。1998年,在ANSI/ISO标准化委员会的支持下,STL被作为标准C++库(Standard C++ Library)的一部分收入了C++国际标准之中。今天,以模板、异常等现代C++技术为代表的语言风格也已在事实上成为了C++世界的“官方风格”。
5. ATL——COM时代的另类C++ | ||
![]() |
除了STL模板库之外,还有一个与模板风格相关的例子。下面的代码片断取自Visual C++自动生成的ATL控件工程:
class ATL_NO_VTABLE CMyATLObj : public IMyATLObj, public IpersistStreamInitImpl <CMyATLObj>, public IOleControlImpl<CMyATLObj>, public IOleObjectImpl<CMyATLObj>, public IoleInPlaceActiveObjectImpl <CMyATLObj>, public IViewObjectExImpl<CMyATLObj>, public IoleInPlaceObjectWindowlessImpl <CMyATLObj>, public IPersistStorageImpl<CMyATLObj>, public IspecifyPropertyPagesImpl <CMyATLObj>, public IQuickActivateImpl<CMyATLObj>, public IDataObjectImpl<CMyATLObj>, public IProvideClassInfo2Impl <&__uuidof(CMyATLObj), NULL>, public CComControl<CMyATLObj> ......
注意控件类CMyATLObj的代码,CMyATLObj类居然是从N个接口类和控件类中派生出来的,类的声明语句中随处可见模板的身影——这就是Microsoft为我们设计的别具一格的ATL风格的代码了。之所以要不惜代价地大量使用模板、多重继承等语言特性,这主要为了适应COM、OLE、ActiveX等在架构上本来就相对复杂的技术体系。但这样一来,使用ATL的代码在所有C++代码中,就拥有了一副异乎寻常的长相了:到处都是尖括号,到处都是以“I”打头的标识符,甚至还有多重尖括号的嵌套……如果要求一个刚学会C++语言的程序员立刻读懂一大段ATL代码,我想,用不了几分钟,他就会被代码中那些晦涩、离奇的语言风格折磨得精神崩溃了。
6. 标准C++——一种全新的语言? | ||
![]() |
C++语言的标准化进程远远落后于语言本身的普及速度。1990年以后,ANSI/ISO的C++标准化委员会才将包括Stroustrup在内的大批专家以及包括Apple、Borland、DEC、HP、IBM、Microsoft、Sun、Unisys在内的知名公司召集在一起,像所有国家的议会或人民代表大会一样通过没完没了的会议、讨论和投票制定C++的国际标准。标准直到1998年9月才正式发布。在国际标准化组织的档案库里,C++标准的代号是ISO/IEC 14882:1998。
Stroustrup建议我们把标准C++当作一种全新的语言来学习[3]。这一说法显然是基于这样一个事实:标准C++语言已经拥有了一种稳定的、可以推广的语言风格,即,通过对STL等既有技术的肯定,ANSI/ISO委员会在1998年的标准中正式认可了包括模板、容器类、I/O流库、异常处理等典型语言特征的现代C++风格。风格的稳定意味着语言本身的进步和成熟,也意味着程序员们对C++的认识必须上升到一个新的层次——那些至今还在编写仅由类和C语言库函数组成的C++代码的程序员,一定会成为Stroustrup及其同仁们的取笑对象的。
Stroustrup的《C++程序设计语言》第3版对标准C++风格做了最权威的阐释。在Stroustrup等专家学者的号召下,越来越多的项目开始编写符合标准C++风格的代码。这一点在许多开放源代码的项目中体现得特别明显。这多半是由于,使用C++语言的开源项目大多都不会像大企业里的项目组那样,在语言风格上会受到公司背景或历史习惯的羁绊。在具体的编程实践中,开源项目的程序员们一方面可以坚决地贯彻标准C++的语言风格,另一方面也可以根据自己的喜好为代码增添一些感情色彩。例如,在OpenOffice的源码中,标识符的前缀规范就相当有特点,连指针和引用类型的变量都由不同的前缀字母区分;下面给出的Linux桌面管理器KDE 3.1.4的源代码片断则显示出,开发KDE的程序员在代码风格上或多或少受到了Java语言风格的影响:
class delUser: public KDialogBase { Q_OBJECT public: delUser(KUser *AUser, QWidget *parent = 0, const char *name = 0); bool getDeleteHomeDir() { return m_deleteHomeDir->isChecked(); } bool getDeleteMailBox() { return m_deleteMailBox->isChecked(); } private: QCheckBox *m_deleteHomeDir; QCheckBox *m_deleteMailBox; };
7. 读不懂的代码——兼容并包的语言风格 | ||
![]() |
说到标准C++语言风格,有必要给大家看一段非常古怪但也非常有趣的代码。你看得懂下面这段C++代码吗?它是真正的C++代码吗?
%:include <iostream> using namespace std; %:define MAX 5 void main() <% int m<:MAX:>; int i = 1; for (i = 0; i < MAX; i++) <% m<:i:> = i; if (i not_eq 3 and i < 5) cout << i << endl; %> %>
这是我自己编写的一段代码。你也许无法在Visual C++环境下运行它,但它的语法的确符合1998年C++标准的规定。在GNU C++环境下,我曾成功地将其编译为可执行程序。
简单说来,这段风格诡异的C++代码其实是根据C++标准中关于可替换标记(Alternative Tokens)的规定而编写的。该规定的设计初衷是要适应欧洲某些国家的标准字符集缺少“{”、“#”等标点符号(特别是在一些传统的终端设备上)的现状。严格地讲,这算不得一种真正的语言风格,但类似的规定的确体现了ANSI/ISO委员会在语言设计上兼容并包的宽广胸襟。
8. C++Builder——Borland的复兴之路 | ||
![]() |
Borland公司在发布了Borland C++ 3.1之后,就因为不思进取而将C++开发工具的市场拱手让给了Microsoft[4]。在经历了Borland C++ 4.0、4.5和5.0等版本的失败后,1997年,Borland推出了全新的C++开发工具C++Builder。这个在市场上为Borland挽回了颜面的产品不但在界面风格上与Borland的支柱产品Delphi别无二致,甚至还在产品内部直接照搬了Delphi的VCL(Visual Component Library)库。结果,使用C++Builder开发的代码天生就受到了Delphi风格的传染,长相酷似Pascal语言了(以下代码取自C++Builder 6.0的示例代码):
class TFormClrDlg : public TForm { published: // IDE-managed Components TColorDialog *ColorDialog; TButton *Button; TPanel *Panel1; void fastcall ButtonClick(TObject *Sender); private: // User declarations public: // User declarations virtual fastcall TFormClrDlg(TComponent* Owner); };
说实话,尽管C++Builder在市场上的表现不错,但我还是不喜欢Borland将C++语言与Delphi中的Object Pascal语言刻意混淆的做法。也许在Borland这种做法的背后有提高产品通用性、缩短产品开发周期等体面的理由,但使用C++Builder开发出的代码在外表上已经离标准C++风格越来越远了。
值得注意的是,Borland于2003年推出了其下一代C++开发工具 ——C++BuilderX。让人哭笑不得的是,这一次Borland居然将C++开发环境构筑在了用Java语言实现的PrimeTime平台上,这多少将C++语言推向了一种极为尴尬的处境。不过,C++BuilderX也为我们带来了一些好消息:在后续的版本中,C++BuilderX将集成vxWindows框架库[5],在这种框架下开发的C++代码显然要比使用VCL的代码具备更多的标准C++风格。
9. Visual C++ .NET——革命还是叛逆? | ||
![]() |
Microsoft将C++引入.NET环境的举动其实比Borland还要激进。单从风格上说,使用Visual C++ .NET开发的代码可能兼具MFC、ATL、标准C++、.NET托管代码等多种不同的风格。其中,对C++语言本身影响最大的,当然要数.NET托管代码为C++注入的若干新鲜血液了:
#using <mscorlib.dll> using namespace System; using namespace System::Reflection; using namespace System::Security::Permissions; public __value enum SomeStuff { e1 = 1, e17 = 17 }; [attribute(AttributeTargets::Class, AllowMultiple=true)] public __gc class ABC { public: ABC(int __gc[]) {} ABC() {} ABC(int) {} ABC(int, float) {} ABC(SomeStuff) {} ABC(String*) {} int rgnField __gc []; double rgdField __gc []; double dField; };
上述代码来自MSDN中的示例程序。看到Microsoft大刀阔斧地为C++语言引入的垃圾收集、Attribute属性等新特性和新技术,看到.NET托管代码新奇得近乎离经叛道的语言风格,我不知道是应该为Microsoft在发展通用语言平台上的努力而欢呼雀跃,还是应该为C++在C#语言阴影下日渐屈居.NET大戏中的配角而灰心丧气。也许,语言风格和程序员的感受在Microsoft眼中,都是些不值一提的小事,它们哪能和.NET的宏伟战略及Microsoft的强大帝国相提并论呢?
10. 回顾和展望 | ||
![]() |
语言风格的变迁从一个侧面反映了技术思想和产业需求的嬗变规律。从1979年Stroustrup完成第一个Cpre预处理程序算起,C++语言来到这个世界上已经快满25个年头了。这是一种在实践中诞生、成长和发展起来的语言。也许,Stroustrup从一开始就压根儿也没想把它设计成像Smalltalk那样纯粹的面向对象语言。开放性、高效率、兼容性和扩展性的需求将C++语言塑造成了一种典型的多模式(Multiparadigm)语言。无论是C++早期对Simula语言的继承,还是后来对Smalltalk、Ada、Clu等语言的借鉴,无论是ANSI/ISO标准风格的迅速普及,还是Visual C++ .NET在技术创新上的不懈努力,所有这些历史变迁都说明,C++在风格上的多样性主要源自C++语言本身“海纳百川”的胸襟和气概。
5年以后,当C++步入而立之年的时候,它会给我们带来新的惊喜吗?我们还会看到更加新奇的C++语言风格吗?也许,没有谁能给出准确的答案。但作为程序员,我们至少应该知道:无论面对什么样的软件需求,无论使用什么样的思维方式,C++语言都赋予了我们选择语言风格的最大自由;当我们真正理解了C++语言的精神实质之后,这种自由也必将成为所有优秀软件和优雅代码的坚实基础。
参考文献 | ||
![]() |