How C++ (C++怎么样)

主要内容:

C++是怎么来的以及怎么样变成今天这样的?
C++看起来(和C作比较)怎么样?
C++将会怎么样? (C++0x)

目的:
这篇文章是一些内容的整理(主要来自于《C++语言的设计和演化》),没有任何创新。主要的目的是:
从C++演化的角度,向不熟悉C++的人解释“C++是什么”
从实用性的角度,帮助新手了解“C++可以用来干什么”
自己学习的总结

实际上,这篇文章充其量只是《C++语言的设计和演化》一书的提纲。


C++是怎么来的?

Bjarne Stroustrup(以下简称BS)在其书中描述了C++的来历。C++设计的最初想法起源于BS对simula和BCPL的欣赏。它们各自具有很吸引人的优势:
simula:在设计程序,组织程序方面给予程序员的支持,也就是将问题域映射到语言特征中的机制,更直接一点说,就是类。
BCPL:因自身的简单性而造成的运行时的快速,以及从分别编译的单元出发组合成整个程序方面的简单和有效性。
BS正是苦于找不到一种同时具备这两个优势的工具,于是开始从一个现成的基础上(C语言)开始设计这样一个语言:
为系统程序设计提供simula的程序组织功能,同时又提供C语言的效率和灵活性。

等等,刚才不是还是BCPL吗?怎么又变成C了?BS在书中同样给出了这一问题的答案:灵活性(适用于任何程序设计技术),高效性(对硬件的操作简单而直接),可用性(在当时几乎所有计算机上都有C编译器),可移植性(是一个“比较”容易移植的语言)。这些条件对语言的使用者来说都非常重要,值得一提的是可移植性是一个极其重要却又不那么容易实现的目标,而C语言因其自身的限制,根本无法保证完全的可移植性。这一点也是作为后来者的Java着重强调,和赖以成功的一个原因。


C++是怎么变成今天这样的?

对C++来说,既然选定了C语言作为基础,只要保证维持与C语言的兼容性,保持住C语言的所有优点,则C++设计的第二个目标就达到了。看起来根本不需要什么工作嘛。的确,维持现状谁都会做。可惜的是,事情并没有这么简单。在C++努力实现其第一个目标的道路上,身为前辈的C一次次的跳出来阻碍C++的前进,与C语言的兼容性被很多人认为是C++前进的巨大障碍。这就好比革命不能和当前的统治者协商,因为建立新秩序往往意味着打破旧秩序。幸运的是,C++要做的也不是革命,在BS和其他设计者的努力下,C++比较圆满的达到了它的目标。最终成型的C++虽然不是和C百分之百的兼容,仍然做到了尽量的兼容,关于这一点后面再讨论。

任何人都明白一个道理,历史上出现的任何东西,都是在一定的背景下,在某些条件的不断影响下,逐渐发展起来的。C++的演化历史也很好的印证了这一规律。C++因BS本人对simula和C各自优点的欣赏而出现,由此奠定了C++的目标,这一目标又进一步注定了C++从一开始就是折衷主义的。C++一点也没有推翻C语言的意思,只想在一个既定基础上提供某些更好的功能。它不能随意设计,而必须在各种各样的限制条件下成长。它必须为程序员带来众多受期待的好处,同时又不能损失任何以前(C语言)就已经具备的优势。

C++的目标更形式化的说法是:
是一个更好的C
支持数据抽象
支持面向对象的程序设计

我一直相信“实践驱动”对学习起到的积极作用。下面就以“这个东西是用来干什么”的模式,来整理一下C++里如此多的特征是如何出现的,以及在它们演化的过程中考虑了那些东西。注意,这里列的都是原来C语言没有,而C++新增的特性:

类:当然,设计C++就是为了干这个的。类的概念用来帮助程序员将问题直接用语言支持的形式表达出来,让数据抽象和面向对象成为语言中显而易见的东西。类的概念也自动带来了控制对类中成员访问权限(public/private)的概念。

构造函数:为对象建立起类的不变式。于是其它成员函数都可以依赖于该不变式,所谓不变式就是,类似“我已经对指针p进行了初始化,因此它不会是null”这样的保证。有了构造函数,析构函数也就被自然而然的引入。

运算符new:C原有的malloc可以用来申请空间,但却不能初始化对象(即调用构造函数)。new则可以做两件事:申请空间和调用构造函数。同样的,有了new,delete也被对应的引入。

friend类:授予一个外来者访问private成员的权限。

内联函数:省掉函数调用的开销。最早是为了提高类成员函数调用的效率,使程序员在获得数据封装的好处的同时又不损失效率。内联函数是一个优雅的概念:兼顾了组织程序的能力和高效性。

public/private继承:使提供“实现性继承”成为可能,也就是说,如果你只想继承一个实现(而不是继承它的界面),那么应该使用private继承,因为在派生类的外面,利用一个派生类的对象将无法访问到基类的函数(即界面)。另外,public/private继承也是对访问权限的一种自然的推广:使权限控制不但适用于成员,也适用于基类。

虚函数:对多态的支持

重载:最早是为了支持用户自定义的运算符,因为用户定义类型往往需要自己定义+,-等等的意义,而不是使用某种默认的意义。

引用:为了支持运算符的重载。这一条可能不是那么显然。我们来看一个例子,假设T是一个用户定义类型,考虑operator +(T a, T b),我们这样调用它 z = x + y,和原有的内部运算符“+”的调用格式是一样的,但是值传递意味着在函数调用时需要拷贝对象,当对象很大时,很可能会造成效率低下。为了提高效率,我们想使用指针传递,这样就变成了operator +(T *a, T *b),调用格式则变为z = &x + &y;和值传递时不一样了,并且也容易出错,而如果有引用,则可以变成operator +(const T &a,const T &b),和 z = x + y,保持了和值传递时一样的模样。这个例子也说明了引用传递的作用可以简单叙述为:在保持和值传递同样的调用形式的同时,又能达到指针传递的效率。

虚析构函数:为了在使用多态时保证对象的析构能够正确完成。具体的说就是,假如类D继承B,为了保证代码B *b = new D();delete b;能正确的析构D对象,需要将B的析构函数声明为虚函数。否则,delete b将只能析构D对象的B部分。

多重继承:可以组合两个或几个类,以便在派生类里利用不同基类中都有的东西。例如,组合一个实现类和一个界面类。

虚基类:也许叫“虚继承”更贴切。完全是因为多重继承的引入而诞生。因为多重继承带来了可能存在同一个基类的不同拷贝的问题,才需要虚基类。需要注意的是,这里的“虚”描述的是派生的性质,而不是基类自身的性质。一个“虚基类”和一个非虚基类并没有什么不同,真正不同的是派生的结构图,对象的包含逻辑和对象的布局。

纯虚函数和抽象类:纯虚函数用于这样的情况,对一个操作基类根本没有有意义的实现,但又必须为派生类提供一个供它们派生的界面。例如,一个shape的旋转函数rotate,它根据不同的派生类有不同的行为。但如果用户定义一个shape对象并使用rotate呢,那将是一个错误,但语言没有避免这个错误。纯虚函数和抽象类避免了它:用户将无法定义一个抽象类的对象。总的来说,抽象类的概念最大的作用在于,使得在语言里做界面和实现的分离更加显而易见。

const成员函数和mutable:const成员函数用于区分哪些函数有可能更新对象的状态,而哪些却决不会。相对来说,这个东西更大的作用是在程序的清晰性上,而不是语言实现上的一些好处。mutable正好和const成员函数对立,以使人不用强制去掉const就可以在const成员函数里修改某些成员变量(mutable变量)。
这件事很有意思,先是提供了一个看起来不错的特征,后来又提供一个概念上和它相反,可以违反它,实际上却是一种补充的“看起来也不错”的特征。这似乎也从一个侧面反映了C++是一个野心很大的语言,想支持的优点、程序设计方法很多,并因此有可能把自己搞得更加复杂。

static成员函数:static成员函数有什么用?为什么不直接用一个全局函数来达到同样的效果?答案:static成员函数可以避免对全局名字空间的污染,并且可以在逻辑上将一个函数归入一个类。

保护成员:原有的public/private的权限控制只能用来区别对待两种用户:陌生人和自己,友元为权限控制系统增加了一个元素:朋友,但是,仍然无法区别对待派生类和陌生人,因此也无法给予派生类某些特别的权力。protected就是干这个的。

指向成员的指针:如果没有指向成员的指针,在使用一个指针来调用成员函数时将不得不用“难看”的强制。

dynamic_cast,typeid和type_info:它们共同组成了RTTI(运行时类型信息),以便支持一种叫做“在运行时确定对象的类型”的编程风格。

static_cast:可以做隐式转换的逆转换。也就是说,只要T->S可以隐式完成,static_cast就能做S->T。它(只)根据静态的信息来转换,与dynamic_cast相对。这也就是说,对多重继承这样的情况有可能会出问题。另一方面,static_cast和dynamic_cast都可以在类的层次上穿行。

reinterpret_cast:为了提供像C语言中(T)e一样的能力(以便减少其滥用)。reinterpret的意思是它用来将某个东西重新“解释”为另一个东西。它不能在类的层次上穿行。

const_cast:C语言的强制包括对const属性的强行修改。C++中提供了很多个*_cast,但是,上面提到的三个*_cast则都不能修改const属性(为了不引起副作用),于是就有了const_cast。
这里有一个区别:static_cast,reinterpret_cast和const_cast是C++里用来替代C语言中“不安全的”强制的特征,而dynamic_cast完全是新东西,他不替代任何东西,而是为了支持RTTI。

模板:用来实现参数化容器类,也就是像vector<class T>这样的东西。如果没有模板,就只能用大量的强制转换,或者依赖于臭名昭著的#define宏。

模板的显式实例化:可以让用户控制实例化的时机,以便在某个特殊的环境里得到某个类。如果没有显式实例化,用户将很难控制所生成类的某些方面(因为它们随时可能受到环境的影响)。

模板的专门化(specializition):在模板生成某个类的时候,允许用户对某个类型提供一种“专门”的实现,以适应某些特殊要求。专门化可以看成是重载的一种特殊的,不规则的形式。

异常处理:提供一种分层次的,逻辑上更为清晰的错误处理机制。异常在语法上的好处是“正常”代码和错误处理代码的分离。

名字空间:解决名字冲突的问题,使组合不同的库或者软件的不同部分更加容易,不会因名字冲突带来很大麻烦。

 

上面是一个按照特征列出的列表。这样做的一个原因是,正如BS指出的,多数人对特征的关注超过了语言的本质。实际上,特征往往是一个语言的表象。对语言进化起决定性作用的,应该是语言的目标,内在的一致性,一些设计原则,受到的限制等等这些更加根本的东西。在C++进化的过程中,同样存在着一些这种东西,某种程度上讲,它们对C++的演化几乎起到了决定性的影响。下面的列表并不是原则性的东西(原则性的东西BS已经详细的列出了,在《C++语言的设计和演化》第四章),而是一些重要的方面(一些由原则决定的东西),对它们的设计或者考虑深刻的影响了C++的演化:

用户定义类型和内部类型的一致性:
这一原则直接决定了用户定义类型可以像内部类型一样出现在堆栈,静态存储区和自由存储区上。同时规定了在为用户定义类型增加某些新特征时也必须考虑内部类型,于是出现了new对内部类型的支持,用户定义类型和内部类型之间的转换,内部类型可以有类似于用户定义类型的默认构造函数的概念等等。

连接模型:
C++出现在一个编译器和连接器都不那么强大的时代,正好BS又是一个尤其重实用的人,于是C++被设计成只使用简单的连接机制,而不依赖于某种高级的东西(比如说依赖性分析的工具等等)。这里就出现了一个问题:如何保证在不同编译单元里的变量或函数等是同一个东西?C语言使用了头文件的方式,在它的那个时代优雅的解决了这一问题。C++因连接器的限制,选择了沿袭这一方式。可是,include虽然好,却又是一个“不那么好”的东西,它会导致重复包含,编译单元过大等问题。后来,这一采纳C语言预编译机制的决定也成为C++无法摆脱饱受诟病的预编译机制的原因之一:某些东西也许确实曾经是好东西,但是随着历史变迁,它很可能会成为继续前进的障碍。

对象布局模型和类的实现:
相比于C,类是C++里的“新概念”。因此,相对来说,这是语言的设计者可以“自由发挥”的地方。但事实上,因为效率始终占据着C++设计目标的半壁江山,对“类”的实现每一次都要付出很大精力来关注程序的执行效率。在这里对象的布局模型处于最关键的地位。关键性的决策包括:普通数据成员沿用类似于C原有的struct的布局,虚函数表放在类的第一个非纯虚的非在线函数之后,同时在对象中插入(仅仅)一个虚表指针来实现动态绑定(以便实现多态)。RTTI也是通过虚表来实现。作为扩展,多重继承则实现为插入多个虚表指针,增加一个基类仅意味着(在虚函数方面)增加一个指针,而不会导致对象的规模无法控制。不同基类的指针转换也通过虚表中的一个delta值来完成,而不会在对象里增加某些控制信息。再进一步,虚基类也将通过虚表访问,并不增加对象的大小。通过这些仔细的设计,C++中的对象的空间效率非常高(仅比原来的struct多了一些虚表指针),同时时间效率上的代价也做得尽量的小。

与C的兼容性:
BS说,C++最强的和最弱的地方都在于它与C的兼容性。C语言是那个时代最被广泛使用的语言,C++从实用的角度出发,在设计之初就确立了一个规则:尽量保持和C的兼容性(其实这个规则挺含糊)。兼容性为C++的推广和发展做出了卓越的贡献,这种兼容性是大量的实际的系统所需要的。但另一方面,兼容性则直接导致了C++的复杂性,C++在制定新规则的时候不得不考虑旧规则,并因此可能形成更庞大的规则集合。而且,在C中有一些东西“不那么好”(比如:int和char之间的隐式转换),但在C++进化的时候由于某种原因而没有把它们去掉,于是它们就会成为一种遗憾,封住了使C++可能做得更好的道路。


C++看起来怎么样?

从历史功绩来看,C语言无疑是最伟大的语言。C语言的出现改变了许多人对系统程序设计的思考方式,因其灵活性而在几乎所有重要的领域都发挥着重要的作用,并结出累累硕果。C语言的设计理念,语法,语义等都深刻的影响了其后的众多语言。直到今天,C语言仍然是很多领域的霸主。那么,C++呢?

很简单,C++是一个更好的C。这就意味着,“对于C能做的那些事,C++都可以做得和C一样好甚至更好”。C++有一个优秀的前辈C,从C那里继承来了众多好的特性,并在此基础上发展更多的优点。

和那些不支持直接的面向对象和范型编程的语言相比,C++的优势自然是面向对象和范型部分。而和那些只强调面向对象方法的语言相比,C++的优势又在它的低级部分的灵活性和高效性。这听起来有点诡辩的味道。确实,很多人指责C++就像个“四不像”,是个什么都想做,却因此带来了过高复杂性的玩意。时至今日,Java的拥护者和使用范围已经大大超过了C++和C,而C语言本身也已经被证明,并没有像一些C++拥护者所希望的那样,逐渐淡出历史舞台。C++的地位显得有些尴尬。

C++的地位成为今天这个样子,和BS本人对系统程序设计的认识和对C++的期望密切相关。BS认为,现实中将有大量这样的应用,它们跨过很多领域,有高级领域,也有低级领域。C++的强项是对很多事情都比较擅长,并支持多种程序设计方法和风格,但具体到某些领域里,不一定是最好的。用BS的话说 ,在所有的领域中,C++的目标是至少成为第二选择。而如果只考虑一个主要语言,C++就会成为第一选择。然而,现实的情况是,人们往往都只首先在一个领域里考虑第一选择,将各部分连接起来则作为另外一个问题单独处理。于是,C++被冷落了。

总的来说,C++有这样一些优点:
直接支持数据抽象和面向对象
类型安全性(静态类型检查和类型安全的连接)
支持范型编程,并且C++0x将增强这一方面
适用范围广,社区庞大
灵活性和高效性
易于和别的语言连接,共同组成一个大系统。

有几个很明显的缺点:
复杂性,难于学习


作为经常谈论的话题,这里也简单的比较一下C++和C。首先,C++多出来了很多东西,用来支持面向对象和范型程序设计方法。这是C所不具备的,也很容易理解。人们常常需要比较的是C++的C子集和C语言。C++的一些设计原则或者需要,导致了C++的C子集和C语言之间出现了不少区别,这些原则和需求包括:
类型安全性
良好的作用域和权限控制机制
重载的需要

下面是C++对C语言的一些具体的改进(或者说区别,如果不算是改进的话),这一列表应该也不完整:
1 C语言允许调用不加声明的函数,编译程序简单的略过它,交给连接程序去处理。C++禁止这种情况。也就是说,C++规定函数必须声明。
2 C语言的函数调用不执行严格的参数类型检查。由于有上面一条,它也没有办法去检查处于不同编译单元里的函数调用。C++严格的执行参数类型检查。当然,现在C语言也会执行它能够执行的参数类型检查并提示错误或警告。
3 C中f()表示函数f可以接受任意个参数,C++中它表示f不接受参数。
4 C中的隐式类型转换没有警告,C++中对缩窄转换会发出警告,例如long或double转换为int,不过,一个特例是int转换为char没有,原因是其使用太广泛,频繁的发出警告让人厌烦。
5 C中对“结构名字”有一个单独的名字空间,C++中没有。不过,这一点上C++做出了妥协,并由此维持了兼容性。
6 C语言没有引用。
7 在C语言中,全局和静态对象只能用常量表达式来初始化,而不能用函数做动态初始化,C++则可以。
8 C语言允许隐含的int:static a = 5; C++中禁止隐含的int。
9 C语言允许在返回值类型和参数类型处定义新的类型。C++禁止
10 C语言中使用struct时必须写出完整的名字,如struct S s;C++不需要:这样写就可以:S s;因为在C++里struct也是一个用户定义类型,所以使用他们就不再需要定义他们时必须的关键字struct,就像使用类时不再需要class关键字一样。
11 C语言不能在条件和循环语句里声明变量,C++可以
12 描述空指针上的细微的不同。C语言一般使用(void*)0,C++使用0。C中void*可以对任意指针赋值,C++不允许。
13 C语言中的连接不检查类型,C++检查。
14 C语言中枚举值的类型是int,用int可以自由的给枚举变量赋值。C++中,一个枚举是一个用户定义类型,也就是说,像class和struct一样,而不再是C语言中万能的int。
15 C语言没有bool类型,C++中有。
16 C语言中没有float和char的右值,它们会被立即提升为double和int。C++为了重载规则的清晰性,对类型规则做了修改,使它们(在参数传递时)不会被提升,对char和float也能使用重载解析规则。
17 C里全局的const变量默认的具有外部连接,C++里默认是内部连接。

上面的列表中,第1 2 3 8 9 12 13 14 条有可能会导致不兼容,也就是说,C语言的程序不能拿过来就用,必须经过某些修改,才能适应新的规则。

下面这些也许是C++的遗憾:
没有一个标准的废料收集系统
没有摆脱C语言的预处理机制


C++将来会怎么样?

C++0x的x是9,也就是今年。C++0x已经近在咫尺了。关于C++0x,BS很早以前给出了一些概貌(这篇文章可以在网上很容易找到),在那里BS展示了一个例子,在非常简短的几行代码里糅合了传统编程方式,数据抽象和范型编程,这就是所谓的多范型编程(multi-paradigm programming)风格。这正是C++赖以成功的因素和不懈追求的目标,也是C++语言设计哲学的一个最主要方面。
总的来说,我们看到C++0x带来的主要是更多新的东西(对范型的更好支持和更好的库),而不是对过去的改革。C++0x最令人激动的地方将是使范型得到更简单、更直接的表达,从而使这种比面向对象更新,在某些领域能发挥强大能力的编程风格更加深入人心。
当然,新的东西将会使C++变得更加复杂。但是,C++在改变人们思考程序设计方法方面和支持更良好的程序设计风格方面的功绩足以盖过其复杂性的缺点,证明其自身:是一个成功,富有生命力的语言。

本文完。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值