深入了解 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 之間的技術差異可能只是決定的一個小因素。 某些類型的產品需要特定的選擇。 如果您仍然不確定,您可以查閱流程圖——但請記住,它最終可能會將您指向第三種語言。