Java与C++

概要

Java并不仅仅是C++语言的一个变种,它们在某些本质问题上有根本的不同。

  • Java比C++程序可靠性更高。C++语言在提供强大的功能的同时也提高了程序含bug的可能性,Java语言通过改变语言特性来大大提高了程序的可靠性。
  • Java语言不需要程序对内存进行分配和回收。Java丢弃了C++中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。特别的,Java语言不使用指针,并提供了自动的废料收集,内存的分配和回收是自动进行的,程序员无需考虑内存碎片的问题。
  • Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”。Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序语言中常见的因为数组操作越界等指针操作而对系统数据进行非法的读写带来的不安全问题。
  • Java使用接口技术取代C++程序中的多继承性。接口和多继承有同样的功能,但是省却了多继承在实现共和维护上的复杂性。

Java引用与C++指针

Java语言让编程者无法找到指针来直接访问内存,并且增添了自动的内存管理功能,从而有效的防止了C/C++语言中指针操作失误,如野指针所造成的系统奔溃,但不是说Java没有指针,虚拟机内部还是使用了指针,只是外人不得使用而已。这有利于Java程序的安全性。

Java的引用和C++的指针都是指向一块内存块的,通过引用或者指针完成对内存数据的操作,但它们在实现、原理、作用等方面有所区别。

  • 类型:引用其值为地址的数据元素,Java封装了地址,可以转换为字符串查看,长度可以不必关心。C++指针是一个存放地址的变量,长度一般是计算机字长,可以认为是int。
  • 所占内存:引用声明时没有实体,不占内存。C++如果声明后用才会赋值,如果用不到不会分配内存。
  • 类型转换:引用的类型转换,也可能不成功,运行时抛异常或者编译就不能通过。C++指针只是一个内存地址,指向哪里,对程序来说都还是一个地址,但可能所指的地址不是程序想要的。
  • 初始化:引用初始化为Java关键字null。C++指针是int,如不初始化指针,它的值就不固定,这就很危险。
  • 计算:引用是不能计算的。C++指针是int,它是可以计算的,如++或–,所以经常用指针来代替数组下标。
  • 控制:引用不可以计算,所以它只在自己的程序中,可以被控制。C++指针是内存地址,可以计算,所以它有可能张指向一个不属于自己程序使用的内存地址,对于其它程序来说是很危险的,对自己程序来说是不容易控制的。
  • 内存泄漏:Java引用不会产生内存泄漏。C++指针是容易产生内存泄漏的,所以程序员要小心使用,即使回收。
  • 作为参数:Java的方法参数只传值,引用作为参数使用时,会给方法内引用的copy,所以在方法中交换两个引用参数是没有意义的,因为方法值交换的是copy值,但是在方法改变一个引用参数的属性是有意义的,因为引用参数的copy所引用的对象和引用参数是同一个对象。C++指针作为参数给函数使用,实际上就是它所指的地址在被函数操作,所以函数的操作都将直接作用到指针所指向的地址(变量、对象、函数等)。

本质上,它们两个都是想通过一个叫做引用或者指针的东西,找到操作的目标,方便在程序中操作。所不同的是Java的办法更方便、更安全一些,但失去了C++的灵活性,也算是对指针的一种包装和改进。

Java接口和C++多重继承

C++之菱形继承

C++支持多重继承,这是C++的一个特征,它允许多父类派生一个类。尽管多重继承功能很强,但使用复杂,而且会引起很多麻烦,编译程序实现它也很不容易。

如上图,菱形继承即多个类继承同一个公共基类,而这些派生类又同时被一个继承。这样,上图的D类的对象模型里面就保存了两份Base,当我们想要调用我们从Base里继承的fun函数时候就会出现调用不明确的问题,并且会造成 数据冗余的问题。而解决这个问题的办法大体上有两种,一种就是使用域限定我们所需访问的函数,但是这样做非常不方便,并且当程序十分庞大的时候会造成我们的思维混乱;另一种就是C++给我们提供的解决办法——虚继承。

C++之虚继承

如上图,虚继承在让A和B在继承Base的时候加上virtual关键字。

可以看到在A和B中不再保存Base中的内容,而是保存了一份偏移地址,然后将Base的数据保存在一个公共位置处,这样在就保证了数据冗余性降低的同时,也能直接在调用Base的fun函数的时候不引起混乱问题。

Java接口

Java也提供了继承机制,但Java的继承机制只是单一继承,Java另外提供了一个叫interface的概念,用来代替C++的多重继承,同时又避免了C++中的多重继承实现方式带来的诸多不便。

从Java程序设计语言的角度来看,Java的interface则表示,一些函数或字段成员,为另一些属于不同类别的物件所需共同拥有,则将这些函数与字段成员,定义在一个interface中,然后让所有不同类别的Java物件可以共同操作使用之。

所以,对于Java的继承和interface,可以做出如下小结:

  • Java的class只能继承一个父类,但是可以实现多个接口(interface)。
  • Java的接口可以继承多个别的接口,但是不可以实现任何接口。
介面继承与实现继承
  • 介面继承

    只继承父类别的函数名称,然后子类别一定会实现取代之。所以当我们以父类别的指标多型于各子类别时,由于子类别一定会实现父类别的多型函数,所以每个子类别的实现都不一样,此时我们(使用父类别指标的人)并不知道此函数到底怎么完成,因此成为黑箱设计。

  • 实现继承

    就是继承父类别的函数名称,也会用到父类别的函数实现。所以我们(使用父类别指标的人)知道此函数怎样完成工作,因为大概也跟父类别的函数实现差不多,因此成为白箱设计。

Java的interface就是介面继承,因为Java的interface只能定义函数名称,无法定义函数实现,所以子类别必须用implements关键字实现之,且每个实现同一个介面的子类别当然彼此不知道对方是如何实现,因此为一个黑箱设计。

Java的类别继承就是实现继承,子类别会用到父类别的实现,所以父类别与子类别有一定程度的相关性。

Java与C++的内存管理

Java程序中所有的对象都是用new操作符建立在内存堆栈上的,这个操作符类似于C++的new操作符。同时new一个对象,C++一定得手动delete掉,而且得时刻记住能delete的最早时间(避免使用空指针)。Java可以存活于作用域之外,也就是说如果要使用某一对象或其引用,它的内存就不会被释放。Java有一个垃圾回收期Garbage Collector,作用是自动把不需要的对象内存给释放掉,这样就消除了内存泄漏的问题。在内存管理这一点上,可以说C++灵活性更好,Java健壮性更好,虽然事实上Java虚拟机内部还是使用指针。

Java和C++的内存区
  • 栈内存空间:保存所有对象的名称(更准确地说是保存了引用的堆内存空间地址)
  • 堆内存空间:保存每个对象的具体属性内容
  • 全局数据区:保存static类型的属性
  • 全局代码区:保存所有的方法定义
  • 程序计数器

总的来说,Java new的对象在堆中,引用变量在栈中;对应的,C++ new的对象在堆中,对象指针变量在栈中。但是可以非new方法创建对象,对象在栈中,对象名只是一个名称,对应了某个内存位置,不占空间。

Java和C++的内存回收原理

无论是Java还是C++,都是通过根集(寄存器、程序栈、静态数据段中指针的集合)出发来找可达的指针。

而C++的原生指针(raw pointer,即一般我们自己声明的指针)不受控制,所以一般不建议使用这种指针,而应使用智能指针。智能指针有如下几种:std::auto_ptr,boost library的scoped_ptr、share_ptr、weak_ptr和intrusive_ptr(不常见)。

  • auto_ptr所指向的对象在作用域之外会自动得到析构,不支持复制
  • scoped_ptr与auto_ptr一致,但两者存在一个主要差别,auto_ptr特意被设计为指针的所有权是可转移的,可以用函数之间传递,同一时刻只能有一个auto_ptr指针,而scoped_ptr将构造函数和赋值函数都声明为私有,拒绝了指针所有权的转让从而保证了指针的绝对安全
  • shared_ptr是通常理解的智能指针,shared_ptr中所实现的本质就是引用计数,也就是说shared_ptr是支持复制的,复制一个shared_ptr的本质是对这个智能指针的引用次数加1,而当这个智能指针的引用次数降低到0的时候,该对象就会自动被析构
  • weak_ptr和shared_ptr的最大区别在于weak_ptr在指向一个对象的时候不会增加引用计数,因此可以用weak_ptr去指向一个对象并且在weak_ptr仍然指向这个对象的时候析构它,此时再去访问weak_ptr的时候,weak_ptr其实返回的回事一个空的shared_ptr

Java中垃圾回收器可以自动回收无用对象占据的内存,但它只负责释放Java中创建的对象所占据的所有内存,通过某种创建对象之外的方式为对象分配的内存空间则无法被垃圾回收器回收,而且垃圾回收器本身也有一定的开销,GC的优先级比较低,所以如果JVM没有面临内存耗尽,它是不会去浪费资源进行垃圾回收以回复内存的。

JVM通过GC判断方法来判断Java对象是否需要被回收。

  • 引用计数:引用计数记录着每一个对象被其他对象所持有的引用数,被引用一次就加一,引用失效就减一,引用计数器为0则说明该对象不再可用,当一个对象被回收后,被该对象所引用的其他对象的引用计数都应该相应减少,它很难解决对象之间的相互循环引用问题
  • 可达性分析算法:从GC Root对象向下搜索其所走过的路径成为引用链,当一个对象不再被任何的GC Root对象引用链相连时说明该对象不再可用,GC Root对象包括四种,方法区常量和静态变量引用的对象,虚拟机栈中变量引用的对象,本地方法栈中引用的对象。解决循环引用是因为GC Root通常是一组特别管理的指针,这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针
  • 采用引用计数算法的系统只需在每个实例对象创建之初,通过计数器来记录所有的引用次数即可。而可达性算法,则需要再次GC时,遍历整个GC根节点来判断是否回收

Java对象的四种引用

  • 强引用:创建一个对象并把这个对象直接赋值给一个变量,不管系统资源有多么紧张,强引用的对象都绝对不会被回收,即使以后不会再用到
  • 软引用:通过SoftReference类实现,内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断它是否已经被回收了
  • 弱引用:通过WeakReference类实现,不管内存是否足够,系统垃圾回收时必定会回收
  • 虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知,通过PhantomReference类和引用队列ReferenceQueue类联合使用实现

Java与C++的数据类型及类型转换

Java是完全面向对象的语言,所有函数和变量都必须是类的一部分。除了基础数据类型之外,其余的都作为类对象,包括数组。对象将数组和方法结合起来,把它们封装在类里面,这样每个类都可以实现自己的特点和行为。而C++允许将函数和变量定义为全局的,此外,Java中取消了C/C++中的结构和联合,取消了不必要的麻烦。

在C和C++中有时出现数据类型的隐式转换,这就涉及了自动强制类型转换问题。例如,C++中可将一浮点值赋予整形变量,并去掉其尾数。Java不支持C++中的自动强制类型转换,如果需要,就必须有程序显式进行强制类型转换。

Java与C++的字符串

C和C++不支持字符串常量,在C和C++程序中使用null终止符代表字符串的结束,在Java中字符串是用类对象(String和StringBuffer)来实现的,这些类对象是Java语言的核心,用类对象实现字符串有以下几个优点。

  • 在整个系统中建立字符串和访问字符串元素的方法是一致的
  • Java字符串类是作为Java语言的一部分定义的,而不是作为外加延伸部分
  • Java字符串执行运行时检空,可帮助排除一些运行时发生的错误
  • 可对字符串用“+”进行连接操作

所有字符串类都源于C语言的字符串,而C语言字符串则是字符的数组。C语言中是没有字符串的,只有字符数组。

C++的字符串

C++提供两种字符串的表示:C风格的字符串和标准C++引入的string类型。一般建议用string类型,但是实际情况还是要使用老式C风格的字符串。

  • C风格字符串:C风格字符串起源于C,并在C++中得到扩展。字符串存储在一个字符数组中,例如:

    1
    2
    const char *str = "test"; // 这里用常量字符数组来表示字符串。操作字符串的时候只要操作指针就可以了
    const char *p = str; // 然后对p进行操作就可以了
  • 标准C++的string类型:如果用的话首先要引入头文件,#include 。在C++中提供的标准字符串类型有如下操作:

    • 支持用字符序列或者第二个字符串去初始化一个字符串对象。C风格的字符串不支持用另外一个字符串初始化另外一个字符串
    • 支持字符串之间的copy。C风格字符串通过strcopy()函数来实现
    • 支持读写访问单个字符。对于C风格的字符串,只有解除引用或者通过下标操作才能访问单个字符串
    • 支持两个字符串相等比较。对于C风格的字符串,比较是通过strcmp()函数来实现的
    • 支持两个字符串连接。对于C风格的字符串用strcopy()函数copy到另一个新的实例中,然后用strcat()把两个字符串串连起来
    • 支持对字符串长度的查询,size()函数可以得到字符串长度
Java的字符串

在Java中,String不属于8种基本类型,所以String是对象,默认值是null。在Java中 == 是对地址的比较,而equals是对内容的比较。下面来看一个栗子。

1
2
3
4
5
6
7
8
9
10
11
String str1 = "test";
String str2 = new String("test");
System.out.println(str1 == str2); //false
System.out.println(str1.equals(str2)); //true
String str3 = "zhang";
String str4 = "san";
String str5 = "zhangsan";
String str6 = "zhangsan";
str3 += str4;
System.out.println(str3 == str5); //true
System.out.println(str5 == str6); //true

当我们将一个字符串赋给一个字符串变量的时候,例如,String str1 = “test”,会先去常量池中找有没有“test”的字符串拷贝,如果有的话,把str1的地址指向常量池中字符串常量“test”的地址,如果没有则在常量池中建立“test”的字符串常量。而当使用new String(“test”)创建的字符串对象是放在堆内存中的,其有自己的内存空间。

Java中的String和StringBuffer

  • String:是对象不是原始类型,为不可变对象,一旦被创建,就不能修改它的值,对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去,String是final类,即不能被继承。
  • StringBuffer:是一个可变对象,当对它进行修改的时候不会像String那样重新建立对象,他只能通过构造函数来建立。不能通过赋值符号对它进行赋值。对象被创建后,在内存中就会分配内存空间,并初始保存一个null,向StringBuffer中赋值的时候可以通过它的append方法,字符串连接操作中StringBuffer的效率比String高。事实上,String的连接操作的处理步骤是通过建立一个StringBuffer对象,然后调用append方法,最后再调用StringBuffer对象的toString方法,所以效率上String对象就慢了。