第1章 案例的选择与评估
C++语言的学习者往往希望有一个源代码公开、质量上乘的C++项目作为学习的范本来临摹。这种案例最理想的来源是知名软件企业使用C++开发的产品,比如微软公司开发的Windows操作系统、Office办公组件,苹果公司开发的MacOs操作系统,Google公司开发的Chrome OS操作系统或者Adobe公司的Photoshop。但显然,这些公司出于商业利益不会公开这些产品的源代码。幸运的是,软件开源运动为C++学习者提供了成千上万个可供借鉴的项目,我们可以在这些项目中遴选出质量高、文档完整、适合C++学习者的案例。
由于C++开源项目数量众多,我们分两个步骤进行遴选。
(1)初步筛选阶段,我们主要依据一个开源项目受关注的程度、该项目是否涉及太多的专业知识来选择。这个阶段我们选择了8个开源项目。
(2)定量评估阶段,我们使用CppDepend工具,评估每个开源项目的代码规模以及代码质量,最终选择了Qt。本章1.1、1.2节详细描述这个过程,读者可以借鉴其中的方法选择其他C++案例,或者在学习其他编程语言时,使用其中的方法选择对应的案例。而且,读者还可以使用这两节讨论的工具CppDepend剖析其他软件的结构与质量。
C++开源社区还有其他优秀的项目,1.3节简要介绍了其中的Boost库以及KDE开发框架。Boost库大量使用了模板技术,某些技术对C++初学者来说过于深奥。而KDE是在Qt基础上构建的一个开发框架。在掌握本书的内容后,读者可以继续研读KDE或者Boost,以进一步提升C++语言的应用能力。
本章1.4节介绍本书对术语等的约定,1.5节专门讨论在UML类图方面的约定。这两节和本章主旨没有关联,但是将它们放在书的前言部分又显得太长,因而只好将它们放在此处。
1.1 案例的初步选择
我们从以下网站搜索开源项目:
(1)Sourceforge(sourceforge.net),这是最著名、历史最悠久、规模最大的开源项目管理网站。
(2)Google code(code.google.com),它的访问速度快,是开源项目管理网站的后起之秀。
(3)C++创始人Bjarne Stroustrup的个人网站www2.research.att.com/~bs/applications.html,其中罗列了一些优秀的开源C++项目。
(4)开源中国社区(www.oschina.net/project/lang/21/c)。
世界上使用C++编写的开源项目很多,比如截至2011年12月仅Sourceforge上就有6450个C++项目。如何在这么多的项目中选择一个优秀案例呢?我们分两个步骤来选择:
第一,初步筛选。依据一些定性的指标,比如一个开源项目被用户关注的程度、该项目是否涉及太多的专业领域知识等,选择少量项目。
第二,定量评估。依据代码规模、注释内容比例、类的内聚性等指标,对各个项目进行定量地评估,选择最优者作为本书剖析的对象。
初步筛选阶段,我们采用了以下指标。
① 关注度。一个开源项目的关注度高,说明该软件实用、在同类软件中性能更好。由于得到用户的关注,该项目的开发小组就能够持之以恒地对软件进行改进,该项目的源代码质量就会更高。我们依据每周用户下载次数对开源项目进行排序,忽略那些下载次数少的项目。
② 涉及其他专业领域的程度。由于C++课程往往是软件专业学生的基础课程,当他们学习C++语言时,对其他专业领域了解不多。如果将那些涉及较深专业知识的项目(比如电路板自动布线系统、编译系统等)选为案例,学生将花费很多时间去学习和C++不相关的知识,这将降低学习效率。
依据以上指标,我们选择了8个C++开源项目。读者可从前文所述的开源网站下载这些项目的源代码以及安装程序,然后将这些项目安装在自己的机器上,体验它们的功能。以下是除了Qt以外的其他7个项目的简要介绍。
Celestia。以三维方式显示宇宙间10万颗星座的位置、形状信息。对于那些已有影像数据的星球,该软件能显示这些星球的表面图像,令用户感觉似乎是驾驶一艘宇宙飞船在星际间畅游。自2001年该软件被免费发布以来,已有3百万次下载,并被广泛地用在家庭、学校、政府机关等场所。该软件的官方网站为www.shatters.net。安装该软件之后,读者可以执行“帮助/系统演示”命令,体验星际旅游的乐趣。
K3DSurf。能够以非常漂亮的三维形式绘制多元数学函数的表面。
WinDirStat。能够计算并显示本地文件系统中各个子目录的字节数,也能够依据文件的类型(比如临时文件、MP3文件等)对文件进行分类,并显示每个类别的字节数。计算机用户可以使用该软件察看各子目录、各种类型文件占用磁盘空间的情况,删除那些不再使用的文件,以释放出更多的磁盘空间。
Source-Navigator。能够分析C、C++、Java等语言的源程序中各种部件(如函数、类)之间的关系,并以图形化的方式显示这些关系,还允许用户查询各个部件之间的关系,比如某个函数调用了哪些函数,该函数又被哪些函数调用。开发者可以使用该软件来查看、理解一个复杂软件的总体结构。
Notepad++。是运行在Windows操作系统之上的一款文本、源代码编辑软件。除了具有同类软件的常见功能之外,它支持Unicode,允许用户使用鼠标滚轮即可放大/缩小字体,支持多达52种编程语言、脚本语言以及标记语言。
WinMerge。能够比较两个文本文件的差异,也能比较两个文件夹中哪些文件存在差异。程序开发者可以使用该软件比较一个源程序的多个版本,以快速澄清各个版本的差异。
CppCheck。能够检测C++程序中的逻辑错误(而不是语法错误),比如,一个类在重载“=”运算符时,应该返回this指针所指对象的引用。
1.2 案例的定量评估
我们依据以下指标对8个C++开源项目进行定量化评估。
1.学生兴趣。从教育心理学角度,如果学习者对一个开源项目感兴趣,他就会积极主动地探询C++的语言特性是如何被应用在他所关注的项目中的,这可以大幅提高学习效率。为了得到定量的数据,我们对南开大学软件学院114名一年级本科生进行了问卷调查。具体地说,我们演示了这8个开源项目的功能,让每个学生独立地填写一个调查表。每个学生对一个项目感兴趣的程度用数字1~5表示,5表示最感兴趣,1表示最不感兴趣,感兴趣程度从1到5逐渐过渡。最后我们计算所有学生对一个项目兴趣程度的平均值。
2.代码规模。规模太大的开源项目将大幅增加学习者的学习难度和学习周期,规模太小的项目将无法展示C++语言解决复杂问题的能力,我们需要在两者之间折中。我们依据源代码行数(Line of Code,LOC)以及源代码中的类型数量来评测一个项目的规模。
3.代码质量。精确地评估一个软件系统的代码质量是比较困难的,这需要专家仔细研读软件的代码以及文档,评估其设计是否精良,是否具有良好的可扩展性、可移植性,源代码是否严格遵循某种编码规范,评估其运行时的性能、健壮程度等。8个C++开源项目的代码总量约为53万行,进行人工评估显然是不现实的。我们选择以下两个可以量化的指标来评估一个项目的代码质量。
(1)代码中注释部分的比例。适当比例的注释可以提高代码的可读性,同时也能体现编程人员的仔细与严谨。
(2)内聚性(cohesion)。所谓内聚性,是指一个类的成员变量和成员函数之间的耦合程度。较高的内聚性往往意味着较高的代码质量。有多种度量内聚性的方法,我们采用LCOM-HS(Lack of Cohesion of Methods,提出者为Henderson-Sellers)度量,其取值范围为0~2,越大表示内聚性越差。
4.C++特性的应用。有的开源项目虽然宣称是使用C++语言开发的,但是大部分代码是C语言编写的,只用到“封装”这样简单的C++特性。为了评判一个开源项目是否大量使用了C++特性,我们选择了以下三个指标。
(1)名字空间(namespace)的个数。在一个中、大型软件项目中,合理使用名字空间可以有效避免名字冲突,提高软件系统的模块化程度。
(2)继承的个数。类的继承是面向对象编程思想的典型特征,是实现多态性的必要条件。
(3)模板(template)的个数。除了使用面向对象思想,现代C++项目还大量使用模板技术,以实现泛型编程(generic programming)的思想。
以下工具可以定量地评估一个C++项目的规模与质量。
(1)SourceAudit(www.frontendart.com),由FrontEndART公司开发,该工具甚至还可以分析出C++程序中是否应用了设计模式。
(2)Telelogic公司的logiscope,对一个软件系统的可维护性、可重用性、可测试性、可读性等进行评估。该公司于2008年被IBM收购,该产品演化为Rational Software Analyzer。
(3)CppDepend(www.cppdepend.com),能够对C++程序进行60多个指标的测量,其中有些是关于代码结构的(如类及名字空间的数量),有些是关于代码质量的(比如程序注释比例,内聚性,项目稳定度等)。该工具还可以直观地显示程序模块、类、函数之间的依赖性。它将被分析的源代码当作数据库来处理,允许用户使用一种代码查询语言(Code Query Language,CQL)对源代码做各种分析。
由于CppDepend小巧(约8.6M字节)、灵活(支持代码查询语言)、被允许在学术机构中免费使用,我们选择了该工具。它生成的分析报告只含有部分评估指标,我们应该使用代码查询语言获得其他评估指标。关于该软件的使用(尤其是代码查询语言的规范),请参考其官方网站,本文不再赘述。
8个C++开源项目的定量化评估结果如表1-1所示。由于整个Qt库的规模太大,我们仅选择了其核心模块QtCore以及QtGui作为分析对象。表中,LOC表示代码行数(Line Of Code),Types表示类型的数量,Comm表示注释行与总代码行的比值,LCOM表示内聚性较差的类在所有类型中的比例,NSpace表示名字空间的数量,Templates和Inherits分别表示模板与继承的数量。
表1-1 8个C++开源项目的定量化评估结果
学生们最感兴趣的是Qt,这归因于该软件包的示例程序所展现的强大的图形/图像处理能力。其次,学生们感兴趣的是K3DSurf,这归因于该软件所展现的精美的数学函数曲面。Qt、Celestia和Winmerge都大量使用了名字空间、模板以及继承,表明它们都能成为C++语言的案例。由于学生们对Winmerge不感兴趣,我们应该在Qt和Celestia两者中选择一个。考虑到Qt不但可以作为C++语言的案例,同时也是一款功能强大的跨平台C++类库,研究该类库不但可以学习C++语言特性,还可以使用它来开发桌面应用程序,因此,我们最终选择了Qt。
1.3 其他案例
除了Qt之外,以下C++项目也可作为学习C++语言的优秀案例。
Boost。由80多个开源的C++子库组成。这些子库所针对的应用领域很广,即涉及通用领域(比如智能指针子库),也涉及众多的具体领域(比如封装不同操作系统文件系统差异的FileSystem子库)。出于性能、灵活性方面的考虑,该库大量使用了模板技术。
以下是该库中功能较强的一些子库:
(1)文本处理方面,处理正则表达式的Regex,处理Unicode编码的Locale。
(2)容器/数据结构方面,表示并操作循环缓冲区的Circular Buffer,表示并处理多个二进制位的Dynamic Bitset,表示各种类型图像数据的GIL(Generic Image Library),表示并处理图的Graph。
(3)并行处理方面,支持多线程的Thread,支持分布式并行处理的MPI(Message Passing Interface),支持线程间通信与同步的Interprocess。
(4)数学方面,表达并处理几何图形的Geometry,处理基本线形代数问题的uBLAS(Basic Linear Algebra Library)。
(5)其他方面,对程序进行调试和单元测试的Test,允许C++和Python交互操作的Python子库。
在开源网站SourceForge上,Boost库每发布一个新版本,就会有10万次下载。一些著名的商业软件使用了该库,比如绘图工具Adobe Photoshop CS2,Adobe Indesign,反病毒软件McAfee Managed VirusScan等。Boost库中的一些子库也被纳入了C++ 11标准。
Boost库具有高质量的源代码。Herb Sutter以及Andrei Alexandrescu在C++Coding Standards[1]中评价该库为“世界上设计最精良、质量最优秀的C++库之一”。Scott Meyers在Effective C++[2]中将掌握Boost库作为C++编程准则之一,也即“第55项:熟悉Boost”。
KDE。从终端用户角度看,KDE(Kool Desktop Environment)是一个能够运行在UNIX/Linux等平台上的一个桌面环境。它具有漂亮的外观,用户能够像使用本地文件一样使用网络上的文件,支持60多种自然语言,还包含了诸多应用程序(比如办公应用套件KOffice)。而从开发者角度看,KDE是一个构建在Qt基础之上、提供了更多功能的应用程序框架。该框架的源代码行数多达6百万行(不包括Qt),具有良好的代码质量。
1.4 基本约定
书中的源代码。除了直接引用Qt的源代码,本书还创建了一些独立的Qt应用程序作为例子。读者可以在随书光盘找到这些例子的完整代码。在本书正文中,为简明起见,我们往往只给出这些例子的主要代码,省略了那些与被讨论话题关系不密切的部分。对于预处理命令,我们只是简单地省略;对于其他语句,我们用省略号“……”表示省略。另外,Qt的源代码使用了较多的预处理命令(比如Q_CORE_EXPORT)。如果这些命令和被讨论话题没有关联,正文将省略它们。有些情况下,我们会将Qt源代码中的一些宏替换为它们所表示的具体内容,以增加程序的可读性。例如,在本书对应的开发环境中,宏Q_INLINE_TEMPLATE表示inline。本书正文将采用后者,使程序更易读。
将目录映射为盘符。本书假设Qt被安装在Windows的d:\qtsdk目录下。供Visual Studio 2010使用的二进制库、头文件等存放在目录d:\qtsdk\desktop\qt\4.8.1\msvc2010下。由于本书多个地方需要访问该目录,为方便起见,我们使用Windows的命令
subst d:\qt\vc q:
将这个目录映射为Q盘。另外,本书使用盘符“Z:”表示随书光盘的位置。您可以将随书光盘的内容复制到您的本地文件系统中的某个文件夹中,使用subst命令将其映射为“Z:”盘。类似地,您也可以使用该命令将您计算机上的Qt安装目录映射为Q盘。这样,本书所有对“Q:”以及“Z:”的引用也会适用于您的计算机。
交互操作的表示。本书会讨论对Windows操作系统或者Visual Studio 2008开发环境的具体操作。有些教科书会将操作过程中屏幕上出现的菜单、对话框等插入正文,指导读者如何进行操作。这种方式虽然直观,但会占用太多的篇幅。本书采用更加简洁的文字描述方式。以修改Windows操作系统的环境变量为例,本书的描述为“显示桌面\我的电脑\右键\属性\高级\环境变量\系统变量\PATH”,表示单击Windows操作系统的“显示桌面”按钮,找到图标“我的电脑”,按鼠标右键,在弹出的菜单中选择“属性”项,在弹出的对话框中单击“高级”标签,单击“环境变量”按钮,在弹出的对话框中的“系统变量”部分,找到“PATH”进行修改。
支持的平台。本书使用Windows XP/Visual Studio 2008编写所有的例子,尚未在其他平台上测试它们。
函数名。正文引用一个函数名时,会在其后加上“()”,比如printf(),以将其和其他标识符(比如类名)区分开来。函数参数全部被省略,不出现在括号中。
基类的对象。设B是一个基类,D是它的派生类。除非特别说明,“B的对象”既可以指B这个类本身的对象,也可以指派生类D的对象。即使B是一个抽象基类(因而不可能定义B本身的对象),我们也使用这个术语来表示其派生类的对象。
术语。同一个英文术语在不同的中文文献中会有不同的翻译,本书对常见术语的翻译如表1-2所示。某些英文术语在中文文献中很少出现(比如trait),本书将直接使用这些英文术语,而不是试图给出一个难以被广泛接受的翻译。
表1-2 英文/中文术语对照表
注①:本书将带有模板参数的类(比如C++标准模板库中的vector)称为类模板(class template)。我们不使用术语“模板类(template class)”,因为有的文献使用这个术语来表示本书中的类模板,而有的文献则使用这个术语来表示一个类模板实例化后生成的一个具体类(比如vector<double>)。类似地,我们将带有模板参数的函数(比如C++标准库中的sort)称为函数模板(function template),不使用模板函数(template function)这样的术语。
注②:有的文献将函子(factor)称为函数对象(function object)。考虑到factor实际上是一个类而不是一个类的对象,本书不采用函数对象的称谓。
1.5 关于类图的约定
本书使用UML(Unified Modeling Language)描述类之间的关系。虽然市面上多款UML建模与绘制工具都宣称它们遵循UML规范,但是这些工具绘制出来的类图存在着细微的差别。本书采用MagicDraw绘制所有类图,下面我们以一个框图编辑器为例介绍该软件的一些约定。
如图1-1所示,类Diagram表示一个框图,类Shape表示抽象意义的图形元素,类Circle和Rectangle表示圆形、矩形的图形元素,它们是Shape的子类。UML使用尾部位三角形的箭头来表示类之间的继承关系。
图1-1 使用MagicDraw绘制的类图
一个类被表示为一个最多具有3行的矩形框,第1行是类的名字,第2行罗列该类的数据成员,第3行罗列该类的成员函数,第2行、第3行可以被单独或者全部隐藏起来。每个数据成员(或者成员函数)前的“-”表示该成员是该类的私有成员(private),而“+”表示公有成员(public),“#”表示受保护成员(protected)。在数据成员那一行,冒号左边的是数据成员的名字,右边的是数据成员的类型,这部分可被省略。例如,类Diagram含有数据成员elements,表示一个框图含有哪些图形元素。它还含有数据成员fileName,表示以什么文件名保存该框图的信息。以上两个数据成员的类型分别为vector<Shape>以及string。
如果一个成员函数为纯虚函数,它的名字被显示为斜体,对应的类成为抽象类,类名也被显示为斜体。例如,类Shape表示抽象意义上的几何图形,其成员函数display()无法显示一个抽象的几何图形,因而被定义为一个纯虚函数。在UML类图中,它们的名字都被显示为斜体。
图1-1中,一端为实心菱形的箭头表示类之间的Composition关系。这种关系表示一个类的对象由另外一个类的对象组成。简单地说,就是表示“部分”与“全部”的关系。本例中的这个箭头表示一个框图(类Diagram的对象)“包含”多个几何图形(类Shape的对象,但实际上是Circle或者Rectange的对象,因为Shape是一个抽象类)。箭头正上方的字符串“contains”表示这两个类的关系。箭头左右两侧的数字揭示了相关对象之间的数量对应关系:给定类Diagram的一个对象(表示一个框图),会有类Shape的0到多个对象(实际上仍然是Circle或者Rectangle的对象)被包含在这个框图中;给定Shape的一个对象,只会有一个框图包含这个图形元素。
图1-1中,给定类Diagram的一个对象,通过其数据成员elements可以访问该框图所包含的任意一个图形元素。然而,给定Shape的一个对象,我们却无法简单地依据其数据成员找到它所属的框图对象。UML将这种属性称为可达性(navigability)。单向的可达性用箭头表示,如图1-1所示,而双向的可达性则用不带箭头的实线表示。
类之间有多种关系,上面的Aggregation关系只是其中的一种。在这些关系中,有两种关系和Aggregation关系存在着逻辑上的联系:Association关系以及Composition关系。在绘制类图时,要注意它们之间的联系与区别。
Association,表示两个类之间存在着语义上的关联。两个类是相互独立的,不一定有包含关系。UML用一个实线来表示这种关系,比如图1-2中类Venue和Event之间的关系:当在某个地方举办某个活动的时候,这两个类的对象会有语义上的联系,但是并不存在谁包含谁的关系。
图1-2 类之间的几种关联关系
Aggregation,如前文所述,表示“部分”与“全部”的关系,一个类的对象会包含另外一个类的对象,但是,可以有多个包含者包含同一个对象。当包含者被析构时,被包含者不一定被析构。UML用一端为空心菱形的实线来表示这种关系,比如图1-2中类Team和Person之间的关系。一个团队包含多个人,而一个人可以隶属于多个团队。当一个团队被解散时,团队成员依旧存在。
Composition,也表示“部分”与“全部”的关系,但是其中的“部分”只能够隶属于最多一个“全部”。当“全部”被删除时,其中的“部分”也必须被删除。UML用一端为实心菱形的一条实线来表示这种关系,比如前文所述的类Diagram和Shape之间的关系。一个框图可以包含一个或者多个图形元素,但是一个图形元素只能隶属于一个框图。当该框图被保存、关闭后,所有的图形元素将被析构。有的文献也将这种关系称为composition aggregation。
这几种关系之间的逻辑联系是显而易见的:按照association,aggregation,composition的次序,约束条件越来越多,因此,aggregation是association的一种特例,而composition又是aggregation的一种特例。但是这几个概念之间也的确存在着区别。并非所有的association都是aggregation,比如类Venue和Event之间存在着关联关系,但是根本就不存在包含关系。另外,并非所有的aggregation都是composition,比如类Team和Person的关系。
在绘制UML类图的时候,我们应该使用尽量精确的关联关系来标注两个类的关系。也就是说,如果两个类之间存在composition的关系,我们就不要使用aggregation或者association来标注。如果两个类之间存在aggregation的关系,我们就不要使用association来标注。只有这样,所绘制的类图才能够精确地揭示相关对象之间的关联关系。
例如,为了描述一个手机的机械结构,我们可以使用图1-3左侧的类图。一个手机含有多个零件(part),比如LCD显示屏、键盘按钮、电池等。一些零件可以组成一个组件(assembly),比如由电阻、电容、芯片等组成的射频信号发射模块。而较小的组件和一些零件可以组成更大的组件,比如由射频信号发射模块、充电模块、音频处理模块等组成的手机主板。由于零件和组件都是更大组件的一个组成部分,我们用基类component来描述它们的共同属性。类Assembly和Component之间应该是composition的关联关系,这是由于某一个component只可能隶属于一个assembly,对某一个assembly的删除操作必然导致其中所有component的删除。
图1-3 描述手机机械结构的两个类图
仅仅使用association关系来描述类Assembly和Component之间的关系是不精确的。图1-3右侧的类图存在错误情形:
① 设有一个assembly的对象a,它可以包含一个component的对象。由于assembly是component的子类,被包含的对象可以是对象a,此时,就出现了对象a包含其自身的错误情形。
② 设有两个assembly的对象a和b。a可以包含一个component的对象。由于b可以被看做一个component的对象,因而a可以包含b。而同时,b也可以包含一个component的对象,比如a,因而b可以包含a。这样,就出现了a和b相互包含的错误情形。
当我们使用图1-3左侧的composition关联关系来描述类Assembly和Component之间的关系时,就不会出现上述两种错误的情形。这是由于composition是aggregation关系的一种,而UML约定aggregation关系必须具备以下两个性质:
(1)反自反(anti-symmetry),即一个对象不能够和自身具有aggregation关系。
(2)传递(transitivity)。如果对象a是b的一个部分,而b是c的一个部分,则a一定是c的一个部分。依据反自反性质,错误情形①可以被排除。在错误情形②中,a包含b,b包含a,依据传递性质,会推导出a包含a自身,这违背了反自反性质,所以错误情形②也可以被排除。可见,使用精确的关联关系可以正确地描述软件系统中各个类之间的关系。
本节只是对UML类图的简要介绍。如果读者需要学习UML,可以参考文献[3]。如果需要了解一个UML术语的严格定义,可以参阅UML官方网站www.uml.org上的语言规范。