Che cosa c'è di male nei puntatori in C ++?

5

Per continuare la discussione in Perché i puntatori non sono consigliato durante la codifica con C ++?

Supponiamo di avere una classe che incapsula oggetti che necessitano dell'inizializzazione per essere validi, come un socket di rete.

// Blah manages some data and transmits it over a socket
class socket; // forward declaration, so nice weak linkage.      

class blah
{
  ... stuff 
  TcpSocket *socket;
}

~blah {
   // TcpSocket dtor handles disconnect
   delete socket; // or better, wrap it in a smart pointer
}

Il ctor assicura che socket sia marcato NULL, quindi più avanti nel codice quando ho le informazioni per inizializzare l'oggetto.

// initialising blah
if ( !socket ) {
   // I know socket hasn't been created/connected
   // create it in a known initialised state and handle any errors 
   // RAII is a good thing ! 
   socket = new TcpSocket(ip,port);
}

// and when i actually need to use it
if (socket) {
   // if socket exists then it must be connected and valid 
}

Questo sembra meglio che avere il socket nello stack, averlo creato in uno stato 'in sospeso' all'avvio del programma e quindi dover continuamente controllare alcune funzioni isOK () o isConnected () prima di ogni utilizzo.
Inoltre, se TcpSocket ctor genera un'eccezione, è molto più semplice da gestire nel momento in cui viene creata una connessione Tcp piuttosto che all'avvio del programma.

Ovviamente il socket è solo un esempio, ma mi viene difficile pensare quando un oggetto incapsulato con qualsiasi tipo di stato interno non deve essere creato e inizializzato con new .

    
posta Martin Beckett 02.09.2012 - 18:37
fonte

5 risposte

19

In generale, i costrutti e le tecniche di programmazione sono comunemente considerati "cattivi" quando sono disponibili alternative "migliori" per un'attività specifica. L'uso di un puntatore può essere tecnicamente corretto in molti punti, ma è raro in C ++ che si presenti una situazione in cui l'uso di un puntatore raw non ha un'alternativa migliore.

La maggior parte delle volte, l'utilizzo di riferimenti, puntatori intelligenti, iteratori e contenitori di librerie standard porterà a un codice più sicuro / più pulito / più idiomatico rispetto a una soluzione equivalente che utilizza i puntatori; e di solito senza costi aggiuntivi per il programmatore (abbastanza spesso a un costo inferiore di fatto).

Ci saranno sempre occasioni in cui un puntatore grezzo è l'opzione più sensata, e in quei casi nessuno guadagna cercando di trovare modi "intelligenti" per evitare i puntatori; in particolare, se si evita un puntatore non elaborato, si rischia di rischiare cambiamenti di rottura più grandi rispetto al codice legacy funzionante che altrimenti non avrebbe dovuto essere modificato; ma per il nuovo codice almeno queste situazioni sono insolite per chiunque usi una moderna implementazione in C ++ 11.

    
risposta data 02.09.2012 - 21:54
fonte
2

I puntatori hanno buoni motivi per essere usati anche, come sostieni. Anche se, nella maggior parte dei casi, dovresti farla franca con i puntatori intelligenti standardizzati introdotti in C ++ 11 per ottenere ulteriore sicurezza, pur mantenendo i puntatori di flessibilità che ti danno.

Tuttavia, è anche vero che potrebbero sembrare confusi per le persone che non hanno familiarità con C ++, quindi dal punto di vista della manutenzione sarebbe forse meglio affidarsi alle tecniche e alle metodologie più semplici per implementare le cose.

    
risposta data 02.09.2012 - 18:48
fonte
1

Secondo me, i puntatori sono descritti come difficili da gestire o cattivi perché interrompono 2 aspetti importanti di qualsiasi progetto di applicazione:

  • semantica
  • sintassi

se ti ricordi, un concetto di base nel mondo della programmazione è dato dal fatto che se scrivessi 4 su una lavagna, questo non significa nulla fino a quando non darò un contesto e una semantica a ciò che ho scritto sulla lavagna, per esempio se dopo aver scritto 45 dico che quello che è sulla lavagna è la mia età, puoi ottenere 45 come una vera informazione, ma senza un contesto / semantica che è inutile e senza una chiara interpretazione: la vera informazione e la sua rappresentazione sono diversa.

Questo è ancora più chiaro nella tecnologia come OpenGL, non si può fare quasi nulla con il puntatore se non si fornisce un contesto, la stessa cosa accade in C ++, se non si fornisce un contesto (un target tipo) per un puntatore o un riferimento, il puntatore da solo non significa nulla ed è inutile.

La sintassi è importante anche perché fissa i limiti della logica aziendale dell'applicazione e della durata dei componenti, i puntatori possono potenzialmente violare tutti i limiti e la tua logica di business, tuttavia, i puntatori contengono riferimenti in modo che non siano nemmeno hanno uno stato reale, puntano semplicemente a qualcosa in memoria che non possono gestire durante il ciclo di vita dell'applicazione, sono più simili a post-it con un indirizzo di memoria piuttosto che oggetti con uno stato ben definito e una durata ben definita.

Tutte le principali differenze tra il vecchio C ++ e C ++ 11 sono le differenze sul design, il che significa sintassi e semantica che possono guidare la logica della tua applicazione; i puntatori intelligenti non sono altro che dei puntatori, ma con un grande vantaggio legato a un design specifico che si trova all'interno del puntatore stesso, quindi puoi avere più controllo su di esso.

Alla fine i puntatori non possono avere uno stato e hanno bisogno di un contesto da gestire, questo è qualcosa che un progetto di applicazione sicuro e buono non può normalmente accettare, i puntatori intelligenti sono un passo nel mondo orientato al design che è fondamentalmente l'approccio moderno al mondo della programmazione.

    
risposta data 02.09.2012 - 19:10
fonte
1

Il puntatore non è assolutamente una brutta cosa, ti dà accesso diretto alla memoria, per le migliori prestazioni.
A volte è difficile tenere traccia della durata, quando il puntatore deve essere passato attorno alle classi, questo è il punto in cui il puntatore intelligente può essere d'aiuto. Se puoi sacrificare il piccolo impatto sulle prestazioni, allora sì, usa il puntatore intelligente se è disponibile.

L'aspetto negativo dell'utilizzo del puntatore intelligente è la dipendenza del file di intestazione per la classe. Con il puntatore raw, puoi semplicemente inoltrare la classe / struct e avere una dipendenza più pulita durante la compilazione.
È inoltre necessario assicurarsi che il puntatore intelligente che si utilizza sia protetto da thread in caso di applicazione multi-threading.

Se la tua classe è abbastanza semplice, direi bastone con puntatore normale piuttosto che intelligente.

    
risposta data 08.05.2014 - 09:00
fonte
0

I puntatori sono negativi per i seguenti motivi:

  1. Le informazioni sulle aree di memoria scompaiono. Non è più chiaro se la tua memoria TcpSocket è all'interno dell'oggetto blah o al di fuori di esso. Riferimenti e composizione possono risolverlo. (riferimento = esterno, composizione = interno)
  2. Il puntatore può essere NULL. Quanto bene ti aspetti che il programma funzioni se il comportamento della chiamata di funzione non viene rispettato. Se chiamate la funzione connect (), è meglio fare la connessione, ma fallire semplicemente perché alcuni ptr sono NULL non è un buon comportamento - i guasti dovrebbero avere motivi reali come la rete scollegata. Fallire semplicemente perché alcune chiamate precedenti non sono riuscite non è un buon comportamento.
  3. Un comportamento dinamico come il ptr == NULL dovrebbe essere fatto con i booleani e non con questo tipo di comportamento nascosto dove ptr == NULL. Quindi sarebbe più facile capire il motivo dell'errore quando i booleani hanno nomi propri indipendenti dal puntatore.
  4. La scelta migliore per i guasti è che non si verificano mai.
  5. La seconda scelta migliore è quella parte dell'applicazione disabilitata quando la rete non è disponibile - come se non venisse visualizzata alcuna nuova informazione e la connessione venisse ripristinata il prima possibile.
  6. Se le funzioni del socket possono fallire, il prototipo della funzione avrà meglio queste informazioni. Il tuo void connect () non è accettabile, int connect () con codice di errore è migliore. Anche restituire un oggetto contenente bit di fail è corretto.

Esempio di quale tipo di alternative sono disponibili: (quella di composizione)

class Blah {
public:
  Blah(ip,port) : s(ip,port) { }
private:
  TcpSocket s;
  bool failbit;
};

O con riferimenti:

class Blah {
public:
  Blah(TcpSocket &s) : s(s) { }
private:
  TcpSocket &s;
};

Se la connessione deve essere creata in seguito, come l'indirizzo non noto abbastanza presto nel costruttore, quindi utilizzare questo:

class Blah {
public:
   Blah() : sock(NULL) { }
   TcpSocket &connect(ip, port) {
     delete sock;
     sock = new TcpSocket(ip,port);
     return *sock;
    }
  ~Blah() { delete sock; }
private:
  TcpSocket *sock;
};

Ma di solito non è la migliore alternativa ... (chiama connect due volte e quella precedente perde la connessione - il TcpSocket restituito non deve essere memorizzato da nessuna parte) La prossima alternativa avrà bisogno di std :: vector < TCPSocket * > ...

class Blah {
public:
   Blah() : vec(), error("logfile.txt") { }
   int connect(ip,port) {
      vec.push_back(new TcpSocket(ip,port));
      return vec.size()-1;
   }
   int write(int conn, char *data) {
      if (vec[conn])
         { vec[conn]->write(data); return OK; }
      else return ERR;
   }
  TcpSocket &connection(int conn) const 
      { if (vec[conn]) return *vec[conn]; else return error; }
  void disconnect(int conn) { delete vec[conn]; vec[conn]=NULL; }
   ~Blah() { for(int i=0;i<vec.size();i++) delete vec[i]; }
private:
   std::vector<TcpSocket*> vec;
   TcpSocket error;
};

Si noti che la restituzione dell'indice alla connessione std :: vector impedisce la rimozione delle connessioni (gli indici si rompono / devono renderli NULL). L'ultimo ha solo un problema che connect () restituisce valore e write () restituisce il valore è diverso. Dovresti usare un qualche tipo di controllo di tipo per renderli diversi:

struct Err { int error; };
Err write(int conn, char *data);
    
risposta data 02.09.2012 - 21:13
fonte

Leggi altre domande sui tag