程序设计语言

透过c++看java

2009年5月5日 阅读(272)

说明:最初写在bmy bbs上,现整理在此处,转载请注明作者:phylips@bmy,谢谢合作。

先来个序。

 

版上又有坑出现啊。最近,大概花了三天时间看了下java编程思想,中间也看了一部日剧,last friends,比较非主流的类型。虽然题目是透过c++看java,我也并不是一个纯正的c++er,其实我的语言经历大概跟某些ser差不多,最初都是从c和java开始的,c->java->c++->c->c++,大概是这样一个轨迹。老实说争论语言的优劣并无多大用处,现存的语言都有其优点,基本形成互补之势。但是不同的公司,因其业务的需求,可能某些语言会占优,但是也不会完全摒弃其他语言吧,语言是为满足人类需求而出现的,学了java去说c++不好或好也没啥必要,到底学啥,要看你未来的目标公司的偏好,当然这个也没有起到决定作用,现在公司大概会在笔试的时候对语言进行考察,有的也会提供c/c++,java独立的试题吧。很多公司,面试的时候没有对语言做定性规定,学好一门语言比学哪一门更重要些,大概是这样的吧。所以学习语言的能力是重要的,但是不是说你说一句“学习语言的能力是重要的”,就代表着你不用学习语言了,因为这种能力的获取需要学一门语言来培养。

 

最好的状况,大概是各种类型的语言各掌握一门,面向对象,脚本,函数式语言之类的,最好至少精于一门,其他的熟练。

 

其实以前用java写的代码多,后来读的c/c++书籍多,写点算法之类的也就用c++。大概就是这个情况。因为我不是一个java白痴,所以也不能以java初学者的c++er们的眼光看待java。看了这本java编程思想,我还是从两种语言的设计上来比较它们吧。Thinking 系列出版很早了,在thinking in java中,随处可以看到作者把java与c++的比较,猜测他大概是先写的thinking in c++,然后写的thinking in java吧。

 

语言的出现都有其设计的初衷,也有其倾向处理的问题域。人月神话的作者发表过一篇文章,"没有银弹",任何一门语言不是银弹,面向对象也不是银弹。每个工具都有其擅长的地方,很多这种特长源于其原来的设计初衷,当然有些则是逐渐演化得到的,具有偶然性。

 

c++和java有着完全不同的设计理念。下面大概从类型设计,初始化,内存管理,名字空间,访问控制,面向对象语义与实现(继承,多态,封装),运行模型,异常处理,容器与算法,库方面说下。主要是做个笔记,以后看起来更方便些。

 

 

(一)类型设计

 

c++在尽量保持与c的兼容性的时候,java选择了可移植性。c++考虑的是类型的效率,java考虑的是类型使用时的安全。

 

从c说起

c++和java的基本类型都是从c继承下来的。而c语言的类型的概念,则是当初获得成功的重要原因,而c语言的前身BCPL语言,是无类型的语言。类型概念紧密结合了计算机的底层结构,同时把这样的底层结构方便的用类型进行了抽象,也就是说类型实际上代表了底层的抽象物。同时丰富的类型概念,方便了底层优化,针对不同的类型可以进行专门的优化,节省存储空间。当然由此带来了使用众多类型的负担,同时类型间的转换也成为很多错误的来源。然而毫无疑问,类型的概念是c语言获得巨大成功的重要原因。

 

基本态度

 

对于c++来说,一开始便是从c开始的,最初称为带类的c,与c的兼容性在很多设计决策中起了很多作用。在类型设计中,表现在类型保持了兼容c的,虽然有了class,但依然保留了struct,决定性的原因就是兼容性。而java最初设计的时候,就是以一门面向对象语言来设计的,从c,c++,smalltalk里学习,但并没有保持向上兼容性,以一门新的语言的姿态出现。最终对class的处理上,c++尽量保持了与基本类型的相容性,让class表现的就像一个基本类型。一个class定义就与基本类型一样,是一组数据实体,同时引入了运算符重装机制,使得class更可以像一个基本类型那样+-*/…。java对于class则采用了另一个方向,与基本类型区别鲜明,也没有运算符重载。为对象引入了一个新的间接层,每个class的对象就是一个引用。这样的设计一方面把内存的管理交给了垃圾回收器,另一方面使得容器实现时,可以将所有的对象一致对待,间接层的引入提高了抽象层次,同时带来了间接引起的访问开销。

 

类型大小

 

c和c++的类型体系里,存在一个很大的隐患,类型大小没有明确的定义,由实现确定,比如对于long类型只是保证至少32位。一方面这样的设计有利于效率的提高,可以根据实际情况选择最适合的大小,比如实际机器字的长度。但是另一方面则带来了可移植性的问题,因为一个int在不同的地方可能就是不同的范围,这就可能引起问题。而java呢,可移植性是其重要的设计目标,它对的类型大小都在语言的层面进行了规定,也就不存在一个int一会是16位,一会是32位的问题,这样就可以提供更好的移植性。这个性质带来了另一个额外的影响,java取消了sizeof运算符,这个运算符是在c,c++中用来得到类型大小的,而由于java对基本类型大小进行了严格限定,另一方面它的class对象又都是引用,所以它的存在也就没有意义了。另外对于运算符方面,java提供了一些新的运算符扩展,比如移位,c++对于有符号数的右移没有明确的定义,java则提供了>>和>>>进行明确的区分,移位的可移植性由此获得。

 

类型的选择

 

另外java没有结构体,union。原因就是class可以替代struct的功能。而union,是内存吃紧的产物吧,同时行为类似于c++的reinterpret_cast,这也是最不安全的类型转换,没有了它程序依然可以实现。可能会有一个问题,以前c/c++里都是用union来判断大小端的,那java如何判断大小端呢?怎么回答呢,大概是这样的,java的大小端已经被完全隐藏了,java也不允许依赖于大小端的代码。当然如果你真的想判断,好吧有个方法:调用本地方法。

 

Java由于没有运算符重载,这样对于==,确定了其意义也就是比较地址。这样就意味着对于对象,==,只能用来判断对象地址,如果要判断对象内容必须重载equals函数,而==的意思是无法再被改变的。

 

同时另一个重要的选择,java抛弃了指针,这个也是c最强大的武器之一。反映了安全性与效率之间的选择,c++选择了效率和兼容性,但是java选择了安全。同时这也是很多程序员对java诟病的原因之一,因为指针是一个伟大的概念,它的缺失可能引起一代程序员概念的缺失。的确指针更接近计算机的底层,java将它封装起来,让程序员远离了指针的泥潭,同时远离了对底层的理解。

 

类型转换

 

c++的类型转换,尤其是隐式转换,随处可见,但是类型转换一般被视为设计缺陷的标志。尤其是隐式的转换往往在排除错误的时候变得异常艰难。好的语言设计,应该是让错误尽早显示,不是隐藏错误。虽然作为人来说,本性更喜欢隐藏错误,当然坦白承认错误的品性才是值得肯定的。c++对c的类型转换体制进行了修补,提出了新的类型转换机制,***_cast系列,当然同样是兼容性的问题,新旧转换机制的混合使得c++的类型转换显得更加复杂,如何有效利用c++的新机制呢,结果就是需要程序员规范类型转换,自觉的使用新的类型转换取代老的机制。

 

java的类型转换则进行了更严格的规定,首先boolean类型无法转换为其他类型,也就是boolean类型只能用在条件判断里,不要想把它用在整数运算里,这更符合boolean的自然语义。基本类型在其他方面与c++的差别不大。对于class类型,c++中允许通过隐式调用构造函数实现类型转换,但是这一支持有时候很让人惊奇,所以我更支持java的做法,至少能让我们更了解自己写的程序。java并不允许这样的转换,class的自动转换大概只有toString()一个,在需要字符串的时候,会自动调用类的这个函数。

 

 

(二)初始化语义

 

未初始化之痛

 

由于未初始化造成的错误屡见不鲜,cpp版应该出现过几次。一般来说,使用前忘记对一个变量初始化,这样会使程序将直接获取那个位置上次的内容,加上在内存释放时并没有对该内存区域清零,这样这个值就是随机的,通常这样的使用肯定是个错误。初始化要做的工作,就是保证你用的是你知道的,在你使用它之前确保它的状态是正确的。

 

C语言初始化语义

 

C语言中,如果不对变量进行显式的初始化,对于外部变量和静态变量将被初始化为0,而自动变量和寄存器变量则没有定义,也就是说是什么值是不确定的。

 

概念的困扰

 

对于声明,定义以及初始化的概念,一般也比较混乱,声明不会伴随变量的分配,定义则伴随着空间分配,而初始化与赋值的区别在于初始化一般发生在空间分配后的第一次。之所以说这些概念比较混乱,是因为比如某些定义,同时也是声明和初始化。再比如如果根据初始化实行者,也会产生不同的叫法,比如如果程序员直接定义int a;但是编译器保证a为0,是否可以称为a已经是初始化了呢。当然如果把初始化定义为显式的初始化,那么上面也可以说a未初始化。也就是说定义一个变量,可能产生以下几种初始状态:1.采用了该内存上次的值 2.编译器对该内存点清0 3.程序员对该内存点进行了显示的初始化。当然只有3才可以称为安全的状态,同时如果状态2是在程序员了解的情况下,也是可以的。而1基本上可以确定就是个错误。

 

数组初始化

 

数组的初始化,可以通过初始化表达式:int days[] ={0,1,2,};c++的初始化列表大概源于此。如果初始化表达式的个数小于数组元素,则对外部变量,静态变量,自动变量来说,则会被自动初始化为0。由于对自动变量不进行显示的初始化,虽然是未显示的初始化,通常也叫做未初始化,这样就导致写出一些使用未初始化的代码,这样的代码通常是错误的。

 

C++初始化语义

 

C++的初始化,首先基本上接受了c的初始化语义,但进行了扩充,因为c++里有了class。比如允许基本类型具有class的初始化表达式形式:int ival (1024); (注意有出现将()与[]混淆的错误。即Int a=new int(5);与int a = new int[5]的区别。)同时由于类变量的初始化方式,使得可以接受以变量对全局变量进行初始化。比如int a =5;int b = a;(然而c语言不可以,因为它要求必须初始化式是常量表达式)。对于这样的一个初始化,大概是这样的一个过程,基本上就是类变量的一个过程。全局变量b会被分配空间,同时被初始化为0,当程序运行时,才会对b用a进行初始化,或者称为赋值。

 

对于静态变量跟自动变量,c/c++均采取了不同的初始化语义。关于静态变量概念,参见以前cpp版我的某贴。静态变量初始化语义,对于一个基本类型的静态变量保证初始化为其对应的0值,比如指针为NULL,bool为false,int 为0。对于类类型的静态变量,则有如下保证:全局对象要求在main函数之前完成初始化,如果是静态局部对象,他的初始化是在该函数第一次执行的时候才完成初始化,对于类中的成员变量,允许基本类型不进行显示的初始化,如果没有显示的初始化,则保证其值为0,也就是0初始化保证,但如果类对象为自动变量则无此保证,至于这个保证如何实现的,参见:初始化之实现方法。而自动变量,与静态变量相比一个很大的区别就是缺少了0初始化的保证。

 

总结一下:对于变量的初始化,c/c++语义如下,对于静态变量,如果没有显示的初始化,编译器会进行0初始化。而自动变量则没有提供这样的保证。另外对于类类型,无论静态还是自动变量,都要调用构造函数进行初始化。

 

初始化之实现方法

 

那么如何实现这样的初始化语义呢?首先看一个概念BSS,“Block Started by Symbol”的缩写,意为“以符号开始的块”。 “以符号开始的块”指的是编译器处理未初始化数据的地方。BSS节不包含任何数据,只是简单的维护开始和结束的地址,以便内存区能在运行时被有效地清零。BSS节在应用程序的二进制映象文件中并不存在。实际上它的存在是为了降低二进制的映像的大小,节省空间(举个例子说明,我们定义int a[5000],如果放到data段需要5000个0,但是我们知道它肯定是用0来初始化,这样只要记住a开始和结束的地址,赋0就可以了,但如果我们int a[5000] ={1,2,3,4,5,6,…},如果我们不把这些信息保存到二进制的映像中,运行时我们怎么知道用何值来初始化呢,也就是这种情况必须放到data段,但是上述情况我们可以放到bss段,可见bss段的确可以减少映像文件大小)。在这里,对于这样的全局变量int a[10000];实际上就被归入未初始化之列,虽然会进行清零。也就像上面所说的关于初始化这一概念在不同的环境下,实际上可以有不同的特指。也就是说在这里,认为int a = 5;是已初始化变量,要放入data段,而int a;则认为是未初始化,放入bss段,即使编译器保证a在使用时为0。

 

对于一个基本类型变量,如果是静态初始化的,它在编译时便已被固化到data段了。当程序执行,可执行文件被加载时,这时那个空间的值便用这个值填充,对于基本类型是如此。而对于类类型,要完成上面的初始化语义,必须由编译器做更多额外工作。比如要保证这些静态类对象的构造函数要在main之前调用,析构函数在结束后调用。再比如如果是local静态变量,还需要保证只有调用了构造函数的条件下,才能调用析构函数。

 

早期的c++是这样实现静态对象的初始化的:

 

编译器检查所有源文件,找出所有的静态变量,为每个源文件生成一个_sti开头的函数,在这个函数里,调用相应的构造函数。然后将所有的_sti函数放到一个函数里,_main,把_main插入到main函数usercode之前。这样就可以保证在main执行前进行完成初始化。实际上在这之前还有一个过程,对于这些静态变量的空间,进行清零,而之后的构造函数只是在这片空间上重新进行了一些赋值操作,未被赋值的区域仍然保留了0值。当然还有对应的析构函数也是类似处理。而对于local静态变量,一个实现方法是引入一个辅助的标志变量,用来标志是否初始化过,之后的初始化和析构就可以根据这个标志变量的值决定是否进行。

 

另外c/c++对于不同编译单元的全局变量的初始化顺序都没有提供保证,也就意味着不能写出依赖于全局变量初始化顺序的代码。那java呢?

 

Java的初始化

 

Java有个重要区别,没有了全部变量,一切都是以类的形式组织的,当然可以用类的static变量来模拟c风格的全局变量。全局变量是个方便而危险的东西,危险导致本质上的不方便。初学者喜欢全局变量的这种用法,只要在某个地方定义了,其他地方都可以用,这真是万能的变量。但是全局变量增加了依赖性,很难支持多线程,或者作为库使用,同时遍布各地的全局变量降低了可读性。舍弃这种用法是个很好的防止滥用的方法。

 

所以java中的变量类型由类静态变量,类成员变量,类方法的局部变量组成。

 

首先变量类型减少了,与此相比c++里的变量类型却庞杂的多,由全局变量-local变量,静态变量-非静态变量,成员变量,加上名字空间组合起来,复杂得多。而java就这么几种,另外这些变量的生存期相比c++变得更短。比如c++的全局静态变量在程序运行之前建立,结束后销毁,而java则是类加载时才产生。

 

此外,java对初始化提供了强烈保证,用以杜绝c中由使用未初始化变量引起的问题。对于方法的局部变量,不会自动初始化,如果程序员没有显示的初始化动作,会报编译错误,对于类成员变量则提供了0初始化保证,也就是允许成员变量不进行显式初始化。无论怎样你都不会让一个变量的值依赖于系统的随机值了,提供了初始值保证。

 

Java中的类型比较简单,要麽是基本类型,直接定义为函数的内部变量或者类的变量,要么是类对象类型,必须通过new得来,当然如果你不去new,一个那个引用的初始值就是null,使用它比然引起运行时异常。再比如int [] d = new int[100];数组内容会自动初始化为0,c++则不会保证这样new出来的数组的初始值为0。

 

对于一个变量,比较安全的初始化可能采取两种策略,一是如果没有显示初始化,则由系统保证为0,另一种是直接编译报错。实际上直接报错应该是更好更安全的做法。当然这也是上面java采用过的两种初始化方式,而c/c++则允许未经程序员或者系统初始化的变量的存在,从而开启了潘多拉的盒子。

 

反过来问,为什么c没有提供这样的保证呢?

C是这样想的,对于一个变量我要尽量延后初始化时间,实际上早期的c是不允许对局部变量进行初始化的,一般通过使用前在进行赋值。这样有什麽好处呢,首先节省空间,因为它可以放到bss段,其次效率高,可以避免不必要的初始化,再者编译器不必为了保证初值做额外的工作,实现起来更简单。使用时赋值还带来了另一个好处,就是增强了可读性,方便快速看到变量初始值,因为早期的c语言只能把变量定义集中到开头,这样的规定就带来了延后变量定义式的好处。但是忽略了另一个问题,程序员经常忘了在使用前为这个变量赋值,最常见的是忘了给它赋0。但是到了今天,那点节省的时空已经微不足道,同时变量可以随时定义,所以消除这样的问题变的更加重要,也就是java采取的策略。

 

上面的文字主要是对变量的初始值,在语义中有何保证进行了说明,以及提供这样的保证的原因,及实现方法。对于何时进行初始化,以及如何进行具体的初始化过程,实际上也是一个比较复杂的过程,涉及到构造函数的语义,这个得单独再来一篇。

 

You Might Also Like