Perché dovrei usare un puntatore anziché l'oggetto stesso?


1602

Vengo da uno sfondo Java e ho iniziato a lavorare con oggetti in C ++. Ma una cosa che mi è venuta in mente è che le persone usano spesso puntatori agli oggetti piuttosto che agli oggetti stessi, ad esempio questa dichiarazione:

Object *myObject = new Object;

piuttosto che:

Object myObject;

O invece di usare una funzione, diciamo testFunc()così:

myObject.testFunc();

dobbiamo scrivere:

myObject->testFunc();

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?


405
Complimenti a te per aver messo in discussione questa pratica piuttosto che semplicemente seguirla. Il più delle volte, i puntatori sono troppo usati.
Luchian Grigore

120
Se non vedi un motivo per usare i puntatori, non farlo. Preferisci gli oggetti. Preferisci gli oggetti prima di unique_ptr prima di shared_ptr prima dei puntatori non elaborati.
stefan

113
nota: in Java, tutto (tranne i tipi di base) è un puntatore. quindi dovresti piuttosto chiedere il contrario: perché ho bisogno di oggetti semplici?
Karoly Horvath,

119
Si noti che, in Java, i puntatori sono nascosti dalla sintassi. In C ++, la differenza tra un puntatore e un non puntatore viene resa esplicita nel codice. Java utilizza puntatori ovunque.
Daniel Martín,

216
Chiudi come troppo ampio ? Sul serio? Per favore, notate che questo modo di programmazione Java ++ è molto comune e uno dei problemi più importanti nella comunità C ++ . Dovrebbe essere trattato seriamente.
Manu343726,

Risposte:


1575

È molto spiacevole vedere l'allocazione dinamica così spesso. Ciò dimostra solo quanti programmatori C ++ sono cattivi.

In un certo senso, hai due domande raggruppate in una. Il primo è quando dovremmo usare l'allocazione dinamica (usando new)? Il secondo è quando dovremmo usare i puntatori?

L'importante messaggio da portare a casa è che dovresti sempre usare lo strumento appropriato per il lavoro . In quasi tutte le situazioni, esiste qualcosa di più appropriato e più sicuro rispetto all'esecuzione dell'allocazione dinamica manuale e / o all'utilizzo di puntatori non elaborati.

Allocazione dinamica

Nella tua domanda, hai dimostrato due modi per creare un oggetto. La differenza principale è la durata della memorizzazione dell'oggetto. Quando si esegue Object myObject;all'interno di un blocco, l'oggetto viene creato con una durata di memorizzazione automatica, il che significa che verrà distrutto automaticamente quando esce dall'ambito. Quando lo fai new Object(), l'oggetto ha una durata di memorizzazione dinamica, il che significa che rimane in vita fino a quando non lo esplicitamente delete. Dovresti utilizzare la durata della memoria dinamica solo quando ne hai bisogno. Cioè, dovresti sempre preferire la creazione di oggetti con durata di archiviazione automatica quando puoi .

Le due principali situazioni in cui potresti richiedere un'allocazione dinamica:

  1. È necessario che l'oggetto sopravviva all'ambito corrente, quell'oggetto specifico in quella posizione di memoria specifica, non una copia di esso. Se stai bene copiando / spostando l'oggetto (il più delle volte dovresti essere), dovresti preferire un oggetto automatico.
  2. È necessario allocare molta memoria , che può facilmente riempire lo stack. Sarebbe bello se non dovessimo preoccuparci di questo (il più delle volte non dovresti farlo), dato che è davvero al di fuori del campo di applicazione del C ++, ma sfortunatamente, dobbiamo occuparci della realtà dei sistemi stiamo sviluppando per.

Quando si richiede assolutamente un'allocazione dinamica, è necessario incapsularla in un puntatore intelligente o in un altro tipo che esegua RAII (come i contenitori standard). I puntatori intelligenti forniscono la semantica della proprietà degli oggetti allocati dinamicamente. Dai un'occhiata std::unique_ptre std::shared_ptr, per esempio. Se li usi in modo appropriato, puoi quasi del tutto evitare di eseguire la tua gestione della memoria (vedi la Regola dello Zero ).

puntatori

Tuttavia, ci sono altri usi più generali per i puntatori non elaborati oltre all'allocazione dinamica, ma la maggior parte ha alternative che dovresti preferire. Come prima, preferisci sempre le alternative a meno che tu non abbia davvero bisogno di puntatori .

  1. Hai bisogno della semantica di riferimento . A volte vuoi passare un oggetto usando un puntatore (indipendentemente da come è stato allocato) perché vuoi che la funzione a cui lo stai passando abbia accesso a quell'oggetto specifico (non una sua copia). Tuttavia, nella maggior parte dei casi, dovresti preferire i tipi di riferimento ai puntatori, perché questo è specificamente progettato per. Nota che non si tratta necessariamente di estendere la durata dell'oggetto oltre l'ambito corrente, come nella situazione 1 sopra. Come prima, se stai bene passando una copia dell'oggetto, non hai bisogno della semantica di riferimento.

  2. Hai bisogno del polimorfismo . È possibile chiamare le funzioni polimorficamente (ovvero, in base al tipo dinamico di un oggetto) tramite un puntatore o un riferimento all'oggetto. Se questo è il comportamento necessario, è necessario utilizzare puntatori o riferimenti. Ancora una volta, i riferimenti dovrebbero essere preferiti.

  3. Si desidera rappresentare che un oggetto è facoltativo consentendo il passaggio di un nullptroggetto quando viene omesso. Se si tratta di un argomento, è preferibile utilizzare argomenti predefiniti o sovraccarichi di funzioni. Altrimenti, è preferibile utilizzare un tipo che incapsuli questo comportamento, ad esempio std::optional(introdotto in C ++ 17 - con precedenti standard C ++, utilizzare boost::optional).

  4. Volete disaccoppiare le unità di compilazione per migliorare i tempi di compilazione . La proprietà utile di un puntatore è che è necessaria solo una dichiarazione diretta del tipo a punta (per utilizzare effettivamente l'oggetto, è necessaria una definizione). Ciò consente di disaccoppiare parti del processo di compilazione, il che può migliorare significativamente i tempi di compilazione. Vedi l' idioma di Pimpl .

  5. È necessario interfacciarsi con una libreria C o una libreria in stile C. A questo punto, sei costretto a utilizzare i puntatori non elaborati. La cosa migliore che puoi fare è assicurarti di liberare i tuoi puntatori grezzi solo nell'ultimo momento possibile. È possibile ottenere un puntatore non elaborato da un puntatore intelligente, ad esempio, utilizzando la sua getfunzione membro. Se una libreria esegue per te alcune allocazioni che prevede di deallocare tramite un handle, puoi spesso avvolgere l'handle in un puntatore intelligente con un deleter personalizzato che distribuirà l'oggetto in modo appropriato.


83
"È necessario l'oggetto per sopravvivere all'ambito corrente." - Una nota aggiuntiva a riguardo: ci sono casi in cui sembra che tu abbia bisogno dell'oggetto per sopravvivere all'ambito attuale, ma in realtà no. Se si inserisce l'oggetto in un vettore, ad esempio, l'oggetto verrà copiato (o spostato) nel vettore e l'oggetto originale può essere distrutto in modo sicuro al termine del suo ambito.

25
Ricorda che s / copia / sposta / in molti punti ora. Restituire un oggetto sicuramente non implica una mossa. Si noti inoltre che l'accesso a un oggetto tramite un puntatore è ortogonale al modo in cui è stato creato.
Puppy

15
Mi manca un riferimento esplicito a RAII su questa risposta. Il C ++ è tutto (quasi tutto) sulla gestione delle risorse e RAII è il modo di farlo su C ++ (E il problema principale che generano i puntatori non
elaborati

11
I puntatori intelligenti esistevano prima di C ++ 11, ad esempio boost :: shared_ptr e boost :: scoped_ptr. Altri progetti hanno il loro equivalente. Non è possibile ottenere la semantica di spostamento e l'assegnazione di std :: auto_ptr è errata, quindi C ++ 11 migliora le cose, ma il consiglio è ancora buono. (E un triste nitpick, non è sufficiente avere accesso a un compilatore C ++ 11, è necessario che tutti i compilatori che potresti desiderare che il tuo codice funzionino con il supporto C ++ 11. Sì, Oracle Solaris Studio, sono ti guarda.)
armb

7
@ MDMoore313 Puoi scrivereObject myObject(param1, etc...)
user000001

173

Esistono molti casi d'uso per i puntatori.

Comportamento polimorfico . Per i tipi polimorfici, i puntatori (o riferimenti) vengono utilizzati per evitare il taglio:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Semantica di riferimento ed evitare la copia . Per i tipi non polimorfici, un puntatore (o un riferimento) eviterà di copiare un oggetto potenzialmente costoso

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Si noti che C ++ 11 ha spostato la semantica che può evitare molte copie di oggetti costosi nell'argomento della funzione e come valori di ritorno. Ma l'uso di un puntatore eviterà sicuramente quelli e consentirà più puntatori sullo stesso oggetto (mentre un oggetto può essere spostato solo da una volta).

Acquisizione delle risorse . La creazione di un puntatore a una risorsa mediante l' newoperatore è un anti-modello nel C ++ moderno. Utilizzare una classe di risorse speciale (uno dei contenitori Standard) o un puntatore intelligente ( std::unique_ptr<>o std::shared_ptr<>). Prendere in considerazione:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Un puntatore non elaborato deve essere utilizzato solo come "vista" e non coinvolto in alcun modo nella proprietà, sia attraverso la creazione diretta o implicitamente attraverso i valori di ritorno. Vedi anche queste domande e risposte dalle Domande frequenti su C ++ .

Controllo più accurato del tempo di vita Ogni volta che un puntatore condiviso viene copiato (ad es. Come argomento di una funzione), la risorsa a cui punta viene mantenuta in vita. Gli oggetti regolari (non creati da te new, o direttamente da te o all'interno di una classe di risorse) vengono distrutti quando escono dall'ambito.


17
"Creare un puntatore a una risorsa usando il nuovo operatore è un anti-schema" Penso che potresti persino migliorare il fatto che avere un puntatore grezzo possieda qualcosa sia un anti-schema . Non solo la creazione, ma il passaggio di puntatori non elaborati come argomenti o valori di ritorno che implicano il trasferimento della proprietà IMHO è deprecato poiché unique_ptr/ sposta la semantica
dyp

1
@dyp tnx, aggiornato e riferimento alle Domande e risposte frequenti su C ++ su questo argomento.
TemplateRex

4
L'uso di puntatori intelligenti ovunque è un anti-schema. Vi sono alcuni casi speciali in cui è applicabile, ma la maggior parte delle volte lo stesso motivo che sostiene l'allocazione dinamica (durata arbitraria) si oppone anche a uno dei soliti puntatori intelligenti.
James Kanze,

2
@JamesKanze Non intendevo implicare che i puntatori intelligenti dovessero essere usati ovunque, solo per la proprietà, e anche che i puntatori grezzi non dovessero essere usati per la proprietà, ma solo per le viste.
TemplateRex

2
@TemplateRex Sembra un po 'sciocco dato che hun(b)richiede anche la conoscenza della firma a meno che tu non stia bene a non sapere che hai fornito il tipo sbagliato fino alla compilazione. Sebbene il problema di riferimento di solito non venga colto al momento della compilazione e richiederebbe maggiore sforzo per il debug, se stai controllando la firma per assicurarti che gli argomenti siano corretti, sarai anche in grado di vedere se qualcuno degli argomenti sono riferimenti quindi il bit di riferimento diventa qualcosa di non problematico (specialmente quando si utilizzano IDE o editor di testo che mostrano la firma di una funzione selezionata). Inoltre, const&.
JAB

130

Ci sono molte risposte eccellenti a questa domanda, inclusi gli importanti casi d'uso di dichiarazioni in avanti, polimorfismo ecc. Ma sento che una parte dell '"anima" della tua domanda non ha una risposta - vale a dire cosa significano le diverse sintassi in Java e C ++.

Esaminiamo la situazione confrontando le due lingue:

Giava:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

L'equivalente più vicino a questo, è:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Vediamo il modo C ++ alternativo:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

Il modo migliore per pensarci è che - più o meno - Java (implicitamente) gestisce i puntatori agli oggetti, mentre C ++ può gestire sia i puntatori agli oggetti, sia gli oggetti stessi. Ci sono eccezioni a questo - per esempio, se dichiarate i tipi "primitivi" di Java, sono valori reali che vengono copiati e non puntatori. Così,

Giava:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Detto questo, usare i puntatori NON è necessariamente il modo corretto o sbagliato di gestire le cose; tuttavia altre risposte lo hanno coperto in modo soddisfacente. L'idea generale è che in C ++ hai molto più controllo sulla durata degli oggetti e su dove vivranno.

Porta a casa: il Object * object = new Object()costrutto è in realtà ciò che è più vicino alla tipica semantica Java (o C # per quella materia).


7
Object2 is now "dead": Penso che intendi myObject1o più precisamente the object pointed to by myObject1.
Clément

2
Infatti! Ha riformulato un po '.
Gerasimos R,

2
Object object1 = new Object(); Object object2 = new Object();è un pessimo codice. Il secondo costruttore nuovo o il secondo oggetto può lanciare, e ora oggetto1 è trapelato. Se stai usando raw news, dovresti avvolgere gli newoggetti nei wrapper RAII il prima possibile.
PSkocik,

8
Anzi, sarebbe se questo fosse un programma, e nient'altro stava succedendo intorno ad esso. Per fortuna, questo è solo un frammento di spiegazione che mostra come si comporta un puntatore in C ++ - e uno dei pochi posti in cui un oggetto RAII non può essere sostituito con un puntatore non elaborato, sta studiando e imparando su puntatori non elaborati ...
Gerasimos R

80

Un altro buon motivo per utilizzare i puntatori sarebbe per le dichiarazioni a termine . In un progetto abbastanza grande possono davvero velocizzare i tempi di compilazione.


7
questo sta davvero aggiungendo al mix di informazioni utili, quindi sono contento che tu l'abbia resa una risposta!
TemplateRex

3
std :: shared_ptr <T> funziona anche con dichiarazioni a termine di T. (std :: unique_ptr <T> no )
berkus

13
@berkus: std::unique_ptr<T>funziona con dichiarazioni anticipate di T. Devi solo assicurarti che quando std::unique_ptr<T>viene chiamato il distruttore di , Tsia un tipo completo. Questo in genere significa che la tua classe che contiene il std::unique_ptr<T>dichiarante è il suo distruttore nel file header e la implementa nel file cpp (anche se l'implementazione è vuota).
David Stone,

I moduli risolveranno questo?
Trevor Hickey,

@TrevorHickey Vecchio commento, lo so, ma per rispondere comunque. I moduli non rimuoveranno la dipendenza, ma dovrebbero rendere la dipendenza molto economica, quasi gratuita in termini di costo delle prestazioni. Inoltre, se l'accelerazione generale dai moduli sarebbe sufficiente per ottenere i tempi di compilazione in un intervallo accettabile, non è nemmeno un problema.
Aidiakapi,

79

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. deletechiama questo distruttore per te.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Questo non ha nulla a che fare con la newsintassi 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::stringinvece, si std::stringridimensiona 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 asparola chiave. Quindi, se qualcuno volesse trattare un Entityoggetto come un Playeroggetto, 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 asparola 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 newparola 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 deletedell'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.


"Quindi! Test è essenzialmente una scorciatoia per verificare se il test è NULL o un puntatore non valido, il che significa che il cast è fallito." Penso che questa frase debba essere riscritta per chiarezza.
Berkus,

4
"La macchina hype Java vorrebbe farti credere" - forse nel 1997, ma questo è ora anacronistico, non c'è più motivazione per confrontare Java con C ++ nel 2014.
Matt R

15
Vecchia domanda, ma nel segmento del codice { std::string* s = new std::string; } delete s; // destructor called.... sicuramente questo deletenon funzionerà perché il compilatore non saprà più cosa sè?
badger5000,

2
NON sto dando -1, ma non sono d'accordo con le dichiarazioni iniziali come scritte. In primo luogo, non sono d'accordo sul fatto che esista un "hype" - potrebbe essere stato intorno a Y2K, ma ora entrambe le lingue sono ben comprese. Secondo, direi che sono abbastanza simili: C ++ è il figlio di C sposato con Simula, Java aggiunge Virtual Machine, Garbage Collector e HEAVILY riduce le funzionalità e C # semplifica e reintroduce le funzionalità mancanti su Java. Sì, questo rende gli schemi e l'uso valido enormemente diversi, ma è utile comprendere l'infrastruttura / il desing comune in modo da poter vedere le differenze.
Gerasimos R,

1
@James Matta: Hai ovviamente ragione sul fatto che la memoria è memoria e sono entrambi allocati dalla stessa memoria fisica, ma una cosa da considerare è che è molto comune ottenere caratteristiche di prestazione migliori lavorando con oggetti allocati in pila perché lo stack - o almeno i suoi livelli più alti - hanno una probabilità molto alta di essere "caldi" nella cache quando le funzioni entrano ed escono, mentre l'heap non ha un tale vantaggio, quindi se si sta inseguendo il puntatore nell'heap è possibile ottenere più mancate cache che voi probabilmente non sarebbe in pila. Ma tutta questa "casualità" normalmente favorisce lo stack.
Gerasimos R,

23

C ++ offre tre modi per passare un oggetto: per puntatore, per riferimento e per valore. Java ti limita con quest'ultima (l'unica eccezione sono i tipi primitivi come int, booleano ecc.). Se vuoi usare C ++ non solo come un giocattolo strano, allora farai meglio a conoscere la differenza tra questi tre modi.

Java finge che non ci siano problemi come "chi e quando dovrebbe distruggerlo?". La risposta è: il Garbage Collector, fantastico e terribile. Tuttavia, non può fornire una protezione del 100% contro le perdite di memoria (sì, Java può perdere la memoria ). In realtà, GC ti dà un falso senso di sicurezza. Più grande è il tuo SUV, più lunga è la tua strada verso l'evacuatore.

C ++ ti lascia faccia a faccia con la gestione del ciclo di vita degli oggetti. Bene, ci sono mezzi per affrontarlo ( famiglia di puntatori intelligenti , QObject in Qt e così via), ma nessuno di essi può essere usato in modo "spara e dimentica" come GC: dovresti sempre tenere a mente la gestione della memoria. Non solo dovresti preoccuparti di distruggere un oggetto, ma devi anche evitare di distruggere lo stesso oggetto più di una volta.

Non hai ancora paura? Ok: riferimenti ciclici: gestiscili da soli, umani. E ricorda: uccidi ogni oggetto esattamente una volta, a noi runtime C ++ non piacciono quelli che pasticciano con i cadaveri, lasciamo soli quelli morti.

Quindi, tornando alla tua domanda.

Quando si passa l'oggetto in base al valore, non al puntatore o al riferimento, si copia l'oggetto (l'intero oggetto, sia esso un paio di byte o un enorme dump del database - sei abbastanza intelligente da preoccuparti di evitarlo, non sono ' tu?) ogni volta che fai '='. E per accedere ai membri dell'oggetto, usi '.' (punto).

Quando si passa l'oggetto per puntatore, si copiano solo pochi byte (4 su sistemi a 32 bit, 8 su quelli a 64 bit), ovvero l'indirizzo di questo oggetto. E per mostrarlo a tutti, usi questo fantasioso operatore '->' quando accedi ai membri. Oppure puoi usare la combinazione di '*' e '.'.

Quando usi i riferimenti, ottieni il puntatore che finge di essere un valore. È un puntatore, ma accedi ai membri tramite '.'.

E, per farti saltare la testa ancora una volta: quando dichiari diverse variabili separate da virgole, allora (guarda le mani):

  • Il tipo è dato a tutti
  • Il modificatore di valore / puntatore / riferimento è individuale

Esempio:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrè deprecato, per favore non usarlo.
Neil

2
Abbastanza sicuro che non puoi avere un riferimento come membro senza fornire anche a un costruttore un elenco di inizializzazione che include la variabile di riferimento. (Un riferimento deve essere inizializzato immediatamente. Anche il corpo del costruttore è troppo tardi per impostarlo, IIRC.)
cHao,

20

In C ++, gli oggetti allocati nello stack (usando l' Object object;istruzione all'interno di un blocco) vivranno solo nell'ambito in cui sono dichiarati. Quando il blocco di codice termina l'esecuzione, l'oggetto dichiarato viene distrutto. Mentre se si alloca memoria sull'heap, usando Object* obj = new Object(), continuano a vivere nell'heap fino a quando non si chiama delete obj.

Vorrei creare un oggetto su heap quando mi piace usare l'oggetto non solo nel blocco di codice che lo ha dichiarato / allocato.


6
Object objnon è sempre nello stack, ad esempio globuli o variabili membro.
dieci

2
@LightnessRacesinOrbit Ho menzionato solo gli oggetti allocati in un blocco, non le variabili globali e dei membri. La cosa è che non era chiaro, ora corretto - aggiunto "all'interno di un blocco" nella risposta. Spero che non siano false informazioni ora :)
Karthik Kalyanasundaram il

20

Ma non riesco a capire perché dovremmo usarlo in questo modo?

Confronterò come funziona all'interno del corpo della funzione se usi:

Object myObject;

All'interno della funzione, myObjectverrai distrutto quando questa funzione tornerà. Quindi questo è utile se non hai bisogno del tuo oggetto al di fuori della tua funzione. Questo oggetto verrà inserito nello stack di thread corrente.

Se si scrive all'interno del corpo della funzione:

 Object *myObject = new Object;

quindi l'istanza della classe Object indicata da myObjectnon verrà distrutta una volta terminata la funzione e l'allocazione è nell'heap.

Ora, se sei un programmatore Java, il secondo esempio è più vicino a come funziona l'allocazione degli oggetti in Java. Questa linea: Object *myObject = new Object;è equivalente a Java: Object myObject = new Object();. La differenza è che sotto java myObject riceverà la spazzatura raccolta, mentre sotto c ++ non verrà liberata, devi da qualche parte chiamare esplicitamente `elimina myObject; ' altrimenti si introdurranno perdite di memoria.

Da c ++ 11 è possibile utilizzare modalità sicure di allocazioni dinamiche new Object:, memorizzando i valori in shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

inoltre, gli oggetti sono molto spesso memorizzati in contenitori, come mappe o vettori, gestiranno automaticamente una vita dei tuoi oggetti.


1
then myObject will not get destroyed once function endsLo farà assolutamente.
Razze di leggerezza in orbita

6
Nel caso del puntatore, myObjectverrà comunque distrutto, proprio come qualsiasi altra variabile locale. La differenza è che il suo valore è un puntatore a un oggetto, non l'oggetto stesso, e la distruzione di un muto puntatore non influisce sulla sua punta. Quindi l' oggetto sopravviverà a detta distruzione.
cao

Risolto il problema, le variabili locali (che includono il puntatore) ovviamente verranno liberate - sono in pila.
marcinj

13

Tecnicamente si tratta di un problema di allocazione della memoria, tuttavia qui ci sono altri due aspetti pratici di questo. Ha a che fare con due cose: 1) Ambito, quando si definisce un oggetto senza un puntatore non sarà più possibile accedervi dopo il blocco di codice in cui è definito, mentre se si definisce un puntatore con "nuovo" allora puoi accedervi da qualsiasi punto in cui hai un puntatore a questa memoria finché non chiami "elimina" sullo stesso puntatore. 2) Se si desidera passare argomenti a una funzione, si desidera passare un puntatore o un riferimento per essere più efficienti. Quando si passa un oggetto, l'oggetto viene copiato, se si tratta di un oggetto che utilizza molta memoria, ciò potrebbe richiedere un utilizzo della CPU (ad esempio, si copia un vettore pieno di dati). Quando si passa un puntatore, tutto ciò che si passa è un int (a seconda dell'implementazione, ma la maggior parte di essi è un int).

A parte questo, devi capire che il "nuovo" alloca memoria sull'heap che deve essere liberato ad un certo punto. Quando non devi usare "nuovo" ti suggerisco di usare una definizione di oggetto regolare "in pila".


6

Bene, la domanda principale è: perché dovrei usare un puntatore anziché l'oggetto stesso? E la mia risposta, non dovresti (quasi) mai usare il puntatore anziché l'oggetto, perché C ++ ha riferimenti , è più sicuro dei puntatori e garantisce le stesse prestazioni dei puntatori.

Un'altra cosa che hai menzionato nella tua domanda:

Object *myObject = new Object;

Come funziona? Crea puntatore di Objecttipo, alloca memoria per adattarsi a un oggetto e chiama costruttore predefinito, suona bene, giusto? Ma in realtà non è così buono, se hai allocato dinamicamente la memoria (parola chiave usata new), devi anche liberare memoria manualmente, ciò significa che nel codice dovresti avere:

delete myObject;

Questo chiama distruttore e libera memoria, sembra facile, tuttavia nei grandi progetti può essere difficile rilevare se un thread ha liberato memoria o meno, ma a tale scopo puoi provare puntatori condivisi , questi diminuiscono leggermente le prestazioni, ma è molto più facile lavorare con loro.


E ora alcune presentazioni sono finite e torniamo alla domanda.

È possibile utilizzare i puntatori anziché gli oggetti per ottenere prestazioni migliori durante il trasferimento dei dati tra le funzioni.

Dai un'occhiata, hai std::string(è anche oggetto) e contiene davvero molti dati, ad esempio XML di grandi dimensioni, ora devi analizzarli, ma per questo hai una funzione void foo(...)che può essere dichiarata in diversi modi:

  1. void foo(std::string xml); In questo caso copierai tutti i dati dalla tua variabile allo stack di funzioni, ci vorrà del tempo, quindi le tue prestazioni saranno basse.
  2. void foo(std::string* xml); In questo caso passerai il puntatore all'oggetto, la stessa velocità del passaggio della size_tvariabile, tuttavia questa dichiarazione è soggetta a errori, poiché puoi passare il NULLpuntatore o il puntatore non valido. I puntatori di solito vengono utilizzati Cperché non hanno riferimenti.
  3. void foo(std::string& xml); Qui passi il riferimento, fondamentalmente è lo stesso del passaggio del puntatore, ma il compilatore fa alcune cose e non puoi passare un riferimento non valido (in realtà è possibile creare una situazione con riferimento non valido, ma sta ingannando il compilatore).
  4. void foo(const std::string* xml); Qui è uguale al secondo, solo il valore del puntatore non può essere modificato.
  5. void foo(const std::string& xml); Qui è uguale al terzo, ma il valore dell'oggetto non può essere modificato.

Che altro voglio menzionare, puoi usare questi 5 modi per passare i dati indipendentemente dal modo di allocazione che hai scelto (con newo regolare ).


Un'altra cosa da menzionare, quando si crea un oggetto in modo regolare , si alloca memoria nello stack, ma mentre lo si crea con newsi alloca heap. È molto più veloce allocare lo stack, ma è un po 'piccolo per array di dati davvero grandi, quindi se hai bisogno di oggetti di grandi dimensioni dovresti usare l'heap, perché potresti ottenere un overflow dello stack, ma di solito questo problema viene risolto usando i contenitori STL e ricorda std::stringè anche contenitore, alcuni ragazzi l'hanno dimenticato :)


5

Diciamo che hai class Aquel contenuto class BQuando vuoi chiamare qualche funzione di class Bfuori class Aotterrai semplicemente un puntatore a questa classe e puoi fare quello che vuoi e cambierà anche il contesto diclass B nel tuoclass A

Ma fai attenzione con l'oggetto dinamico


5

Ci sono molti vantaggi nell'uso dei puntatori all'oggetto:

  1. Efficienza (come hai già sottolineato). Passare oggetti alle funzioni significa creare nuove copie dell'oggetto.
  2. Lavorare con oggetti di librerie di terze parti. Se il tuo oggetto appartiene a un codice di terze parti e gli autori intendono utilizzare i loro oggetti solo tramite puntatori (nessun costruttore di copie, ecc.), L'unico modo per passare attorno a questo oggetto è usare i puntatori. Il passaggio per valore può causare problemi. (Problemi di copia profonda / copia superficiale).
  3. se l'oggetto possiede una risorsa e si desidera che la proprietà non debba essere condivisa con altri oggetti.

3

Questo è stato discusso a lungo, ma in Java tutto è un puntatore. Non fa distinzione tra allocazioni di stack e heap (tutti gli oggetti sono allocati sull'heap), quindi non ti rendi conto che stai usando i puntatori. In C ++, puoi mescolare i due, a seconda delle esigenze di memoria. Le prestazioni e l'utilizzo della memoria sono più deterministici in C ++ (duh).


3
Object *myObject = new Object;

In questo modo verrà creato un riferimento a un oggetto (sull'heap) che deve essere eliminato in modo esplicito per evitare perdite di memoria .

Object myObject;

In questo modo verrà creato un oggetto (myObject) del tipo automatico (nello stack) che verrà automaticamente eliminato quando l'oggetto (myObject) esce dall'ambito.


1

Un puntatore fa riferimento direttamente alla posizione di memoria di un oggetto. Java non ha nulla di simile. Java ha riferimenti che fanno riferimento alla posizione dell'oggetto attraverso le tabelle hash. Con questi riferimenti non puoi fare nulla di simile all'aritmetica del puntatore in Java.

Per rispondere alla tua domanda, è solo la tua preferenza. Preferisco usare la sintassi simile a Java.


Tabelle hash? Forse in alcune JVM ma non ci contare.
Zan Lynx,

Che dire della JVM fornita con Java? Ovviamente puoi implementare QUALSIASI cosa che ti viene in mente come una JVM che utilizza direttamente i puntatori o un metodo che esegue la matematica dei puntatori. È come dire "le persone non muoiono per il raffreddore comune" e ottenere una risposta "Forse la maggior parte delle persone non lo fa ma non ci conta!" Ah ah
RioRicoRick,

2
@RioRicoRick HotSpot implementa i riferimenti Java come puntatori nativi, vedere docs.oracle.com/javase/7/docs/technotes/guides/vm/… Per quanto posso vedere, JRockit fa lo stesso. Entrambi supportano la compressione OOP, ma nessuno dei due usa mai tabelle hash. Le conseguenze sulle prestazioni sarebbero probabilmente disastrose. Inoltre, "è solo una tua preferenza" sembra implicare che i due siano semplicemente sintassi diverse per un comportamento equivalente, cosa che ovviamente non lo sono.
Max Barraclough,


0

Con puntatori ,

  • può parlare direttamente con la memoria.

  • può prevenire molte perdite di memoria di un programma manipolando i puntatori.


4
" in C ++, usando i puntatori, puoi creare un garbage collector personalizzato per il tuo programma " che sembra un'idea terribile.
quant

0

Uno dei motivi per utilizzare i puntatori è l'interfaccia con le funzioni C. Un altro motivo è quello di risparmiare memoria; per esempio: invece di passare un oggetto che contiene molti dati e ha un costruttore di copia ad alta intensità di processore a una funzione, basta passare un puntatore all'oggetto, risparmiando memoria e velocità soprattutto se sei in un ciclo, tuttavia un il riferimento sarebbe meglio in quel caso, a meno che non si stia utilizzando un array in stile C.


0

Nelle aree in cui l'utilizzo della memoria è al massimo, i puntatori sono utili. Ad esempio, considera un algoritmo minimax, in cui verranno generati migliaia di nodi usando la routine ricorsiva, e successivamente li userai per valutare la prossima mossa migliore nel gioco, la capacità di deallocare o ripristinare (come nei puntatori intelligenti) riduce significativamente il consumo di memoria. Mentre la variabile non puntatore continua ad occupare spazio fino a quando la sua chiamata ricorsiva non restituisce un valore.


0

Includerò un importante caso d'uso del puntatore. Quando si memorizza un oggetto nella classe base, ma potrebbe essere polimorfico.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Quindi in questo caso non puoi dichiarare bObj come oggetto diretto, devi avere un puntatore.


-5

"Necessità è la madre dell'invenzione." La differenza più importante che vorrei sottolineare è il risultato della mia esperienza di programmazione. A volte è necessario passare oggetti alle funzioni. In tal caso, se il tuo oggetto è di una classe molto grande, passandolo come oggetto copierà il suo stato (che potresti non volere .. E PU CAN ESSERE GRANDE SOVRACCARICO), risultando in un sovraccarico di copia dell'oggetto. Mentre il puntatore è fisso Dimensione di 4 byte (presupponendo 32 bit). Altri motivi sono già menzionati sopra ...


14
dovresti preferire il passaggio per riferimento
bolov

2
Consiglio di passare per riferimento costante come per la variabile std::string test;che abbiamo, void func(const std::string &) {}ma a meno che la funzione non debba cambiare l'input, nel qual caso consiglio di usare i puntatori (in modo che chiunque legga il codice se ne accorga &e capisca che la funzione può cambiare il suo input)
Inizio pagina Master

-7

Ci sono già molte risposte eccellenti, ma lascia che ti dia un esempio:

Ho una semplice classe di oggetti:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Faccio un vettore per tenerne un mazzo.

std::vector<Item> inventory;

Creo un milione di oggetti oggetto e li spingo nuovamente sul vettore. Ordino il vettore per nome, quindi eseguo una semplice ricerca binaria iterativa per un determinato nome di elemento. Collaudo il programma e ci vogliono più di 8 minuti per terminare l'esecuzione. Quindi cambio il mio inventario in questo modo:

std::vector<Item *> inventory;

... e crea il mio milione di oggetti oggetto tramite nuovo. Le SOLO modifiche che apporto al mio codice sono di usare i puntatori a Items, ad eccezione di un loop che aggiungo per la pulizia della memoria alla fine. Quel programma funziona in meno di 40 secondi, o meglio di un aumento di velocità 10x. EDIT: il codice è su http://pastebin.com/DK24SPeW Con le ottimizzazioni del compilatore mostra solo un aumento di 3,4x sulla macchina su cui l'ho appena testato, che è ancora considerevole.


2
Bene, stai confrontando i puntatori allora o confronti ancora gli oggetti reali? Dubito fortemente che un altro livello di riferimento indiretto possa migliorare le prestazioni. Si prega di fornire il codice! Ti ripulisci correttamente dopo?
stefan,

1
@stefan Metto a confronto i dati (in particolare il campo nome) degli oggetti sia per l'ordinamento che per la ricerca. Pulisco correttamente, come ho già detto nel post. l'accelerazione è probabilmente dovuta a due fattori: 1) std :: vector push_back () copia gli oggetti, quindi la versione del puntatore deve solo copiare un singolo puntatore per oggetto. Ciò ha un impatto multiplo sulle prestazioni, poiché non solo vengono copiati meno dati, ma l'allocatore di memoria della classe vettoriale viene ridotto di meno.
Darren,

2
Ecco il codice che mostra praticamente nessuna differenza per il tuo esempio: l'ordinamento. Il codice puntatore è più veloce del 6% rispetto al codice non puntatore solo per l'ordinamento, ma nel complesso è più lento del 10% rispetto al codice non puntatore. ideone.com/G0c7zw
stefan,

3
Parola chiave: push_back. Certamente questo copia. Avresti dovuto emplacetrovarti sul posto durante la creazione dei tuoi oggetti (a meno che tu non abbia bisogno che vengano memorizzati nella cache altrove).
underscore_d

1
I vettori di puntatori sono quasi sempre sbagliati. Si prega di non raccomandarli senza spiegare, in dettaglio, le avvertenze, i pro ei contro. Sembra che tu abbia trovato un professionista, che è solo una conseguenza di un contro-esempio scarsamente codificato, e lo ha travisato
Razze di leggerezza in orbita
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.