深入了解 C++ 与 Java
已发表: 2022-07-22无数文章比较了 C++ 和 Java 的技术特性,但哪些差异最需要考虑? 例如,当比较显示 Java 不支持多重继承而 C++ 支持时,这是什么意思? 这是一件好事吗? 一些人认为这是 Java 的一个优势,而另一些人则认为这是一个问题。
让我们探讨一下开发人员应该选择 C++、Java 或其他语言的情况,更重要的是,为什么这个决定很重要。
检查基础:语言构建和生态系统
C++ 于 1985 年作为 C 编译器的前端推出,类似于 TypeScript 编译为 JavaScript 的方式。 现代 C++ 编译器通常编译为本机机器代码。 尽管有些人声称 C++ 的编译器降低了它的可移植性,并且它们确实需要为新的目标架构重新构建,但 C++ 代码几乎可以在每个处理器平台上运行。
Java 于 1995 年首次发布,并不直接构建为本机代码。 相反,Java 构建字节码,一种在 Java 虚拟机 (JVM) 上运行的中间二进制表示。 换句话说,Java 编译器的输出需要特定于平台的本机可执行文件才能运行。
C++ 和 Java 都属于类 C 语言,因为它们的语法通常类似于 C。 最显着的区别在于它们的生态系统:虽然 C++ 可以无缝调用基于 C 或 C++ 或操作系统 API 的库,但 Java 最适合基于 Java 的库。 您可以使用 Java 本机接口 (JNI) API 访问 Java 中的 C 库,但它容易出错并且需要一些 C 或 C++ 代码。 C++ 也比 Java 更容易与硬件交互,因为 C++ 是一种低级语言。
详细权衡:泛型、内存等
我们可以从多个角度将 C++ 与 Java 进行比较。 在某些情况下,C++ 和 Java 之间的决定是明确的。 除非应用程序是游戏,否则原生 Android 应用程序通常应该使用 Java。 大多数游戏开发者应该选择 C++ 或其他语言以获得尽可能流畅的实时动画; Java 的内存管理经常会导致游戏过程中出现延迟。
非游戏的跨平台应用程序超出了本次讨论的范围。 在这种情况下,C++ 和 Java 都不是理想的,因为它们对于高效的 GUI 开发来说过于冗长。 对于高性能应用程序,最好创建 C++ 模块来完成繁重的工作,并为 GUI 使用更具开发人员生产力的语言。
非游戏的跨平台应用程序超出了本次讨论的范围。 在这种情况下,C++ 和 Java 都不是理想的,因为它们对于高效的 GUI 开发来说过于冗长。
鸣叫
对于某些项目,选择可能不清楚,所以让我们进一步比较:
特征 | C++ | 爪哇 |
---|---|---|
适合初学者 | 不 | 是的 |
运行时性能 | 最好的 | 好的 |
潜伏 | 可预见 | 不可预料的 |
引用计数智能指针 | 是的 | 不 |
全局标记和清除垃圾收集 | 不 | 必需的 |
堆栈内存分配 | 是的 | 不 |
编译为本机可执行文件 | 是的 | 不 |
编译成 Java 字节码 | 不 | 是的 |
与低级操作系统 API 直接交互 | 是的 | 需要 C 代码 |
与 C 库的直接交互 | 是的 | 需要 C 代码 |
与 Java 库直接交互 | 通过 JNI | 是的 |
标准化的构建和包管理 | 不 | 马文 |
除了表中比较的特性外,我们还将关注面向对象编程 (OOP) 特性,如多重继承、泛型/模板和反射。 请注意,两种语言都支持 OOP:Java 强制要求它,而 C++ 支持 OOP 以及全局函数和静态数据。
多重继承
在 OOP 中,继承是指子类从父类继承属性和方法。 一个标准示例是继承自更通用的Shape
类的Rectangle
类:
// Note that we are in a C++ file class Shape { // Position int x, y; public: // The child class must override this pure virtual function virtual void draw() = 0; }; class Rectangle: public Shape { // Width and height int w, h; public: void draw(); };
多重继承是指子类从多个父类继承。 下面是一个示例,使用了Rectangle
和Shape
类以及一个附加的Clickable
类:
// Not recommended class Shape {...}; class Rectangle: public Shape {...}; class Clickable { int xClick, yClick; public: virtual void click() = 0; }; class ClickableRectangle: public Rectangle, public Clickable { void click(); };
在这种情况下,我们有两种基本类型: Shape
( Rectangle
的基本类型)和Clickable
。 ClickableRectangle
继承自两者以组成两种对象类型。
C++支持多重继承; Java 没有。 多重继承在某些边缘情况下很有用,例如:
- 创建高级领域特定语言 (DSL)。
- 在编译时执行复杂的计算。
- 以 Java 中根本不可能的方式提高项目类型的安全性。
但是,通常不鼓励使用多重继承。 除非与模板元编程结合使用,否则它会使代码复杂化并影响性能——最好只有最有经验的 C++ 程序员才能做到这一点。
泛型和模板
适用于任何数据类型的类的通用版本对于代码重用是实用的。 两种语言都提供这种支持——Java 通过泛型,C++ 通过模板——但 C++ 模板的灵活性可以使高级编程更安全、更健壮。 每次您在模板中使用不同类型时,C++ 编译器都会创建新的自定义类或函数。 此外,C++模板可以根据顶层函数的参数类型调用自定义函数,允许特定数据类型有专门的代码。 这称为模板特化。 Java 没有等效的功能。
相反,当使用泛型时,Java 编译器通过称为类型擦除的过程创建没有类型的通用对象。 Java 在编译期间执行类型检查,但程序员不能根据其类型参数修改泛型类或方法的行为。 为了更好地理解这一点,让我们看一个使用 C++ 模板template<class T1, class T2>
的通用std::string format(std::string fmt, T1 item1, T2 item2)
函数的快速示例我创建的库:
std::string firstParameter = "A string"; int secondParameter = 123; // Format printed output as an eight-character-wide string and a hexadecimal value format("%8s %x", firstParameter, secondParameter); // Format printed output as two eight-character-wide strings format("%8s %8s", firstParameter, secondParameter);
C++ 将生成format
函数为std::string format(std::string fmt, std::string item1, int item2)
,而 Java 将在没有item1
和item2
的特定string
和int
对象类型的情况下创建它。 在这种情况下,我们的 C++ 模板知道最后一个传入参数是一个int
,因此可以在第二个format
调用中执行必要的std::to_string
转换。 如果没有模板,C++ printf
语句试图将数字打印为字符串,就像在第二种format
调用中一样,将具有未定义的行为,并可能使应用程序崩溃或打印垃圾。 Java 函数在第一次format
调用中只能将数字视为字符串,而不能直接将其格式化为十六进制整数。 这是一个简单的例子,但它展示了 C++ 选择一个专门的模板来处理任意类对象而不修改其类或format
函数的能力。 我们可以使用反射而不是泛型在 Java 中正确生成输出,尽管这种方法可扩展性较差且更容易出错。
反射
在 Java 中,可以(在运行时)找出结构细节,例如在类或类类型中哪些成员可用。 这个特性被称为反射,大概是因为它就像举起一面镜子对着物体看里面有什么。 (更多信息可以在 Oracle 的反射文档中找到。)
C++ 没有完全反射,但现代 C++ 提供运行时类型信息 (RTTI)。 RTTI 允许对特定对象类型进行运行时检测,但它无法访问对象成员等信息。
内存管理
C++ 和 Java 之间的另一个关键区别是内存管理,它有两种主要方法:手动,开发人员必须手动跟踪和释放内存; 和自动的,其中软件跟踪哪些对象仍在使用以回收未使用的内存。 在 Java 中,垃圾收集就是一个例子。
Java 需要垃圾收集内存,提供比手动方法更容易的内存管理,并消除通常会导致安全漏洞的内存释放错误。 C++ 本身不提供自动内存管理,但它支持一种称为智能指针的垃圾回收形式。 智能指针使用引用计数,如果使用正确,是安全且高性能的。 C++ 还提供了在对象销毁时清理或释放资源的析构函数。
虽然 Java 仅提供堆分配,但 C++ 支持堆分配(使用new
和delete
或较旧的 C malloc
函数)和堆栈分配。 堆栈分配比堆分配更快更安全,因为堆栈是线性数据结构,而堆是基于树的,因此堆栈内存的分配和释放要简单得多。
C++ 与堆栈分配相关的另一个优点是一种称为资源获取即初始化 (RAII) 的编程技术。 在 RAII 中,引用等资源与其控制对象的生命周期相关联; 资源将在该对象的生命周期结束时被销毁。 RAII 是 C++ 智能指针在没有手动取消引用的情况下工作的方式——在函数顶部引用的智能指针在退出函数时会自动取消引用。 如果这是对智能指针的最后一次引用,则连接的内存也会被释放。 尽管 Java 提供了类似的模式,但它比 C++ 的 RAII 更尴尬,尤其是当您需要在同一个代码块中创建多个资源时。
运行时性能
Java 具有可靠的运行时性能,但 C++ 仍然占据桂冠,因为手动内存管理比实际应用程序的垃圾收集更快。 尽管由于 JIT 编译,Java 在某些极端情况下可以胜过 C++,但 C++ 赢得了大多数非平凡的情况。
特别是,与 C++ 减少堆分配的使用相比,Java 的标准内存库的分配使垃圾收集器过度工作。 但是,Java 仍然相对较快,并且应该可以接受,除非延迟是最重要的问题——例如,在具有实时限制的游戏或应用程序中。
构建和包管理
Java 在性能上的不足之处在于它的易用性弥补了这一点。 影响开发人员效率的一个组件是构建和包管理——我们如何构建项目并将外部依赖项引入应用程序。 在 Java 中,一个名为 Maven 的工具将这个过程简化为几个简单的步骤,并与 IntelliJ IDEA 等许多 IDE 集成。
然而,在 C++ 中,不存在标准化的包存储库。 甚至没有在应用程序中构建 C++ 代码的标准化方法:一些开发人员更喜欢 Visual Studio,而另一些开发人员则使用 CMake 或其他自定义工具集。 进一步增加了复杂性,某些商业 C++ 库是二进制格式的,并且没有一致的方法将这些库集成到构建过程中。 此外,构建设置或编译器版本的变化可能会给二进制库的工作带来挑战。
初学者友好
构建和包管理的摩擦并不是 C++ 远不如 Java 对初学者友好的唯一原因。 程序员可能难以安全地调试和使用 C++,除非他们熟悉 C、汇编语言或计算机的低级工作。 把 C++ 想象成一个强大的工具:它可以完成很多事情,但如果滥用就会很危险。
Java 的上述内存管理方法也使其比 C++ 更易于访问。 Java 程序员不必担心释放对象内存,因为该语言会自动处理这些问题。
决策时间:C++ 还是 Java?
现在我们已经深入探讨了 C++ 和 Java 之间的差异,我们回到我们最初的问题:C++ 还是 Java? 即使对这两种语言有深入的了解,也没有一刀切的答案。
不熟悉低级编程概念的软件工程师在将决定限制为 C++ 或 Java 时可能会更好地选择 Java,但游戏等实时环境除外。 另一方面,希望扩展视野的开发人员可能会通过选择 C++ 了解更多信息。
但是,C++ 和 Java 之间的技术差异可能只是决定的一个小因素。 某些类型的产品需要特定的选择。 如果您仍然不确定,您可以查阅流程图——但请记住,它最终可能会将您指向第三种语言。