Dogłębne spojrzenie na C++ i Java

Opublikowany: 2022-07-22

Niezliczone artykuły porównują funkcje techniczne C++ i Javy, ale które różnice są najważniejsze do rozważenia? Kiedy porównanie pokazuje, na przykład, że Java nie obsługuje wielokrotnego dziedziczenia, a C++ tak, co to oznacza? A czy to dobrze? Niektórzy twierdzą, że jest to zaleta Javy, podczas gdy inni deklarują, że jest to problem.

Przyjrzyjmy się sytuacjom, w których programiści powinni wybrać C++, Javę lub zupełnie inny język — i, co ważniejsze, dlaczego decyzja ma znaczenie.

Badanie podstaw: konstrukcje językowe i ekosystemy

C++ został wprowadzony na rynek w 1985 roku jako nakładka na kompilatory C, podobnie jak TypeScript kompiluje się do JavaScript. Nowoczesne kompilatory C++ zazwyczaj kompilują się do natywnego kodu maszynowego. Chociaż niektórzy twierdzą, że kompilatory C++ zmniejszają jego przenośność i wymagają przebudowy dla nowych architektur docelowych, kod C++ działa na prawie każdej platformie procesorowej.

Wydana po raz pierwszy w 1995 r. Java nie tworzy bezpośrednio w kodzie natywnym. Zamiast tego Java buduje kod bajtowy, pośrednią reprezentację binarną, która działa na wirtualnej maszynie Java (JVM). Innymi słowy, dane wyjściowe kompilatora Java wymagają do uruchomienia natywnego pliku wykonywalnego specyficznego dla platformy.

Zarówno C++, jak i Java należą do rodziny języków podobnych do C, ponieważ generalnie przypominają C w swojej składni. Najważniejszą różnicą są ich ekosystemy: podczas gdy C++ może bezproblemowo odwoływać się do bibliotek opartych na C lub C++ lub API systemu operacyjnego, Java najlepiej nadaje się do bibliotek opartych na Javie. Dostęp do bibliotek C w Javie można uzyskać za pomocą interfejsu API Java Native Interface (JNI), ale jest to podatne na błędy i wymaga trochę kodu C lub C++. C++ również łatwiej współpracuje ze sprzętem niż Java, ponieważ C++ jest językiem niższego poziomu.

Szczegółowe kompromisy: generyki, pamięć i nie tylko

Możemy porównać C++ do Javy z wielu perspektyw. W niektórych przypadkach decyzja między C++ a Javą jest jasna. Natywne aplikacje na Androida powinny zazwyczaj używać języka Java, chyba że aplikacja jest grą. Większość twórców gier powinna wybrać C++ lub inny język, aby uzyskać możliwie płynną animację w czasie rzeczywistym; Zarządzanie pamięcią w Javie często powoduje opóźnienia podczas rozgrywki.

Aplikacje wieloplatformowe, które nie są grami, wykraczają poza zakres tej dyskusji. Ani C++, ani Java nie są w tym przypadku idealne, ponieważ są zbyt gadatliwe, aby efektywnie tworzyć GUI. W przypadku aplikacji o wysokiej wydajności najlepiej jest utworzyć moduły C++ do wykonywania ciężkich zadań i używać bardziej wydajnego języka dla programistów dla interfejsu GUI.

Aplikacje wieloplatformowe, które nie są grami, wykraczają poza zakres tej dyskusji. Ani C++, ani Java nie są w tym przypadku idealne, ponieważ są zbyt gadatliwe, aby efektywnie tworzyć GUI.

Ćwierkać

W przypadku niektórych projektów wybór może nie być jasny, więc porównajmy dalej:

Funkcja C++ Jawa
Przyjazny dla początkujących Nie TAk
Wydajność w czasie wykonywania To, co najlepsze Dobrze
Czas oczekiwania Możliwy do przewidzenia Nieobliczalny
Inteligentne wskaźniki zliczające referencje TAk Nie
Globalna zbiórka śmieci typu mark-and-sweep Nie Wymagany
Alokacja pamięci stosu TAk Nie
Kompilacja do natywnego pliku wykonywalnego TAk Nie
Kompilacja do kodu bajtowego Java Nie TAk
Bezpośrednia interakcja z niskopoziomowymi interfejsami API systemu operacyjnego TAk Wymaga kodu C
Bezpośrednia interakcja z bibliotekami C TAk Wymaga kodu C
Bezpośrednia interakcja z bibliotekami Java Poprzez JNI TAk
Standaryzowane zarządzanie kompilacją i pakietami Nie Maven


Oprócz funkcji porównanych w tabeli skupimy się również na funkcjach programowania obiektowego (OOP), takich jak wielokrotne dziedziczenie, ogólne/szablony i refleksja. Zauważ, że oba języki obsługują OOP: wymaga tego Java, podczas gdy C++ obsługuje OOP wraz z funkcjami globalnymi i danymi statycznymi.

Dziedziczenie wielokrotne

W OOP dziedziczenie ma miejsce, gdy klasa podrzędna dziedziczy atrybuty i metody z klasy nadrzędnej. Jednym ze standardowych przykładów jest klasa Rectangle , która dziedziczy z bardziej ogólnej klasy Shape :

 // 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(); };

Dziedziczenie wielokrotne ma miejsce, gdy klasa podrzędna dziedziczy od wielu rodziców. Oto przykład z wykorzystaniem klas Rectangle i Shape oraz dodatkowej klasy 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(); };

W tym przypadku mamy dwa typy bazowe: Shape (podstawowy typ Rectangle ) i Clickable . ClickableRectangle dziedziczy z obu, aby skomponować dwa typy obiektów.

C++ obsługuje dziedziczenie wielokrotne; Java nie. Dziedziczenie wielokrotne jest przydatne w niektórych skrajnych przypadkach, takich jak:

  • Tworzenie zaawansowanego języka specyficznego dla domeny (DSL).
  • Wykonywanie skomplikowanych obliczeń w czasie kompilacji.
  • Poprawa bezpieczeństwa typów projektów w sposób, który po prostu nie jest możliwy w Javie.

Jednak generalnie odradza się stosowanie dziedziczenia wielokrotnego. Może komplikować kod i wpływać na wydajność, chyba że jest połączony z metaprogramowaniem szablonów — co najlepiej wykonują tylko najbardziej doświadczeni programiści C++.

Generyki i szablony

Ogólne wersje klas, które działają z dowolnym typem danych, są praktyczne w przypadku ponownego użycia kodu. Obydwa języki oferują tę obsługę — Java przez generyki, C++ przez szablony — ale elastyczność szablonów C++ może sprawić, że zaawansowane programowanie będzie bezpieczniejsze i bardziej niezawodne. Kompilatory C++ tworzą nowe dostosowane klasy lub funkcje za każdym razem, gdy w szablonie używasz różnych typów. Co więcej, szablony C++ mogą wywoływać funkcje niestandardowe na podstawie typów parametrów funkcji najwyższego poziomu, umożliwiając poszczególnym typom danych specjalny kod. Nazywa się to specjalizacją szablonów. Java nie ma równoważnej funkcji.

W przeciwieństwie do tego, używając generyków, kompilatory Java tworzą ogólne obiekty bez typów w procesie zwanym wymazywaniem typów. Java sprawdza typ podczas kompilacji, ale programiści nie mogą modyfikować zachowania ogólnej klasy lub metody na podstawie jej parametrów typu. Aby lepiej to zrozumieć, spójrzmy na szybki przykład ogólnej funkcji std::string format(std::string fmt, T1 item1, T2 item2) , która używa szablonu, template<class T1, class T2> , z C++ biblioteka, którą stworzyłem:

 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++ utworzy funkcję format jako std::string format(std::string fmt, std::string item1, int item2) item2 podczas gdy Java utworzy ją bez określonych typów obiektów string i int dla item1 i 2 . W tym przypadku nasz szablon C++ wie, że ostatnim przychodzącym parametrem jest int i dlatego może wykonać niezbędną konwersję std::to_string w drugim wywołaniu format . Bez szablonów, instrukcja printf C++ próbująca wydrukować liczbę jako ciąg znaków, jak w drugim wywołaniu format , miałaby niezdefiniowane zachowanie i mogłaby spowodować awarię aplikacji lub śmieci drukowania. Funkcja Java byłaby w stanie potraktować liczbę jako ciąg znaków tylko w pierwszym wywołaniu format i nie sformatowałaby jej bezpośrednio jako szesnastkowej liczby całkowitej. Jest to trywialny przykład, ale pokazuje zdolność C++ do wybrania wyspecjalizowanego szablonu do obsługi dowolnego obiektu klasy bez modyfikowania jego klasy lub funkcji format . Możemy poprawnie wygenerować dane wyjściowe w Javie, używając refleksji zamiast generyków, chociaż ta metoda jest mniej rozszerzalna i bardziej podatna na błędy.

Odbicie

W Javie można dowiedzieć się (w czasie wykonywania) szczegółów strukturalnych, takich jak elementy, które są dostępne w klasie lub typie klasy. Ta cecha nazywa się odbiciem, prawdopodobnie dlatego, że jest to jak podnoszenie lustra do obiektu, aby zobaczyć, co jest w środku. (Więcej informacji można znaleźć w dokumentacji refleksji Oracle.)

C++ nie ma pełnego odzwierciedlenia, ale nowoczesny C++ oferuje informacje o typie środowiska uruchomieniowego (RTTI). RTTI pozwala na wykrywanie w czasie wykonywania określonych typów obiektów, chociaż nie ma dostępu do informacji takich jak składowe obiektu.

Zarządzanie pamięcią

Inną krytyczną różnicą między C++ a Javą jest zarządzanie pamięcią, które ma dwa główne podejścia: ręczne, w którym programiści muszą śledzić i zwalniać pamięć ręcznie; i automatyczny, w którym oprogramowanie śledzi, które obiekty są nadal używane, aby odzyskać nieużywaną pamięć. W Javie przykładem jest garbage collection.

Java wymaga pamięci gromadzonej przez śmieci, zapewniając łatwiejsze zarządzanie pamięcią niż podejście ręczne i eliminując błędy zwalniania pamięci, które często przyczyniają się do powstawania luk w zabezpieczeniach. C++ nie zapewnia automatycznego zarządzania pamięcią natywnie, ale obsługuje formę zbierania śmieci zwaną inteligentnymi wskaźnikami. Inteligentne wskaźniki wykorzystują zliczanie odwołań i są bezpieczne i wydajne, jeśli są używane prawidłowo. C++ oferuje również destruktory, które czyszczą lub zwalniają zasoby po zniszczeniu obiektu.

Podczas gdy Java oferuje tylko alokację sterty, C++ obsługuje zarówno alokację sterty (przy użyciu new i delete lub starszych funkcji C malloc ), jak i alokację stosu. Alokacja stosu może być szybsza i bezpieczniejsza niż alokacja sterty, ponieważ stos jest liniową strukturą danych, podczas gdy sterta jest oparta na drzewie, więc alokacja i zwolnienie pamięci stosu jest znacznie prostsze.

Kolejną zaletą C++ związaną z alokacją stosu jest technika programowania znana jako inicjalizacja pozyskiwania zasobów (RAII). W RAII zasoby, takie jak referencje, są powiązane z cyklem życia obiektu kontrolującego; zasoby zostaną zniszczone pod koniec cyklu życia tego obiektu. RAII to sposób działania inteligentnych wskaźników C++ bez ręcznego wyłuskiwania — inteligentny wskaźnik, do którego odwołuje się u góry funkcji, jest automatycznie wyłuskiwany po wyjściu z funkcji. Podłączona pamięć jest również zwalniana, jeśli jest to ostatnie odniesienie do inteligentnego wskaźnika. Chociaż Java oferuje podobny wzorzec, jest bardziej niewygodna niż RAII w C++, zwłaszcza jeśli potrzebujesz utworzyć kilka zasobów w tym samym bloku kodu.

Wydajność w czasie wykonywania

Java ma solidną wydajność w czasie wykonywania, ale C ++ nadal jest koroną, ponieważ ręczne zarządzanie pamięcią jest szybsze niż wyrzucanie śmieci w rzeczywistych aplikacjach. Chociaż Java może przewyższać C++ w niektórych kluczowych przypadkach dzięki kompilacji JIT, C++ wygrywa w większości nietrywialnych przypadków.

W szczególności standardowa biblioteka pamięci Javy przeciąża moduł odśmiecania pamięci jego alokacjami w porównaniu ze zmniejszonym wykorzystaniem alokacji sterty w C++. Jednak Java jest nadal stosunkowo szybka i powinna być akceptowalna, chyba że opóźnienia są głównym problemem — na przykład w grach lub aplikacjach z ograniczeniami czasu rzeczywistego.

Zarządzanie kompilacją i pakietami

Braki wydajności w Javie nadrabiają łatwością użytkowania. Jednym z komponentów wpływających na wydajność programistów jest zarządzanie kompilacją i pakietami — sposób, w jaki budujemy projekty i wprowadzamy zależności zewnętrzne do aplikacji. W Javie narzędzie o nazwie Maven upraszcza ten proces do kilku prostych kroków i integruje się z wieloma IDE, takimi jak IntelliJ IDEA.

Jednak w C++ nie istnieje standardowe repozytorium pakietów. Nie istnieje nawet ustandaryzowana metoda budowania kodu C++ w aplikacjach: niektórzy programiści preferują Visual Studio, podczas gdy inni używają CMake lub innego niestandardowego zestawu narzędzi. Dodatkowo dodając do złożoności, niektóre komercyjne biblioteki C++ są sformatowane w formacie binarnym i nie ma spójnego sposobu na zintegrowanie tych bibliotek z procesem kompilacji. Co więcej, różnice w ustawieniach kompilacji lub wersjach kompilatora mogą powodować problemy z uruchomieniem bibliotek binarnych.

Przyjazność dla początkujących

Tarcia w kompilację i zarządzanie pakietami nie są jedynym powodem, dla którego C++ jest znacznie mniej przyjazny dla początkujących niż Java. Programista może mieć trudności z debugowaniem i bezpiecznym używaniem C++, chyba że jest zaznajomiony z C, językami asemblerowymi lub działaniem niższego poziomu komputera. Pomyśl o C++ jak o potężnym narzędziu: może wiele zdziałać, ale jest niebezpieczny, jeśli zostanie niewłaściwie użyty.

Wspomniane wcześniej podejście do zarządzania pamięcią w Javie sprawia, że ​​jest ona znacznie bardziej dostępna niż C++. Programiści Java nie muszą się martwić zwalnianiem pamięci obiektowej, ponieważ język zajmuje się tym automatycznie.

Czas decyzji: C++ czy Java?

Schemat blokowy z ciemnoniebieską bańką „Start” w lewym górnym rogu, który ostatecznie łączy się z jednym z siedmiu jasnoniebieskich pól konkluzji poniżej, poprzez serię białych skrzyżowań decyzyjnych z ciemnoniebieskimi gałęziami dla „Tak” i innych opcji, i jasnoniebieskie gałązki dla „Nie”. Pierwszym z nich jest "Cros-platformowa aplikacja GUI?" z którego odpowiedź „Tak” prowadzi do wniosku: „Wybierz wieloplatformowe środowisko programistyczne i użyj jego podstawowego języka”. „Nie” wskazuje na „Native Android App?” z którego odpowiedź „Tak” wskazuje na drugorzędne pytanie: „Czy to gra?” Z drugiego pytania „Nie” wskazuje na wniosek „Użyj Java (lub Kotlin)”, a „Tak” wskazuje na inny wniosek: „Wybierz międzyplatformowy silnik gry i użyj zalecanego języka”. Z „Natywnej aplikacji na Androida?” pytanie, „Nie” wskazuje na „Natywna aplikacja dla systemu Windows?” z którego odpowiedź „Tak” wskazuje na drugorzędne pytanie: „Czy to gra?” Z pytania drugorzędnego „Tak” wskazuje na wniosek „Wybierz wieloplatformowy silnik gry i użyj jego zalecanego języka”, a „Nie” wskazuje na inny wniosek: „Wybierz środowisko GUI systemu Windows i użyj jego podstawowego język (zazwyczaj C++ lub C#)." Z „Natywnej aplikacji dla systemu Windows?” pytanie, „Nie” wskazuje na „Aplikacja serwerowa?” z którego odpowiedź „Tak” wskazuje na pytanie drugorzędne „Typ programisty?” Z pytania drugorzędnego decyzja „średnie umiejętności” prowadzi do wniosku „Użyj języka Java (lub C# lub TypeScript)”, a decyzja „umiejętności” wskazuje na pytanie trzeciorzędne „Najwyższy priorytet?” Z pytania trzeciego stopnia decyzja „Produktywność programisty” prowadzi do wniosku „Użyj języka Java (lub C# lub TypeScript)”, a decyzja „Wydajność” prowadzi do innego wniosku: „Użyj C++ (lub Rust)”. Z „Aplikacji serwerowej?” pytanie, „Nie” wskazuje na pytanie drugorzędne: „Rozwój sterowników?” Z pytania drugorzędnego „Tak” wskazuje na wniosek „Użyj C++ (lub Rust)”, a „Nie” wskazuje na pytanie trzecie: „Rozwój Internetu Rzeczy?”. Z pytania trzeciego stopnia „Tak” wskazuje na wniosek „Użyj C++ (lub Rust)”, a „Nie” wskazuje na pytanie czwartego stopnia „Handel o dużej szybkości?” Z pytania czwartorzędowego „Tak” wskazuje na wniosek „Użyj C++ (lub Rust)”, a „Nie” wskazuje na ostatnią konkluzję: „Zapytaj kogoś, kto zna twoją domenę docelową”.
Rozbudowany przewodnik po wyborze najlepszego języka dla różnych typów projektów.

Teraz, gdy dogłębnie zbadaliśmy różnice między C++ i Javą, wracamy do naszego pierwotnego pytania: C++ czy Java? Nawet przy głębokim zrozumieniu tych dwóch języków nie ma jednej uniwersalnej odpowiedzi.

Inżynierowie oprogramowania niezaznajomieni z koncepcjami programowania niskiego poziomu mogą lepiej wybrać Javę, ograniczając decyzję do C++ lub Javy, z wyjątkiem kontekstów czasu rzeczywistego, takich jak gry. Z drugiej strony programiści, którzy chcą poszerzyć swoje horyzonty, mogą dowiedzieć się więcej, wybierając C++.

Jednak techniczne różnice między C++ i Javą mogą być tylko małym czynnikiem przy podejmowaniu decyzji. Niektóre rodzaje produktów wymagają szczególnego wyboru. Jeśli nadal nie masz pewności, możesz zapoznać się ze schematem blokowym, ale pamiętaj, że może on ostatecznie wskazać Ci trzeci język.