Prefazione
Java non assomiglia al C ++, contrariamente all'hype. La macchina hype Java vorrebbe farti credere che, poiché Java ha una sintassi simile al C ++, i linguaggi sono simili. Niente può essere più lontano dalla verità. Questa disinformazione fa parte del motivo per cui i programmatori Java passano al C ++ e usano una sintassi simile a Java senza comprendere le implicazioni del loro codice.
Andiamo avanti
Ma non riesco a capire perché dovremmo farlo in questo modo. Suppongo che abbia a che fare con l'efficienza e la velocità poiché abbiamo accesso diretto all'indirizzo di memoria. Ho ragione?
Al contrario, in realtà. L'heap è molto più lento dello stack, perché lo stack è molto semplice rispetto all'heap. Le variabili di archiviazione automatiche (ovvero variabili dello stack) vengono chiamate i loro distruttori una volta che escono dall'ambito. Per esempio:
{
std::string s;
}
// s is destroyed here
D'altra parte, se si utilizza un puntatore allocato dinamicamente, il suo distruttore deve essere chiamato manualmente. delete
chiama questo distruttore per te.
{
std::string* s = new std::string;
}
delete s; // destructor called
Questo non ha nulla a che fare con la new
sintassi prevalente in C # e Java. Sono utilizzati per scopi completamente diversi.
Vantaggi dell'allocazione dinamica
1. Non è necessario conoscere in anticipo la dimensione dell'array
Uno dei primi problemi che incontrano molti programmatori C ++ è che quando accettano input arbitrari dagli utenti, è possibile allocare solo una dimensione fissa per una variabile di stack. Non è nemmeno possibile modificare la dimensione degli array. Per esempio:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
Ovviamente, se hai usato un std::string
invece, si std::string
ridimensiona internamente in modo che non dovrebbe essere un problema. Ma essenzialmente la soluzione a questo problema è l'allocazione dinamica. È possibile allocare memoria dinamica in base all'input dell'utente, ad esempio:
int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];
Nota a margine : un errore che fanno molti principianti è l'uso di array di lunghezza variabile. Questa è un'estensione GNU e anche una in Clang perché rispecchiano molte delle estensioni di GCC. Quindi il seguente
int arr[n]
non si deve fare affidamento su .
Poiché l'heap è molto più grande dello stack, si può allocare / riallocare arbitrariamente la quantità di memoria di cui ha bisogno, mentre lo stack ha un limite.
2. Le matrici non sono puntatori
In che modo questo è un vantaggio che chiedi? La risposta diventerà chiara quando capirai la confusione / mito dietro array e puntatori. Si presume comunemente che siano uguali, ma non lo sono. Questo mito deriva dal fatto che i puntatori possono essere sottoscritti proprio come le matrici e, a causa della decadenza delle matrici, si riducono ai puntatori di livello superiore in una dichiarazione di funzione. Tuttavia, una volta che un array decade in un puntatore, il puntatore perde il suosizeof
informazioni. Quindi sizeof(pointer)
fornirà la dimensione del puntatore in byte, che di solito è 8 byte su un sistema a 64 bit.
Non è possibile assegnare alle matrici, solo inizializzarle. Per esempio:
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
D'altra parte, puoi fare quello che vuoi con i puntatori. Sfortunatamente, poiché la distinzione tra puntatori e array è agitata a mano in Java e C #, i principianti non comprendono la differenza.
3. Polimorfismo
Java e C # dispongono di funzionalità che consentono di trattare gli oggetti come un altro, ad esempio utilizzando la as
parola chiave. Quindi, se qualcuno volesse trattare un Entity
oggetto come un Player
oggetto, si potrebbe fare. Player player = Entity as Player;
Ciò è molto utile se si intende chiamare funzioni su un contenitore omogeneo che dovrebbe essere applicato solo a un tipo specifico. Di seguito è possibile ottenere la funzionalità in modo simile:
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();
}
Quindi, se solo Triangoli avesse una funzione Ruota, sarebbe un errore del compilatore se si provasse a chiamarlo su tutti gli oggetti della classe. Usando dynamic_cast
, puoi simulare la as
parola chiave. Per essere chiari, se un cast fallisce, restituisce un puntatore non valido. Quindi !test
è essenzialmente una scorciatoia per verificare setest
è NULL o un puntatore non valido, il che significa che il cast non è riuscito.
Vantaggi delle variabili automatiche
Dopo aver visto tutte le grandi cose che l'allocazione dinamica può fare, probabilmente ti starai chiedendo perché nessuno NON dovrebbe usare l'allocazione dinamica in ogni momento? Ti ho già detto una ragione, l'heap è lento. E se non hai bisogno di tutta quella memoria, non dovresti abusarne. Quindi, ecco alcuni svantaggi in nessun ordine particolare:
È soggetto a errori. L'allocazione manuale della memoria è pericolosa e si è soggetti a perdite. Se non sei esperto nell'uso del debugger o valgrind
(uno strumento di perdita di memoria), puoi strapparti i capelli dalla testa. Fortunatamente i modi di dire RAII e gli indicatori intelligenti lo attenuano un po ', ma devi avere familiarità con pratiche come La regola dei tre e La regola dei cinque. Ci sono molte informazioni da prendere e i principianti che non sanno o non si preoccupano cadranno in questa trappola.
Non è necessario. A differenza di Java e C # dove è idiomatico usare la new
parola chiave ovunque, in C ++, dovresti usarla solo se necessario. La frase comune dice, tutto sembra un chiodo se hai un martello. Mentre i principianti che iniziano con C ++ hanno paura dei puntatori e imparano a usare le variabili dello stack per abitudine, i programmatori Java e C # iniziano usando i puntatori senza capirlo! Questo sta letteralmente scendendo con il piede sbagliato. Devi abbandonare tutto ciò che sai perché la sintassi è una cosa, imparare la lingua è un'altra.
1. (N) RVO - Aka, (Named) Ottimizzazione del valore di ritorno
Un'ottimizzazione che molti compilatori fanno sono le cose chiamate elision e ottimizzazione del valore di ritorno . Queste cose possono ovviare a copie non necessarie che sono utili per oggetti molto grandi, come un vettore contenente molti elementi. Normalmente la pratica comune è quella di utilizzare i puntatori per trasferire la proprietà anziché copiare gli oggetti di grandi dimensioni per spostarli . Ciò ha portato alla nascita della semantica di movimento e dei puntatori intelligenti .
Se si utilizzano i puntatori, (N) RVO NON si verifica. È più vantaggioso e meno soggetto a errori sfruttare (N) RVO piuttosto che restituire o passare i puntatori se si è preoccupati per l'ottimizzazione. Perdite di errore possono verificarsi se il chiamante di una funzione è responsabile delete
dell'immissione di un oggetto allocato dinamicamente e simili. Può essere difficile tenere traccia della proprietà di un oggetto se i puntatori vengono passati come una patata bollente. Usa le variabili dello stack perché è più semplice e migliore.