C++编程:与Java的比较
Java编程语言和C++有很多共同点。以下是这两种语言的比较。要更深入地了解Java,请参阅Java编程维基教科书。
Java最初是为了支持嵌入式系统上的网络计算而创建的。Java的设计目标是极度可移植、安全、多线程和分布式,而这些都不是C++的设计目标。Java的语法对C程序员来说很熟悉,但没有保持与C的直接兼容性。Java也专门设计得比C++更简单,但它仍在继续发展,超越了这种简化。
在1999年到2009年之间,特别是在专注于企业解决方案的编程行业部分,“基于Java”的语言,依靠在Smalltalk中常见的“虚拟机”,变得越来越突出。这是性能和生产力之间的权衡,在当时计算能力和对简化和更精简语言的需求(不仅允许轻松采用,而且学习曲线更低)非常合理的情况下,这是有意义的。这两种语言之间有足够的相似之处,熟练的C++程序员可以轻松地适应Java,即使在今天,与C++相比,Java在某些方面也更简单,甚至在采用的范式方面也更一致。
然而,这种兴趣的转变已经减少,主要是因为语言的演变。C++和Java的演变很大程度上弥合了两种语言的问题和局限性之间的差距,如今的软件需求也发生了变化,并且更加碎片化。现在,我们对移动、数据中心和桌面计算有特定的需求,这使得编程语言的选择成为一个更加核心问题。
C++和Java之间的区别是
- C++的解析比Java稍微复杂一些;例如,如果Foo是一个变量,则
Foo<1>(3);
是一系列比较,但如果Foo是类模板的名称,则它会创建一个对象。 - C++允许命名空间级别的常量、变量和函数。所有此类Java声明必须位于类或接口内部。
- C++中的
const
表示数据为“只读”,并应用于类型。Java中的final
表示变量不能重新赋值。对于基本类型,例如const int
与final int
,它们是相同的,但对于复杂类,它们是不同的。 - C++在C++11标准之前不支持构造函数委托,而且只有最近的编译器才支持这一点。
- C++生成在硬件上运行的机器代码,Java生成在虚拟机上运行的字节码,因此使用C++,您可以获得更大的功能,但代价是可移植性。
- C++,int main()本身就是一个函数,没有类。
- C++访问说明符(public、private)使用标签并分组进行。
- C++对类成员的访问默认是private,在Java中是包访问。
- C++类声明以分号结尾。
- C++缺乏语言级别的垃圾回收支持,而Java具有内置的垃圾回收来处理内存释放。
- C++支持
goto
语句;Java不支持,但它的带标签的break和带标签的continue语句提供了一些结构化的类似goto
的功能。事实上,Java强制执行结构化控制流,目标是使代码更容易理解。 - C++提供了一些Java缺少的底层功能。在C++中,指针可用于操作特定的内存位置,这是编写底层操作系统组件所必需的任务。类似地,许多C++编译器支持内联汇编程序。在Java中,仍然可以通过Java本地接口以库的形式访问汇编代码。但是,每次调用都会产生很大的开销。
- C++允许在原生类型之间进行一系列隐式转换,并且还允许程序员定义涉及复合类型的隐式转换。但是,Java仅允许原生类型之间的扩展转换是隐式的;任何其他转换都需要显式的强制转换语法。C++11禁止从初始化列表进行缩窄转换。
- 其结果是,尽管Java和C++中的循环条件(
if
、while
和for
中的退出条件)都期望布尔表达式,但诸如if(a = 5)
之类的代码将在Java中导致编译错误,因为从int到boolean没有隐式缩窄转换。如果代码是if(a == 5)
的打字错误,这将非常有用,但是当将诸如if (x)
之类的语句从Java转换为C++时,需要显式强制转换会增加冗长性。
- 其结果是,尽管Java和C++中的循环条件(
- 对于向函数传递参数,C++同时支持真正的传引用和传值。与C一样,程序员可以使用传值参数和间接寻址来模拟传引用参数。在Java中,所有参数都是按值传递的,但对象(非基本)参数是引用值,这意味着间接寻址是内置的。
- 通常,Java内置类型具有指定的大小和范围;而C++类型具有各种可能的大小、范围和表示形式,甚至可能在同一编译器的不同版本之间发生变化,或者可以通过编译器开关进行配置。
- 特别是,Java字符是16位的Unicode字符,字符串由一系列此类字符组成。C++提供了窄字符和宽字符,但每个字符的实际大小取决于平台,使用的字符集也取决于平台。字符串可以由任一类型组成。
- C++中浮点值和运算的舍入和精度取决于平台。Java提供了一个严格的浮点模型,该模型保证跨平台的一致结果,尽管通常使用更宽松的操作模式来允许最佳的浮点性能。
- 在 C++ 中,指针 可以直接作为内存地址值进行操作。Java 没有指针——它只有对象引用和数组引用,两者都不允许直接访问内存地址。在 C++ 中,可以构造指向指针的指针,而 Java 引用只能访问对象。
- 在 C++ 中,指针可以指向函数或成员函数(函数指针 或 仿函数)。Java 中等效的机制使用对象或接口引用。C++11 对函数对象提供了库支持。
- C++ 支持程序员自定义的 运算符重载。Java 中唯一重载的运算符是“
+
”和“+=
”运算符,它们既可以连接字符串,也可以执行加法运算。 - Java 提供了对 反射 和 动态加载任意新代码的标准 API 支持。
- Java 有泛型。C++ 有模板。
- Java 和 C++ 都区分原生类型(也称为“基本”或“内置”类型)和用户定义类型(也称为“复合”类型)。在 Java 中,原生类型只有值语义,而复合类型只有引用语义。在 C++ 中,所有类型都具有值语义,但可以为任何对象创建引用,这将允许通过引用语义操作对象。
- C++ 支持任意类的 多重继承。Java 支持类型的多重继承,但只支持单一实现继承。在 Java 中,一个类只能派生自一个类,但一个类可以实现多个 接口。
- Java 明确区分接口和类。在 C++ 中,多重继承和纯虚函数使得定义的功能与 Java 接口相同的类成为可能。
- Java 在语言和标准库中都支持 多线程。Java 中的
synchronized
关键字 提供简单安全的 互斥锁来支持多线程应用程序。C++11 提供了类似的功能。虽然在早期版本的 C++ 中可以通过库获得互斥锁机制,但缺乏语言语义使得编写 线程安全代码更加困难且容易出错。
- Java 需要自动 垃圾回收。C++ 中的内存管理通常由手工完成,或通过 智能指针完成。C++ 标准允许垃圾回收,但不强制要求;在实践中很少使用垃圾回收。当允许重新定位对象时,现代垃圾回收器可以提高整体应用程序的空间和时间效率,而不是使用显式释放。
- C++ 可以分配任意大小的内存块。Java 只能通过对象实例化来分配内存。(注意,在 Java 中,程序员可以通过创建字节数组来模拟任意内存块的分配。但是,Java 数组是对象。)
- Java 和 C++ 使用不同的习惯用法进行资源管理。Java 主要依赖垃圾回收,而 C++ 主要依赖 RAII(资源获取即初始化)习惯用法。这反映在两种语言之间的一些差异上。
- 在 C++ 中,通常将复合类型的对象分配为局部栈绑定变量,这些变量在它们超出范围时会被销毁。在 Java 中,复合类型始终在堆上分配并由垃圾回收器回收(除非在使用 逃逸分析将堆分配转换为栈分配的虚拟机中)。
- C++ 有析构函数,而 Java 有 终结器。两者都在对象释放之前调用,但它们有很大差异。C++ 对象的析构函数必须隐式(对于栈绑定变量)或显式调用以释放对象。析构函数在程序中对象被释放的点同步执行。因此,C++ 中同步、协调的初始化和释放满足了 RAII 习惯用法。在 Java 中,对象的释放由垃圾回收器隐式处理。Java 对象的终结器在其最后一次访问之后和实际释放之前异步调用,这可能永远不会发生。很少有对象需要终结器;只有必须保证在释放之前清理对象状态的对象(通常是释放 JVM 外部的资源)才需要终结器。在 Java 中,使用 try/finally 结构执行安全的同步资源释放。
- 在 C++ 中,可能存在 悬空指针——指向已销毁对象的 引用;尝试使用悬空指针通常会导致程序失败。在 Java 中,垃圾回收器不会销毁引用的对象。
- 在 C++ 中,可能存在已分配但无法访问的对象。无法访问的对象是没有可访问的引用的对象。无法访问的对象无法销毁(释放),会导致 内存泄漏。相比之下,在 Java 中,对象只有在变得无法访问(由用户程序)时才会被垃圾回收器释放。(注意:支持 弱引用,它与 Java 垃圾回收器一起使用,以允许不同的可访问性强度。)Java 中的垃圾回收可以防止许多内存泄漏,但在某些情况下仍然可能发生泄漏。
- C++ 标准库提供了一组有限的基本且相对通用的组件。Java 有一个大得多的标准库。C++ 可以通过(通常是免费的)第三方库获得这些额外的功能,但第三方库没有提供与标准库相同的无处不在的跨平台功能。
- C++ 与 C 大部分 向后兼容,并且 C 库(例如大多数 操作系统的 API)可以直接从 C++ 访问。在 Java 中,其标准库更丰富的功能提供了对许多通常仅在特定于平台的库中可用的功能的 跨平台访问。从 Java 直接访问本机操作系统和硬件功能需要使用 Java 本地接口。
- C++ 通常直接编译为 机器码,然后由 操作系统直接执行。Java 通常编译为 字节码,然后 Java 虚拟机(JVM)将其 解释或 JIT 编译为机器码,然后执行。
- 由于某些 C++ 语言特性(例如未检查的数组访问、原始指针)的使用缺乏约束,编程错误可能导致低级的 缓冲区溢出、页面错误和 段错误。标准模板库提供了更高级别的抽象(如向量、列表和映射)来帮助避免此类错误。在 Java 中,此类错误要么根本不会发生,要么由 JVM检测到并以 异常的形式报告给应用程序。
- 在Java中,所有数组访问操作都会隐式地执行边界检查。在C++中,对原生数组的数组访问操作不会进行边界检查,而对于像std::vector和std::deque这样的标准库集合的随机访问元素访问,边界检查是可选的。
- Java和C++使用不同的技术将代码分割成多个源文件。Java使用包系统来规定所有程序定义的文件名和路径。在Java中,编译器导入可执行的类文件。C++使用头文件源代码包含系统来共享源文件之间的声明。
- C++中的模板和宏,包括标准库中的模板和宏,可能会导致编译后出现类似代码的重复。其次,与标准库的动态链接消除了在编译时绑定库。
- C++编译具有文本预处理阶段,而Java没有。Java支持许多优化措施,可以减少对预处理器的需求,但一些用户会在其构建过程中添加预处理阶段,以更好地支持条件编译。
- 在Java中,数组是容器对象,您可以随时检查其长度。在这两种语言中,数组的大小都是固定的。此外,C++程序员通常只通过指向其第一个元素的指针来引用数组,从中他们无法检索数组的大小。但是,C++和Java都提供了容器类(分别为std::vector和java.util.ArrayList),这些类是可调整大小的,并存储其大小。C++11的std::array提供了固定大小的数组,其效率与经典数组类似,具有返回大小的函数,以及可选的边界检查。
- Java的除法和模运算符被明确定义为截断为零。C++没有指定这些运算符是截断为零还是“截断为负无穷大”。-3/2在Java中始终为-1,但C++编译器可能会
返回
-1或-2,具体取决于平台。C99以与Java相同的方式定义除法。两种语言都保证(a/b)*b + (a%b) == a
对于所有a和b(b != 0)都成立。C++版本有时会更快,因为它允许选择处理器本机的任何截断模式。 - 整数类型的尺寸在Java中是定义好的(int为32位,long为64位),而在C++中,整数和指针的尺寸取决于编译器。因此,精心编写的C++代码可以利用64位处理器的功能,同时仍然可以在32位处理器上正常运行。但是,在编写时未考虑处理器字长的C++程序在某些编译器上可能无法正常运行。相反,Java固定的整数大小意味着程序员无需关心不同的整数大小,程序将以完全相同的方式运行。这可能会导致性能损失,因为Java代码无法使用任意处理器的字长运行。C++11提供了诸如uint32_t之类的类型,这些类型具有保证的大小,但编译器并不强制要求在没有对该大小提供原生支持的硬件上提供它们。
计算性能是衡量硬件和软件系统执行计算工作(例如算法或事务)时资源消耗的指标。更高的性能被定义为“使用更少的资源”。感兴趣的资源包括内存、带宽、持久存储和CPU周期。由于现代台式机和服务器系统上除了后者之外的所有资源都非常充足,因此性能在口语中通常指最少的CPU周期;这通常直接转化为最少的挂钟时间。比较两种软件语言的性能需要一个固定的硬件平台和(通常是相对的)两种或多种软件子系统的测量结果。本节比较了C++和Java在Windows和Linux等常见操作系统上的相对计算性能。
早期的Java版本在性能方面明显不如C++等静态编译语言。这是因为这两种密切相关的语言的程序语句可以用C++编译成少量机器指令,而当由Java JVM解释时,则编译成大量字节码,每个字节码涉及多个机器指令。例如
Java/C++语句 | C++生成的代码 | Java生成的字节码 |
---|---|---|
vector[i]++; | mov edx,[ebp+4h] mov eax,[ebp+1Ch] |
aload_1 iload_2 |
虽然对于嵌入式系统来说,由于需要占用空间小,这种情况可能仍然存在,但是即时(JIT)编译器技术在长期运行的服务器和桌面Java进程中的进步缩小了性能差距,并在某些情况下使Java获得了性能优势。实际上,Java字节码在运行时被编译成机器指令,其方式类似于C++静态编译,从而产生类似的指令序列。
目前,即使在低级和数值计算方面,C++在大多数操作中的速度也仍然快于Java。有关详细信息,您可以查看Java与C++的性能比较。它有点偏向Java,但非常详细。
C和C++程序员之间可能会对导入的工作原理感到困惑,反之,Java程序员也可能对包含文件的正确使用方法感到困惑。在现代编程语言中的符号表导入与#includes(如C和C++中)的使用之间进行比较。尽管这两种技术都是解决同一问题的方案,即跨多个源文件进行编译,但它们是截然不同的技术。由于几乎所有现代编译器都包含基本相同的编译阶段,因此最大的区别可以用以下事实来解释:包含发生在编译的词法分析阶段,而导入则要等到语义分析阶段才进行。
导入的优点
- 导入不会重复任何词法分析工作,这通常会导致大型项目的编译速度更快。
- 导入不需要将代码拆分为单独的文件进行声明/实现。
- 导入更好地促进了对象代码而不是源代码的分发。
- 导入允许源文件之间的循环依赖关系。
- 导入隐式地提供了一种机制,用于在多个符号表定义相同符号时解决符号冲突。
导入的缺点
- 当可导入模块发生更改时,由于没有定义和实现的分离,因此所有依赖模块都必须重新编译,这在大型项目中可能需要大量的编译时间。
- 导入需要一种在对象代码中定义符号表的标准机制。这种限制是否真的是一个弱点尚有争议,因为标准符号表对于许多其他原因都很有用。
- 导入需要一种在编译时发现符号表的方法(例如Java中的类路径)。但是,当存在一种标准方法来执行此操作时,这并不一定比指定包含文件的位置更复杂。
- 当允许循环依赖关系时,可能需要交错处理多个相互依赖的源文件的语义分析。
- 除非语言包含对部分类型的支持,否则使用导入而不是包含的语言要求类的所有源代码都位于单个源文件中。
包含的优点
- 使用包含,源文件在语义分析阶段之间没有相互依赖关系。这意味着在此阶段,每个源文件都可以作为独立的单元进行编译。
- 将定义和实现分离到头文件和源文件中减少了依赖关系,并且在实现细节发生更改时,只需要重新编译受影响的源文件,而不需要其他文件。
- 与其他预处理器功能结合使用的包含文件允许进行几乎任意的词法处理。
- 尽管这种做法并不普遍,但如果语言本身不支持,包含可以为几个现代语言特性(例如Mixin和方面)提供基本的支持。
- 包含不是底层语言语法的一部分,而是预处理器语法的一部分。这样做有一些缺点(需要学习另一种语言),但也有一些优点。预处理器语法,在某些情况下包括文件本身,可以在几种不同的语言之间共享。
包含的缺点
- 包含和必需的预处理器可能需要在编译的词法分析阶段进行更多遍处理。
- 在大型项目中重复编译多次包含的头文件可能非常慢。但是,可以通过使用预编译头文件来缓解这种情况。
- 对于初学者来说,正确使用头文件,特别是全局变量的声明,可能很棘手。
- 由于包含通常需要在源代码中指定包含文件的位置,因此通常需要环境变量来提供包含文件路径的一部分。更糟糕的是,此功能在所有编译器之间都没有以标准方式支持。
一个比较C++和Java的比较示例这里存在。