Java与C++比较

来源:互联网 发布:大连矩阵科技有限公司 编辑:程序博客网 时间:2024/05/13 15:55

两种面向对象语言的比较,C++和Java

1概述

面向对象编程的基本思想是把软件(尤其是大型软件)看成是一个由对象所组成的社会。对
象拥有足够的智能,能够理解从其它对象接收到的信息,并且以适当的行为对此做出反应;
对象能够从上一层对象继承属性和行为,并允许下一层对象从自己继承属性和行为等。拥有
相同属性,展示相同行为对象被分为一组,我们就得到了一个类。实际中,我们首先定义一
个类,然后通过对类进行实例化来创建这个类的若干对象。
目前,C++和Java 是两种被广泛使用的面向对象程序设计语言。
C++是从C 语言发展而来的。贝尔实验室的Dennis Ritchie 于1972 年在一台DEC PDP-11 上
实现了C 语言。最初,C 语言是作为UNIX 系统的开发语言而被人们广泛了解。在过去的
20 年中,大多数计算机上都实现了对C 语言的支持。作为C 语言的扩展,C++是贝尔实验
室的Bjarne Stroustrup 于20 世纪80 年代开发出来的。C++提供了大量完善的C 语言的特性,
但更重要的是它提供了面向对象的编程功能。今天,基本上所有的操作系统都是用C 或C++
编写的。
Java 是由美国Sun Microsystems 公司开发的一种能在Internet 上具有“硬件/软件独立性”和
交互能力的新型面向对象的程序设计语言。Java 不同于Pascal 这样的个人开发的语言,也
不同于一个小组为了自己的使用目的而开发出来的C 或C++,它纯粹是为了商业目的而开
发。Java 的一个突出的特性是它的平台无关性,Java 程序可以一次编写而到处使用。
值得指出的是,C++与Java 这两种面向对象语言具有许多的相似之处,将这两种语言放在
一起比较是一件有趣的事。然而,将它们的所有特征都拿出来相比是十分困难的。限于篇幅,
本文主要就基本语言特征、面向对象机制及相关方面对它们做一个比较。
2. 基本语言特征
2.1 字符集、标识符
字符集是指允许出现在一个语言的程序里的字符的全体。C++采用7 位的ASCII 字符集。我
们知道,ASCII 码把每个字符与一个二进制代码相关联,它的范围是0000000 至1111111 之
间,也就是十进制的0 至127 之间。与C++不同的是,Java 采用的是16 位的Unicode 字符
集,包含65536 个字符。其中,前127 个字符与7 位的ASCII 字符集相同。其它的字符可
用于注释、标识符、字符和字符串字面量。当然,用于标识符可能并不好。
与C++相比,Java 采用了更大的Unicode 字符集。这样做的优点是明显的,一个大的字符集
其表达能力会很强。例如,字符的16 位表示形式允许Java 标识符包括世界上许多不同地理
区域的字母和数字。另一方面,这样也会带来一些问题。比如说,标识符的处理效率低,可
读性差等等。
标识符是指用作语言里的关键字以及程序对象的命名的字符序列。对于C++和Java,标识
符中的字符必需是字母、数字或者下划线。需要说明的是,这两种语言的标识符都是大小写
区别对待的。例如,apple 和AppLE 和APPLE 是3 个不同的标识符。对于标识符的第一个
字符,C++和Java 的规定有一点不同。C++要求起始字符必须是字母或下划线,而Java 既
允许起始字符是字母或下划线,也允许是美元符号“$”。
2.2 基本类型
C++和Java 的基本类型包括:布尔类型、字符类型、整数类型、浮点类型。
2.2.1 布尔类型
C++中的bool,Java 中的boolean 为布尔类型。一个布尔类型的值只可以是true 或false 二者
之一。布尔值用来表示逻辑运算的结果。在C++中,整数和指针都可以隐式地转换为布尔值:
非零整数或非空指针转换为true,0 或空指针转换为false。布尔值也可以隐式地转为整数:
true 转为1,false 转为0。然而,Java 既不允许将一个布尔类型的数据转为其他类型的数据,
也不允许将其它类型的数据转为布尔类型的数据。
2.2.2 字符类型
C++允许我们使用三种不同的字符类型:char、unsigned char 和signed char。一个字符类型
的变量占用一个字节的存储,它可以容纳256 个值之一。每个字符常量都对应一个整数值。
对于unsigned char,其十进制值的范围是-128 至127;对于signed char,其范围则是0 至255。
然而,对于普通的char,其十进制值的范围到底是-128 至127 还是0 至255 呢?这取决于
具体的编译器。不管在哪种情况下,差异只出现在那些超出127 的值,而最常用的字符都在
127 之内。在Java 中,基本的字符类型只有一个,那就是char。一个char 变量代表一个16
位的Unicode 字符,占用两个字节的存储。Java 的char 变量是无符号的,这表示它相应的
整数值的范围是0 至65535。
2.2.3 整数类型
C++和Java 都支持这三种整数类型:short、int 和long。除此之外,C++还支持无符号整数
(unsigned)和有符号整数(signed)类型,Java 还支持一种额外的整数类型byte。在C++
中,与char 不同的是,普通的int 总是有符号的,因此有符号整数类型只不过是其对应的普
通int 类型的一个同义词罢了。C++提供了这么多个整数类型,是为了支持系统程序设计中
的细致选择。
C++标准要求一个short 类型的变量占用的空间不小于char 类型,一个int 类型的变量占用
的空间不小于short 类型,一个long 类型的变量占用的空间不小于int 类型。但是它并没有
准确规定各种类型的整数应分别占用多少个字节,把这些问题留给了实现。具体实现可以选
择运行平台上最合适的编码形式,以提高实现效率,得到适应各种新环境的能力和需要。当
然,这样做的缺点是会损害程序的可移植性。在这一点上,Java 与C++形成了鲜明的对比。
Java 明确规定了一个byte、short、int、long 类型的整数分别占用1、2、4、8 个字节。此外,
这四种类型都是有符号的。
2.2.4 浮点类型
C++和Java 都支持这两种浮点类型:float 和double。此外,C++还支持long double 类型,
用于对浮点数的精度进行扩展。在C++中,float、double 和long double 的确切意义是由实
现决定的。比如说,一个双精度浮点数到底占几个字节可能因编译器而异。反之,Java 浮点
数的格式由IEEE 754 严格定义。该标准明确规定一个float 类型的变量占4 个字节,一个
double 类型的变量占8 个字节。通过比较可以看出,无论对于是整数类型还是浮点类型,
C++标准都没有规定其确切意义,而Java 则把一切都规定清楚了。Java 做出这样严格的规
定,优点是有利于程序在不同平台之间的移植,这于Java 的平台无关性需求是相一致的。
当然,这样做也会带来一定的缺点,那就是某些机器上可能难以有效实现这些类型。
2.3 声明、定义、初始化、作用域
2.3.1 声明是否定义
无论是在C++还是Java 中,我们在使用一个标识符之前必须对它进行声明。在某些情况下,
一个声明同时也是一个定义,然而声明和定义并不等同。在声明中,除非我们为一个标识符
分配了足量的内存,否则这个标识符便没有被定义。
一个声明是否同时也是一个定义?这个问题在C++和Java 中的答案并不相同。在C++中,
绝大多数的声明同时也是定义,不管变量是全局的、局部的还是一个类的对象的数据成员。
但是,也有一些C++声明不是定义,这里有几个例子。
extern int num;
变量的extern 存储类型能够使几个不同的源文件共享同一个变量。上面的extern 声明通知编
译器num 是一个int 变量,但不必为这个变量分配内存。
class Student;
这条声明是Student 类的不完整定义。编译器将期望在别的地方看到一个完整的定义。
int f(double);
这个声明是一个函数原型。它表示f 是一个函数,接收一个double 类型的参数并返回一个
int 类型的值。编译器将在其它地方寻找它的定义。
在Java 中,一个变量声明是否同时也是定义取决于这个标识符是一个方法的局部变量还是
一个类的数据成员(注意在Java 中不允许有全局变量)。下面这一声明
int num;
如果位于一个方法内部,那么它就不是一个定义。这个声明不会为num 分配任何内存。这
一点与C++是不同的。如果希望在声明一个局部变量时同时对它进行定义,就必须给它一个
初始值,如下所示。
int num = 0;
反之,如果一个变量时一个Java 类的对象的数据成员,那么它的声明也同时是它的定义。
2.3.2 是否默认初始化
假如我们定义了一个变量,但没有为这个变量提供初始值,那么编译器会不会给这个变量提
供一个初始值?同样地,这个问题的答案在C++和Java 中也是不相同的。在C++中,对于
基本类型的变量,有下述两种情形。
(1)对于全局变量,用全0 的二进制序列进行默认初始化。例如,数值变量被初始化为0,
指针变量被初始化为空指针。
(2)对于局部变量,不提供默认初始化。这样做是为了保证执行效率。缺点在于,如果未
赋值就使用这些变量,后果是无法预料的。
对于一个类类型的对象,如果这个类有一个无参构造函数,那么不管该对象是全局的还是局
部的,它都会根据这个无参构造函数进行默认的初始化。如果这个类没有提供无参构造函数,
我们又要考虑两种不同的情况。
(1)如果这个类没有提供任何构造函数,那么系统会为这个类提供一个默认的无参构造函
数。然而,这个无参构造函数到底做什么,这是因编译器而异的。有可能只进行内存分配而
不进行初始化。
(2)如果这个类提供了其它的构造函数,只是没有一个无参的构造函数,那么无参构造函
数就不存在。如果我们对某对象只进行定义而不进行显式的初始化,就会导致一个编译错误。
在Java 中,一个基本规则是,当一个变量被定义时(而不是只声明),它总是被默认的初始
化为适当的“0”。例如,boolean 被初始化为false,double 被初始化为0.0,对象引用被初始
化为空引用null。Java 要求在每个变量使用前,都必须给它“定义性”赋值(初始化)。即在
到达变量的每个使用点的每条控制路径上,必须出现对这个变量的赋值这种规定是为了防止
出现使用未初始化的变量的值的情况。这个规定是静态的要求,是充分条件,可能并不必要。
2.3.3 作用域
标识符与被定义对象之间的约束由定义建立,在续存期间固定不变。这一约束在源程序里的
作用范围(在该范围里,这个名字指称这个对象)称为这一约束的作用域。作用域单位是指
一种语言所规定的可以作为作用域的结构。C++常见的作用域单位有:全局的外部作用域、
子程序定义的局部作用域(不允许局部嵌套定义的子程序)、子程序里任意嵌套的复合语句
(块)作用域、类定义形成的作用域、方法的作用域嵌套在类作用域里、名字空间(namespace)
定义的模块作用域。Java 常见的作用域单位有:全局作用域(只能定义类)、类定义形成的
作用域、方法作用域嵌套在类作用域里、包(package)形成的模块作用域。关于作用域,
Java 与C++有两点明显的不同之处。一是Java 的全局作用域里只能定义类,不能定义全局
变量或是全局子程序。此外,Java 中如果一个标识符已经在一个外层代码块中进行了声明,
那么它不允许在内层代码块中重新声明。例如,下列代码段在Java 中是非法的,而在C++
中没有问题。
int i = 1;
{int i = 2;}
//...i...
2.4 表达式
我们主要讨论一下运算对象的计算顺序以及表达式的副作用。在这两个方面,C++与Java
有着明显的差异。
2.4.1 运算对象的计算顺序
运算对象的计算顺序是一个有趣的问题。例如,对于表达式(a - b) * (c + d),(a - b)与(c + d)
中哪个会先被计算?对于这一问题,C++与Java 有着不同的回答。C++对运算对象的求值顺
序不予规定,目的是允许编译器采用任何求值顺序,允许在表达式编译中做更多优化。而
Java 则明确规定一律按照从左到右的顺序计算各个运算对象。这样做的优点在于,提高了程
序的可移植性和安全性。然而,这样做也限制了语言的新实现方式和实现技术的应用,限制
了目标代码优化的可能性。
2.4.2 表达式的副作用
C++和Java 都允许一个表达式产生副作用。一个问题是,如果一个表达式产生了副作用,
什么时候能看到这个作用的效果?通常而言,语言规定了变量修改的最晚实现时间,称为顺
序点或执行点。编译器保证到每个执行点时,此前的所有修改都体现到内存,此后出现的修
改都没有发生。然而,在执行点之间,副作用是否体现到内存是没有任何保证的。
关于顺序点,C++和Java 有着不同的规定。C++的顺序点有如下几种。
(1)每个完整表达式结束时。包括变量初始化表达式,表达式语句,return 表达式,条件、
循环和switch 的控制表达式。
(2)运算符“&&”、“||”、“?:”和逗号运算符“,”的第一个运算对象之后。
(3)函数调用中对所有实参和函数名表达式的求值完成之后,进入函数体之前。
与此不同的是,Java 的每个计算动作之后都有顺序点。也就是说,Java 中每个计算的副作
用将立即得到体现。Java 的这种做法达到了语义的确定性,却降低了实现效率。
2.5 变量的语义模型
2.5.1 变量的值模型和引用模型
我们讨论两种变量的语义模型:值模型和引用模型。在变量的值模型中,值保存在变量的存
储区里;而在变量的引用模型中,变量的值需要用另一个值对象表示,变量的存储区里存放
的是对值对象的引用。
C++里的变量都采用值模型。Java 里基本类型的变量采用值模型,其他类型的变量都采用引
用模型。
2.5.2 C++中的引用变量与指针变量
C++提供了引用变量。例如,我们可以写:
int n = 0;
int &r = n;
对于引用变量,我们应当把它看作是被引用变量的一个别名。对引用变量本身不能做任何操
作,只能通过它操作被引用的对象。
C++中还支持指针变量,即以地址作为值的变量。在C++中引入了指针变量,地址就成为可
以操作的了。我们可以对地址进行赋值和运算。例如,一个地址值可以加上(或减去)一个
偏移量,得到一个新的地址。C++允许指针运算,这带来了许多的灵活性,但另一方面,也
有可能导致意想不到的严重后果。需要指出的是,指针是采用值语义的语言里的一种机制,
用于模拟引用语义。设想一下,如果在C++中没有指针,我们就无法进行动态存储管理,无
法建立起复杂的数据结构(例如链表)。
Java 中除基本类型外,变量采用的都是引用模型,因此Java 没有必要再另外提供引用变量
或指针变量机制。
2.5.3 废料、废料收集
废料是指丧失了引用途径,已经不可能在程序里访问,但仍然占据着存储的“死”对象。在
C++中,由于程序员的不当疏忽,或由于情况复杂不能确认而未能释放,可能导致废料的产
生。而在Java 中,这一问题显得更为突出:所有赋值都是修改引用,要求程序员去确认某
个操作可能导致对象丢掉所有引用,将成为很大负担,甚至根本不可能。针对这一问题,Java
提供了自动废料收集机制,它能够自动识别废料,并把它们送回自由空间。提供自动废料收
集机制,优点是明显的。对于面向对象程序的开发,自动废料收集能大大地降低开发的代价。
程序员在编程时的负担减轻了许多,不必去考虑废料问题。然而,这样做的缺点也是不可避
免的。自动废料收集不仅在时间和空间上的开销很大,而且可能会导致程序执行的时间特征
名字 值
名字 值
变量的值模型
变量的引用模型
无法预期。值得庆幸的是,近年来随着CPU 速度的提高和内存容量的增大,废料收集的开
销已经不是大问题。尽管C++中也存在类似的废料问题,但是C++没有提供自动废料收集。
其原因在于,C++语言设计者的一个目标就是尽可能避免对于自动存储回收的依赖性,目标
是支持系统程序设计,提高效率,减少不确定性。
2.5.4 两种变量的语义模型的优缺点比较
变量的值模型和引用模型各有其优缺点,主要体现在下述几个方面。
(1)变量的值模型实现简单,易使用,易理解。而引用模型实现复杂,概念的理解也比较
困难。
(2)采用变量的值模型,值可以直接访问,效率高。而采用变量的引用模型,需要多做一
次间接访问,效率有所降低。
(3)对于变量的值模型而言,赋值是值的拷贝,语义清晰,易于理解,但对于大对象可能
耗费时间。而对于变量的引用模型,赋值是引用共享,语义比较复杂,但赋值只修改引用,
效率高(尤其对于大的对象)。
(4)采用变量的值模型,存储管理方便,可根据变量的作用域,利用栈机制统一进行管理,
存储管理简单,管理的开销少。而采用变量的引用模型,由于值对象的复杂引用关系,必须
有堆存储管理的支持和废料收集,管理复杂,开销大。
(5)采用变量的值模型,需要静态确定变量的类型,静态分配存储,带来高的执行效率,
但较难支持动态确定大小的数组和其他具有动态性质的数据结构,包括字符串等。而采用变
量的引用模型,变量可以无类型,变量许多特征不必静态确定,可自然地支持动态大小的数
组和字符串等,但可能会增加一些运行时开销。
(6)变量的值模型需要另提供动态存储分配和指针概念,支持动态数据结构等高级程序设
计技术。而与引用相比,指针是更危险难用的机制,是程序错误的重要根源。采用变量的引
用模型,能方便自然地支持各种高级程序设计技术,包括动态数据结构,面向对象的程序设
计等等,不需要另外的指针概念。
3. 面向对象机制
3.1 类的基本概念
3.1.1 类的定义、构造函数
下面是一个C++类的简单例子。
class User {
string name;
int age;
public:
User(string str, int i) {name = str; age = i;} //A
void print() {cout << "name: " << name << " age: " << age;}
};
这个类包含两个数据成员,前者的类型是string,后者是int。此外,它还包括一个成员函数
(方法)print,用来将该类对象的信息打印出来。注意在C++中,我们需要用一个分号来表
示类定义的结束。
在第A 行,我们提供了User 类的一个构造函数。构造函数是与类同名的特殊成员函数,没
有返回值,用来对该类的对象进行初始化。有了这一构造函数,我们就可以用下列两种方法
之一来创建这个类的一个对象实例。
User u("Bob", 20);
User *p = new User("Bob", 20);
第一种方法在堆栈上为对象u 分配内存,当u 离开它的作用域之后,其内存会自动释放。第
二种方法在堆上为对象分配内存,并将这块内存的地址赋给指针p。以后这块地址需要调用
delete 操作符显式地进行释放。需要指出的是,C++允许类的一个成员函数的实现代码可以
位于类定义的外部。在C++中,如果一个成员函数的实现代码是在类定义本身提供的,这个
函数便被认为是在线的(inline)。函数是否为在线的会影响编译器执行某些优化措施的能力。
通过显式地声明,一个在类定义外部定义的成员函数也可以是在线的。
在Java 中,我们可以作出一个等价的类定义。
class User {
private String name;
private int age;
public User(String str, int i) {name = str; age = i;}
public void print() {System.out.print ("name: " + name + " age: " + age);}
}
在这个类中,我们同样定义了两个数据成员。其中,name 成员的类型是String,这是Java
的字符串类型。与C++不同的是,Java 的类定义不需要以一个分号结束。
我们在上述的类中提供了一个构造函数。有了这一构造函数,我们就可以象下面这样而且只
能这样创建这种类型的对象。
User u = new User("Bob", 20);
该表达式右边部分的调用创建了一个User 类型的新对象并返回对该对象的引用。
3.1.2 访问控制
类的每个成员都有一个相关的访问控制属性。在C++中,成员的访问控制属性是private(私
有)、protected(保护)、public(公共)三者之一。如果没有显式地声明一个成员的访问控
制属性,那么它就按照缺省情况作为这个类的私有成员。因此,在上面User 类的例子中,
数据成员name 和age 的访问控制属性是private,而构造函数和方法print 的访问控制属性是
public。
对于某个类的private 成员,它的名字将只能由该类的成员函数和友元使用。对于某个类的
protected 成员,它的名字只能由该类的成员函数和友元,以及由该类的子类的成员函数和友
元使用。而对于某个类的public 成员,它的名字可以由任何函数使用。
在Java 中,限定符public 和private 与它们在C++中的含义相同。至于protected,含义与C++
中的类似,只不过在同一个程序包中这类成员就像是公共成员一样。也就是说,我们可以在
同一个程序包的所有类中访问一个类的保护成员,但是在这个包之外,只有这个类的子类可
以访问这些成员。除了private、protected 和public 之外,Java 中还有一个访问控制限定符
package。对于package 成员,在同一个程序包内它们就像是公共成员,而在包的外部它们就
像是私有成员。此外,如果我们没有为一个成员指定访问限定符,那么这个成员的访问控制
属性就是package。
3.1.3 对象的复制与赋值
在C++中,按照默认规定,类对象可以进行复制。特别是可以用同一个类的对象的复制对该
类的其他对象进行初始化。例如:
X a = b; //通过复制初始化
按照默认方式,类对象的复制就是其中各个成员的复制。如果某个类X 所需要的不是这种
默认方式,那么就可以定义一个复制构造函数X::X(const X&),由它提供所需要的行为。类
似地,类对象也可以通过赋值进行按默认方式的复制。例如:
a = b;
其中a 和b 都是类X 的对象。在这里也一样,默认的语义就是按成员复制。如果对于某个
类而言这种方式不是正确选择,那么用户可以重载赋值运算符,由它提供所需要的行为。
在Java 中,情况要简单一些。对于语句
X a = b;
其语义并不是对象的复制,而是变量a 得到了变量b 所保存的那个对象的引用的一份拷贝。
也就是说,在初始化之后,a 和b 都是对同一个对象的引用。如果我们希望a 所引用的是b
所引用的那个对象的一份拷贝,那么我们可以通过调用clone 方法来实现。该方法是从根类
Object 继承而来的,这里就不细述了。
3.1.4 对象的终结处理
在对象销毁之前,可能需要做一些最后动作,例如释放对象所占用的各种资源,维护有关状
态等等。在C++中,终结动作以类的析构函数的形式定义。析构函数的名称就是类名前面加
一个波浪号。它不应接受任何参数,也不应返回任何值。对于堆栈上的类的对象,在其作用
域退出时,它的析构函数会被自动调用。对于堆上的类的对象,我们需要显式地释放它们所
占的存储。在释放之前,析构函数会被自动调用。如果一个类没有显式地提供一个析构函数,
那么系统在销毁这个类的对象之前会执行析构函数的缺省行为。这个缺省行为就是依次调用
类的每个数据成员的析构函数。
Java 的对象销毁方式与C++有着很大的不同。前面说过,Java 提供了自动废料收集的机制。
在一个Java 程序中,如果没有任何变量保存了对一个对象的引用,那么这个对象就成为了
废料收集的候选目标。在实际销毁一个对象之前,如果该对象存在finalize 方法,那么废料
收集器就会执行其中的代码。Java 的finalize 方法所执行的任务类似于C++中程序员所提供
的析构函数。然而,执行终结动作的时间是不可预计的,存在时间上和顺序上的不确定性。
3.2 类继承
3.2.1 子类的定义、相关概念
继承是面向对象程序设计的基本概念之一。通过继承一个已有的类,我们可以对类进行扩展。
对于上面给出的User 类的例子,我们可以定义该类的一个子类StudentUser。
//在C++中定义子类
class StudentUser : public User {
string school;
public:
StudentUser(string str, int i, string s) : User(str, i) {school = s;}
void print() {
User::print();
cout << " School: " << school;
}
};
//比较。在Java 中定义类似的子类
class StudentUser extends User {
private String school;
public StudentUser(String str, int i, String s) {
super(str, i);
school = s;
}
public void print() {
super.print();
System.out.print(" School: " + school);
}
}
在上述定义的基础上,我们称User 是一个父类、基类或超类,StudentUser 是一个子类、派
生类或扩展类。在定义子类时,C++和Java 中的书写格式有所不同。C++中的类定义的头部
“class StudentUser : public User”表示StudentUser 类是User 类的一个子类。在Java 中的写
法则是“class StudentUser extends User”。子类能够继承基类的所有成员,但它不能访问基
类的私有成员。在上面的例子中,由于name 和age 是基类User 的私有成员,因此子类
StudentUser 中的任何成员函数(方法)都无法访问它们。注意子类StudentUser 的构造函数。
在C++和Java 的例子中,我们分别通过“User(str, i)”和“super(str, i)”调用了父类的构造
函数,对那些只能由基类的成员函数进行访问的数据成员进行赋值。实际上,一个派生类的
构造函数在执行任何任务之前必须调用它的基类的构造函数。如果在派生类的构造函数中并
没有显式地调用基类的构造函数,系统将会试图调用基类的无参构造函数(缺省构造函数)。
在上面的例子中,子类StudentUser 的print 函数的定义覆盖了它从基类User 继承而来的同
名函数的定义。然而,我们仍然可以访问被隐藏的基类函数,例如C++中的“User::print();”
以及Java 中的“super.print();”。最后需要指出的是,基类的指针或引用可以安全引用子类
的对象,但子类的指针或引用不能引用基类的对象。这一点在C++和Java 中是一致的。
3.2.2 C++的不同继承方式
C++提供了三种继承方式,以控制对基类成员的访问。第一种是public 继承,也就是通常情
况下的继承方式。记子类为D 而基类为B,那么子类头部的写法是“class D : public B”。在
public 继承中,基类的public 成员变成子类的public 成员,protected 成员变成子类的protected
成员。注意基类的private 成员在子类中不可访问。第二种是protected 继承,子类头部的写
法是“class D : protected B”。在protected 继承中,基类的public 成员和protected 成员都变
成派生类的protected 成员。第三种是private 继承,子类头部的写法是“class D : private B”。
在private 继承中,基类的public 成员和protected 成员都变成派生类的private 成员。在编程
中,我们可以根据实际的需要来选择适当的继承方式。其中,public 继承是最为常用的。
3.2.3 阻断继承、Java 中的关键字final
有的时候,我们会有这样的一种需要,就是希望某个类不能被继承。比如说,编译程序如果
预先能够知道一个类不能被扩展,那么它可能会在函数调用的数据访问方式的优化上做得更
好。另外,出于安全方面的一些考虑,我们也有可能决定不让用户对销售商所提供的类进行
扩展。Java 提供了一种方便的方法,如果我们在一个类的头部以关键字final 作为前缀,那
么这个类就不能被扩展。在Java 中,我们也可以只把一个类的其中一些方法声明为final,
从而实现有选择地控制继承的目的。当超类中的一个方法被声明为final 时,我们就无法在
子类中对它进行覆盖。相比之下,C++并没有提供一个类似final 的关键字来防止一个类被
扩展或者有选择地对覆盖机制进行阻断。然而我们知道,一个派生类的构造函数在执行任何
任务之前必须调用它的基类的构造函数。因此,如果我们把基类的构造函数放在类的私有部
分,那么这个类就不能被扩展。
3.3 方法的动态约束
面向对象语言里的方法调用通常采用“x.m(...)”的形式。其中,x 是一个指向或者引用对象
的变量,m 是x 的定义类型的一个方法。现在的问题是,“x.m(...)”所调用的方法是根据什
么确定的?有两种可能性。如果是根据变量x 的类型静态确定,我们就称之为静态约束;如
果是根据方法调用时被指向或引用的对象的类型确定,我们就称之为动态约束。动态约束是
面向对象程序设计的一个非常重要的概念。动态约束带来的优点是明显的,它使得我们能够
编写程序来通用化地处理一个层次中的所有类的对象。然而,采用动态约束调用时会带来一
些额外开销,如果需要调用的方法能够静态确定的话,采用静态约束有速度优势。动态约束
方法的另一个缺点是不能做在线展开(inline)处理。
在Java 语言里,所有方法都采用动态约束。与此不同的是,C++提供了静态约束和动态约
束两种方式,以静态约束作为默认方式,而把动态约束的方法称为虚方法(virtual 方法)。
C++之所以会这么做,是与它的C 基础有关。
我们来举一个简单的Java 例子来说明方法的动态约束。
//Test.java
class User {
private String name;
private int age;
public User(String str, int i) {name = str; age = i;}
public void print() {System.out.print ("name: " + name + " age: " + age);}
}
class StudentUser extends User {
private String school;
public StudentUser(String str, int i, String s) {
super(str, i);
school = s;
}
public void print() {
super.print();
System.out.print(" School: " + school);
}
}
class Test {
public static void main(String[] args) {
User u = new User("Bob", 20);
User su = new StudentUser("Chris", 25, "Math");
u.print();
System.out.println();
su.print();
System.out.println();
}
}
这是一个完整的Java 程序。在main 方法中,我们定义了两个User 类型的变量。变量u 引
用了一个User 类型的对象,而变量su 则引用了一个StudentUser 类型(User 的子类)的对
象。我们对变量u 调用方法print,与对变量su 调用方法print,调用的是不同的方法。这是
由于Java 采用的是方法的动态约束(假如采用的是静态约束的话,调用的就是同一个方法)。
该程序输出的结果是:
name: Bob age: 20
name: Chris age: 25 School: Math
(假如采用的是静态约束的话,输出结果中就不会有“School: Math”)。
3.3.1 方法覆盖的限制
在C++中,当一个方法在基类中被声明为虚拟方法时,我们可以在派生类中用一个同名的方
法对它进行覆盖。然而,覆盖不是任意的,要受到一些条件的限制。第一,当基类方法的返
回值是基本类型时,派生类的覆盖方法的返回类型必须和基类方法相同;当方法的返回类型
是一个指向类B 类型的指针或引用时,派生类的覆盖方法的返回类型可以是指向类B 的子
类型的指针或引用类型。第二,基类虚拟方法的访问控制属性对于派生类的覆盖定义是否合
法没有影响。即使基类的虚拟方法位于保护或私有部分,派生类的覆盖方法也可以出现在派
生类的任何一个部分。第三,如果基类的虚拟方法带有异常说明,那么派生类的覆盖方法就
不允许抛出这个异常说明之外的异常。
在Java 中,关于方法的覆盖也有一些限制。第一,派生类覆盖方法的返回类型必须和基类
中被覆盖方法的返回类型相同。与C++的规定不同,Java 对覆盖方法的返回类型的限制与
返回类型是基本类型还是类类型无关。第二,覆盖方法的访问限制不能比基类的被覆盖方法
的访问限制更严格。因此,如果基类方法的访问限制属性是保护,那么派生类的覆盖方法的
访问限制可以是保护或公共,但不可以是私有。与C++不同的是,Java 中在类的私有部分
声明的方法是不能被覆盖的。第三,派生类中一个覆盖方法的异常说明必须是基类中被覆盖
方法的异常说明的一个子集。这一点与C++中的相应限制是类似的。
3.4 抽象类、接口
一般而言,如果我们无法从一个类创建对象,那么这个类就是抽象类。导致这个结果的原因
不止一种。在C++中,如果一个类的一个或多个成员函数被声明为纯虚函数,那么我们就无
法从这个类创建对象。在Java 中,如果我们对一个类显式地使用关键字abstract 进行声明,
那么我们也不能从这个类创建任何对象。
如果我们无法由一个类创建对象,那么这个类有什么用呢?首先,抽象类可以在一个类层次
体系中起到组织其它类的作用。其次,抽象类可以表示一种特殊的行为,当它与其它类混合
使用时,可以允许我们创建具有这种行为的类。此外,抽象类可以帮助我们以增量的方式创
建类的实现代码。
在面向对象程序设计中,抽象类可以作为一个包含了一些具体类的类层次体系的根系,把那
些分散封装在各个子类中的共同概念归纳在一起。例如,圆形和矩形等都是形状,我们可以
写一个具体类Circle 来描述圆形,写一个具体类Rectangle 来描述矩形,……,我们也可以
写一个抽象类Shape 来描述形状。显然,为Circle 和Rectangle 类创建对象是合理的,而为
Shape 类来创建对象就显得没有意义。Shape 类在这里扮演了一个关键的角色,它把其它的
类组织到一个单一的类层次体系中。需要说明的是,“组织在一起”这个概念实际上比看上
去要复杂的多,它涉及到了继承和动态约束。利用继承,我们可以把各个子类中的公共代码
放在Shape 类中,使程序的效率更高。假如我们有一个Shape 变量(引用或指针)的列表,
其中有些变量所引用的实际是Circle 类型的对象,还有一些引用的则是Rectangle 等类型的
对象。我们可以在整个列表中调用一个诸如area 的方法,动态约束会使列表中每个变量都
调用到适当的area 方法。
下面是我们在C++中定义抽象类的一个简单例子。
class Shape {
public:
virtual double area() = 0; //A
//...
};
其中,第A 行的符号“= 0”告诉编译器这个函数是纯虚函数。编译器要求纯虚函数不包含
任何实现代码。前面提到过,如果一个C++类具有至少一个纯虚函数,那么这个类就是一个
抽象类。
在Java 中,抽象类是按照下面这种方式定义的
abstract class Shape {
abstract public double area();
//...
}
Java 要求抽象类的头部以关键字abstract 开头。同时,如果我们的类包含了任何我们不想提
供实现代码的方法,我们在这个方法的声明中也包含这个关键字。
除了抽象类之外,Java 还支持接口的概念。接口是一种只由抽象方法和常量组成的类。Java
中声明接口的语法如下。
interface Collection {
public boolean add(Object o);
public boolean remove(Object o);
//...
}
在接口中声明的方法访问控制类型始终是公共的,即使它们并没有显式地用public 关键字进
行声明也是如此。而且,接口中的方法也不允许声明为静态方法。此外,Java 的接口还可以
包含常量。在接口中定义的常量被隐式地作为了final 和静态处理。当一个类继承了该接口
时,这些常量就像是在这个类中局部定义的一样。需要说明一下该例中的类Object。Java
采用的是单根的类层次结构。也就是说,Java 有一个唯一的根超类,这个类就是Object,其
它所有的类都是由它直接或间接地派生出来的。与此不同的是,C++采用的是任意的类层次
结构。
现在又出现了一个问题。为什么C++只用抽象类就可以了,而Java 需要同时使用抽象类和
接口?这与多重继承有关。C++允许多重继承而Java 不允许。Java 只允许从一个基类继承
实现代码。虽然这个限制消除了多重继承所带来的许多编程难题,但它同时也导致了一些自
身的设计限制,不得不使用接口的概念予以解决。一个Java 类只可以是一个基类的子类,
却可以是任意多个接口的子类。
3.5 C++的多重继承
C++允许一个类从多个基类继承实现代码,这称为多重继承。在面向对象程序设计中,一个
实体可能同时从多个来源继承了属性。在这种情况下,采用多重继承就显得是一件自然的事
情。一方面,多重继承是一种功能强大的编程工具;然而另一方面,多重继承会带来一些缺
点。在一个类层次结构中,当一个派生类从两条不同的路径继承了某个基类的相同成员时,
会导致许多复杂的问题。此外,多重继承也会带来实现方面的困难。
与C++不同的是,Java 中不允许一个类从多个基类继承实现代码。然而,Java 允许一个类
从多个接口继承各种行为。我们称之为混入式继承。
4. 小结 

本文旨在讨论C++与Java 这两种语言在基本语言特征和面向对象机制方面的异同。然而,
由于篇幅和个人水平所限,文中只是讨论了这些内容中的一部分,有许多方面未涉及到,例
如异常处理等等。
C++和Java 是两种目前常用的面向对象程序设计语言。它们在许多方面都有着值得比较的
地方。相对而言,C++的许多规定都比较灵活,甚至把一些细节留给了实现,而Java 几乎
把所有的问题都规定得很严格,没有什么更改的余地。之所以会出现这样的差异,与这两种
语言的设计目标有关。C++这样做,是因为C++的设计追求灵活性和高效性,而Java 这样
做,是因为Java 的设计追求安全性和平台无关性。在这种意义上讲,C++和Java 是两种成
功的面向对象程序设计语言。