Przedmowa
Java nie przypomina C ++, w przeciwieństwie do hype. Maszyna do hype Java chciałaby, abyś wierzył, że ponieważ Java ma składnię podobną do C ++, języki są podobne. Nic nie może być dalsze od prawdy. Ta dezinformacja jest jednym z powodów, dla których programiści Java przechodzą do C ++ i używają składni podobnej do języka Java, nie rozumiejąc konsekwencji swojego kodu.
Idziemy dalej
Ale nie mogę zrozumieć, dlaczego powinniśmy to zrobić w ten sposób. Zakładam, że ma to związek z wydajnością i szybkością, ponieważ uzyskujemy bezpośredni dostęp do adresu pamięci. Czy mam rację?
Wręcz przeciwnie. Stos jest znacznie wolniejszy niż stos, ponieważ stos jest bardzo prosty w porównaniu do stosu. Automatyczne zmienne pamięci (inaczej zmienne stosu) mają swoje wywoływacze, gdy wychodzą poza zakres. Na przykład:
{
std::string s;
}
// s is destroyed here
Z drugiej strony, jeśli użyjesz wskaźnika przydzielanego dynamicznie, jego destruktor musi zostać wywołany ręcznie. delete
nazywa to Destruktorem.
{
std::string* s = new std::string;
}
delete s; // destructor called
Nie ma to nic wspólnego ze new
składnią rozpowszechnioną w C # i Javie. Są używane do zupełnie innych celów.
Korzyści z dynamicznej alokacji
1. Nie musisz z góry znać rozmiaru tablicy
Jednym z pierwszych problemów, na jakie napotyka wielu programistów C ++, jest to, że kiedy akceptują dowolne dane wejściowe od użytkowników, można przydzielić tylko stały rozmiar zmiennej stosu. Nie można również zmienić rozmiaru tablic. Na przykład:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
Oczywiście, jeśli użyłeś std::string
zamiast tego, std::string
rozmiar wewnętrzny sam się zmienia, więc nie powinno to stanowić problemu. Ale zasadniczo rozwiązaniem tego problemu jest alokacja dynamiczna. Możesz przydzielić pamięć dynamiczną na podstawie danych wejściowych użytkownika, na przykład:
int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];
Uwaga dodatkowa : Jednym z błędów wielu początkujących jest użycie tablic o zmiennej długości. Jest to rozszerzenie GNU, a także w Clang, ponieważ odzwierciedla wiele rozszerzeń GCC. Dlatego int arr[n]
nie należy polegać na następujących
kwestiach.
Ponieważ stos jest znacznie większy niż stos, można dowolnie przydzielić / realokować tyle pamięci, ile on / ona potrzebuje, podczas gdy stos ma ograniczenia.
2. Tablice nie są wskaźnikami
Jaka jest korzyść, o którą prosisz? Odpowiedź stanie się jasna, gdy zrozumiesz zamieszanie / mit kryjący się za tablicami i wskaźnikami. Powszechnie przyjmuje się, że są takie same, ale nie są. Mit ten wynika z faktu, że wskaźniki można subskrybować podobnie jak tablice, a ponieważ tablice rozpadają się na wskaźniki na najwyższym poziomie w deklaracji funkcji. Jednak gdy tablica rozpadnie się na wskaźnik, wskaźnik traci sizeof
informacje. Podaje więc sizeof(pointer)
rozmiar wskaźnika w bajtach, który zwykle wynosi 8 bajtów w systemie 64-bitowym.
Nie możesz przypisywać tablic, tylko je inicjuj. Na przykład:
int arr[5] = {1, 2, 3, 4, 5}; // initialization
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
// be given by the amount of members in the initializer
arr = { 1, 2, 3, 4, 5 }; // ERROR
Z drugiej strony możesz robić, co chcesz, korzystając ze wskaźników. Niestety, ponieważ różnice między wskaźnikami i tablicami są ręcznie pomachane w Javie i C #, początkujący nie rozumieją różnicy.
3. Polimorfizm
Java i C # mają funkcje, które pozwalają traktować obiekty jak inne, na przykład używając as
słowa kluczowego. Więc jeśli ktoś chciałby traktować Entity
obiekt jako Player
obiekt, można to zrobić. Player player = Entity as Player;
Jest to bardzo przydatne, jeśli zamierzasz wywoływać funkcje na jednorodnym kontenerze, które powinny mieć zastosowanie tylko do określonego typu. Funkcjonalność można osiągnąć w podobny sposób poniżej:
std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
if (!test) // not a triangle
e.GenericFunction();
else
e.TriangleOnlyMagic();
}
Powiedzmy, że gdyby tylko trójkąty miały funkcję obracania, byłby to błąd kompilatora, gdybyś próbował wywołać ją na wszystkich obiektach klasy. Za pomocą dynamic_cast
możesz symulować as
słowo kluczowe. Aby wyjaśnić, jeśli rzutowanie nie powiedzie się, zwraca nieprawidłowy wskaźnik. !test
Jest to zatem w skrócie sprawdzenie, czytest
NULL lub nieprawidłowy wskaźnik, co oznacza, że rzutowanie nie powiodło się.
Korzyści ze zmiennych automatycznych
Po zobaczeniu wszystkich wspaniałych rzeczy, które może zrobić dynamiczna alokacja, prawdopodobnie zastanawiasz się, dlaczego nikt NIE powinien używać dynamicznej alokacji przez cały czas? Powiedziałem ci już jeden powód, że stos jest wolny. A jeśli nie potrzebujesz całej tej pamięci, nie powinieneś jej nadużywać. Oto kilka wad w nie określonej kolejności:
Jest podatny na błędy. Ręczne przydzielanie pamięci jest niebezpieczne i masz skłonność do wycieków. Jeśli nie jesteś biegły w używaniu debugera lub valgrind
(narzędzia do wycieku pamięci), możesz wyciągnąć włosy z głowy. Na szczęście idiomy RAII i inteligentne wskaźniki nieco to łagodzą, ale musisz znać takie praktyki, jak Zasada Trzech i Reguła Pięciu. Jest wiele informacji do przyjęcia, a początkujący, którzy albo nie wiedzą, albo nie przejmują się, wpadną w tę pułapkę.
To nie jest konieczne. W przeciwieństwie do Java i C #, gdzie idiomatyczne jest używanie new
słowa kluczowego wszędzie, w C ++ powinieneś go używać tylko wtedy, gdy jest to konieczne. Mówi się, że jeśli masz młotek, wszystko wygląda jak gwóźdź. Podczas gdy początkujący, którzy zaczynają od C ++, boją się wskaźników i uczą się używania zmiennych stosu według przyzwyczajenia, programiści Java i C # zaczynają od używania wskaźników bez ich zrozumienia! To dosłownie stąpanie po niewłaściwej stopie. Musisz porzucić wszystko, co wiesz, ponieważ składnia to jedno, uczenie się języka to drugie.
1. (N) RVO - Aka, (nazwana) Optymalizacja wartości zwracanej
Jedną z optymalizacji wielu kompilatorów są tzw . Optymalizacja elision i return . Te rzeczy mogą wyeliminować niepotrzebne kopie, co jest przydatne w przypadku bardzo dużych obiektów, takich jak wektor zawierający wiele elementów. Zwykle powszechną praktyką jest używanie wskaźników do przenoszenia własności, a nie kopiowanie dużych obiektów w celu ich przenoszenia . Doprowadziło to do powstania semantyki ruchów i inteligentnych wskaźników .
Jeśli używasz wskaźników, (N) RVO NIE występuje. Bardziej korzystne i mniej podatne na błędy jest skorzystanie z (N) RVO zamiast zwracania lub przekazywania wskaźników, jeśli martwisz się optymalizacją. Wycieki błędów mogą się zdarzyć, jeśli wywoływacz funkcji jest odpowiedzialny za wywołanie delete
dynamicznie przydzielanego obiektu i tym podobne. Śledzenie własności obiektu może być trudne, jeśli wskaźniki są przekazywane jak gorący ziemniak. Wystarczy użyć zmiennych stosu, ponieważ jest to prostsze i lepsze.