Domanda

Ricordo di aver appreso per la prima volta dei vettori in STL e dopo un po' di tempo volevo utilizzare un vettore di bool per uno dei miei progetti.Dopo aver visto alcuni comportamenti strani e aver fatto qualche ricerca, l'ho imparato un vettore di bool non è realmente un vettore di bool.

Ci sono altre trappole comuni da evitare in C++?

È stato utile?

Soluzione

Un breve elenco potrebbe essere:

  • Evitare perdite di memoria utilizzando puntatori condivisi per gestire l'allocazione e la pulizia della memoria
  • Usa il L'acquisizione delle risorse è l'inizializzazione (RAII) linguaggio per gestire la pulizia delle risorse, soprattutto in presenza di eccezioni
  • Evitare di chiamare funzioni virtuali nei costruttori
  • Ove possibile, utilizzare tecniche di codifica minimaliste, ad esempio dichiarando le variabili solo quando necessario, definendo l'ambito delle variabili e progettando in anticipo ove possibile.
  • Comprendi veramente la gestione delle eccezioni nel tuo codice, sia per quanto riguarda le eccezioni lanciate, sia per quanto riguarda quelle lanciate dalle classi che potresti utilizzare indirettamente.Ciò è particolarmente importante in presenza di modelli.

RAII, puntatori condivisi e codifica minimalista ovviamente non sono specifici del C++, ma aiutano a evitare problemi che emergono frequentemente durante lo sviluppo nel linguaggio.

Alcuni ottimi libri su questo argomento sono:

  • C++ efficace - Scott Meyers
  • C++ più efficace - Scott Meyers
  • Standard di codifica C++ - Sutter e Alexandrescu
  • Domande frequenti su C++ - Cline

Leggere questi libri mi ha aiutato più di ogni altra cosa a evitare il tipo di trappole di cui mi chiedi.

Altri suggerimenti

Insidie ​​in ordine decrescente di importanza

Prima di tutto, dovresti visitare il premiato Domande frequenti su C++.Ha molte buone risposte alle insidie.Se hai ulteriori domande, visita ##c++ SU irc.freenode.org In IRC.Siamo lieti di aiutarti, se possiamo.Nota che tutte le seguenti insidie ​​​​sono state scritte originariamente.Non vengono semplicemente copiati da fonti casuali.


delete[] SU new, delete SU new[]

Soluzione:Facendo quanto sopra si ottiene un comportamento indefinito:Tutto potrebbe succedere.Comprendi il tuo codice e cosa fa, e sempre delete[] cosa tu new[], E delete cosa tu new, allora ciò non accadrà.

Eccezione:

typedef T type[N]; T * pT = new type; delete[] pT;

Devi delete[] anche se tu new, dal momento che hai nuovo un array.Quindi, se stai lavorando con typedef, prestare particolare attenzione.


Chiamare una funzione virtuale in un costruttore o distruttore

Soluzione:La chiamata di una funzione virtuale non chiamerà le funzioni di override nelle classi derivate.Chiamando a pura funzione virtuale in un costruttore o distruttore è un comportamento indefinito.


Chiamando delete O delete[] su un puntatore già eliminato

Soluzione:Assegna 0 a ogni puntatore che elimini.Chiamando delete O delete[] su un puntatore nullo non fa nulla.


Prendendo la dimensione di un puntatore, quando si deve calcolare il numero di elementi di un 'array'.

Soluzione:Passa il numero di elementi accanto al puntatore quando devi passare un array come puntatore in una funzione.Utilizza la funzione proposta Qui se prendi la dimensione di un array che dovrebbe essere realmente un array.


Utilizzando un array come se fosse un puntatore.Quindi, utilizzando T ** per un array bidimensionale.

Soluzione:Vedere Qui per il motivo per cui sono diversi e come gestirli.


Scrivere su una stringa letterale: char * c = "hello"; *c = 'B';

Soluzione:Assegna un array inizializzato dai dati della stringa letterale, quindi puoi scrivervi:

char c[] = "hello"; *c = 'B';

Scrivere su una stringa letterale è un comportamento indefinito.Ad ogni modo, la conversione di cui sopra da una stringa letterale a char * è deprecato.Quindi i compilatori probabilmente avviseranno se si aumenta il livello di avviso.


Creare risorse, per poi dimenticare di liberarle quando qualcosa si lancia.

Soluzione:Usa puntatori intelligenti come std::unique_ptr O std::shared_ptr come sottolineato da altre risposte.


Modificando un oggetto due volte come in questo esempio: i = ++i;

Soluzione:Quanto sopra avrebbe dovuto assegnare a i il valore di i+1.Ma cosa fa non è definito.Invece di incrementare i e assegnando il risultato, questo cambia i anche sul lato destro.La modifica di un oggetto tra due punti di sequenza è un comportamento indefinito.I punti della sequenza includono ||, &&, comma-operator, semicolon E entering a function (elenco non esaustivo!).Modificare il codice nel seguente per farlo funzionare correttamente: i = i + 1;


Problemi vari

Dimenticare di svuotare i flussi prima di chiamare una funzione di blocco come sleep.

Soluzione:Svuota il flusso anche tramite streaming std::endl invece di \n o chiamando stream.flush();.


Dichiarare una funzione invece di una variabile.

Soluzione:Il problema sorge perché il compilatore interpreta ad esempio

Type t(other_type(value));

come dichiarazione di funzione di una funzione t ritorno Type e avere un parametro di tipo other_type che è chiamato value.Lo risolvi mettendo tra parentesi il primo argomento.Ora ottieni una variabile t di tipo Type:

Type t((other_type(value)));

Chiamando la funzione di un oggetto libero dichiarato solo nell'unità di traduzione corrente (.cpp file).

Soluzione:Lo standard non definisce l'ordine di creazione degli oggetti liberi (nell'ambito dello spazio dei nomi) definiti tra diverse unità di traduzione.Chiamare una funzione membro su un oggetto non ancora costruito è un comportamento indefinito.Puoi invece definire la seguente funzione nell'unità di traduzione dell'oggetto e chiamarla da altre:

House & getTheHouse() { static House h; return h; }

Ciò creerebbe l'oggetto su richiesta e ti lascerebbe con un oggetto completamente costruito nel momento in cui chiami le funzioni su di esso.


Definizione di un modello in a .cpp file, mentre è utilizzato in un file diverso .cpp file.

Soluzione:Quasi sempre otterrai errori come undefined reference to ....Inserisci tutte le definizioni del modello in un'intestazione, in modo che quando il compilatore le utilizza, possa già produrre il codice necessario.


static_cast<Derived*>(base); se base è un puntatore a una classe base virtuale di Derived.

Soluzione:Una classe base virtuale è una base che ricorre una sola volta, anche se viene ereditata più di una volta indirettamente da classi diverse in un albero di ereditarietà.Fare quanto sopra non è consentito dallo Standard.Usa Dynamic_cast per farlo e assicurati che la tua classe base sia polimorfica.


dynamic_cast<Derived*>(ptr_to_base); se la base non è polimorfica

Soluzione:Lo standard non consente il downcast di un puntatore o di un riferimento quando l'oggetto passato non è polimorfico.Essa o una delle sue classi base deve avere una funzione virtuale.


Far accettare la tua funzione T const **

Soluzione:Potresti pensare che sia più sicuro dell'uso T **, ma in realtà causerà mal di testa alle persone che vogliono passare T**:La norma non lo consente.Fornisce un chiaro esempio del motivo per cui non è consentito:

int main() {
    char const c = ’c’;
    char* pc;
    char const** pcc = &pc; //1: not allowed
    *pcc = &c;
    *pc = ’C’; //2: modifies a const object
}

Accetta sempre T const* const*; Invece.

Un altro thread (chiuso) di insidie ​​​​sul C++, quindi le persone che li cercano li troveranno, è la domanda Stack Overflow Insidie ​​del C++.

Alcuni devono avere libri sul C++ che ti aiuteranno a evitare le trappole comuni del C++:

C++ efficace
C++ più efficace
STL efficace

Il libro Effective STL spiega il vettore del problema dei bool :)

Brian ha una lista fantastica:Aggiungerei "Contrassegna sempre espliciti i costruttori ad argomento singolo (tranne in quei rari casi in cui desideri il cast automatico)."

Non proprio un consiglio specifico, ma una linea guida generale:controlla le tue fontiIl C++ è un linguaggio vecchio ed è cambiato molto nel corso degli anni.Le migliori pratiche sono cambiate con esso, ma sfortunatamente ci sono ancora molte vecchie informazioni là fuori.Ci sono stati alcuni ottimi consigli sui libri qui: posso acquistare in secondo luogo tutti i libri di Scott Meyers C++.Acquisisci familiarità con Boost e con gli stili di codifica utilizzati in Boost: le persone coinvolte in quel progetto sono all'avanguardia nella progettazione C++.

Non reinventare la ruota.Acquisisci familiarità con STL e Boost e utilizza le loro strutture quando possibile lanciando le tue.In particolare, utilizza stringhe e raccolte STL a meno che tu non abbia un'ottima ragione per non farlo.Impara a conoscere molto bene auto_ptr e la libreria dei puntatori intelligenti Boost, capisci in quali circostanze è previsto l'utilizzo di ciascun tipo di puntatore intelligente e quindi utilizza i puntatori intelligenti ovunque potresti altrimenti utilizzare puntatori grezzi.Il tuo codice sarà altrettanto efficiente e molto meno soggetto a perdite di memoria.

Utilizza static_cast, Dynamic_cast, const_cast e reinterpret_cast invece dei cast in stile C.A differenza dei cast in stile C, ti faranno sapere se stai davvero chiedendo un tipo di cast diverso da quello che pensi di chiedere.E risaltano visivamente, avvisando il lettore che è in corso un cast.

La pagina web Insidie ​​del C++ di Scott Wheeler copre alcune delle principali insidie ​​del C++.

Due trucchi che vorrei non aver imparato nel modo più duro:

(1) Molti output (come printf) vengono memorizzati nel buffer per impostazione predefinita.Se stai eseguendo il debug del codice che va in crash e stai utilizzando istruzioni di debug memorizzate nel buffer, l'ultimo output che vedi potrebbe non essere davvero l'ultima istruzione print incontrata nel codice.La soluzione è svuotare il buffer dopo ogni stampa di debug (o disattivare del tutto il buffering).

(2) Fare attenzione alle inizializzazioni: (a) evitare istanze di classi come globali/statiche;e (b) provare a inizializzare tutte le variabili membro su un valore sicuro in un ctor, anche se si tratta di un valore banale come NULL per i puntatori.

Ragionamento:l'ordine dell'inizializzazione dell'oggetto globale non è garantito (i globali includono variabili statiche), quindi potresti ritrovarti con un codice che sembra fallire in modo non deterministico poiché dipende dall'inizializzazione dell'oggetto X prima dell'oggetto Y.Se non inizializzi esplicitamente una variabile di tipo primitivo, come un membro bool o enum di una classe, ti ritroverai con valori diversi in situazioni sorprendenti: ancora una volta, il comportamento può sembrare molto non deterministico.

Ne ho già parlato qualche volta, ma i libri di Scott Meyers C++ efficace E STL efficace valgono davvero tanto oro per aver aiutato con C++.

Ora che ci penso, quello di Steven Dewhurst C++ Gotchas è anche un'ottima risorsa "dalla trincea".Il suo articolo sull'implementazione delle proprie eccezioni e su come dovrebbero essere costruite mi ha davvero aiutato in un progetto.

Usare C++ come C.Avere un ciclo di creazione e rilascio nel codice.

In C++, questo non è sicuro rispetto alle eccezioni e quindi il rilascio potrebbe non essere eseguito.In C++, usiamo RAII risolvere questo problema.

Tutte le risorse che hanno una creazione e un rilascio manuale dovrebbero essere racchiuse in un oggetto in modo che queste azioni vengano eseguite nel costruttore/distruttore.

// C Code
void myFunc()
{
    Plop*   plop = createMyPlopResource();

    // Use the plop

    releaseMyPlopResource(plop);
}

In C++, questo dovrebbe essere racchiuso in un oggetto:

// C++
class PlopResource
{
    public:
        PlopResource()
        {
            mPlop=createMyPlopResource();
            // handle exceptions and errors.
        }
        ~PlopResource()
        {
             releaseMyPlopResource(mPlop);
        }
    private:
        Plop*  mPlop;
 };

void myFunc()
{
    PlopResource  plop;

    // Use the plop
    // Exception safe release on exit.
}

Il libro C++ Gotchas potrebbe rivelarsi utile.

Ecco alcune buche in cui ho avuto la sfortuna di cadere.Tutto ciò ha delle buone ragioni che ho capito solo dopo essere stato morso da un comportamento che mi ha sorpreso.

  • virtual funzioni nei costruttori non lo sono.

  • Non violare il ODR (regola di una definizione), ecco a cosa servono gli spazi dei nomi anonimi (tra le altre cose).

  • L'ordine di inizializzazione dei membri dipende dall'ordine in cui vengono dichiarati.

    class bar {
        vector<int> vec_;
        unsigned size_; // Note size_ declared *after* vec_
    public:
        bar(unsigned size)
            : size_(size)
            , vec_(size_) // size_ is uninitialized
            {}
    };
    
  • Valori predefiniti e virtual hanno una semantica diversa.

    class base {
    public:
        virtual foo(int i = 42) { cout << "base " << i; }
    };
    
    class derived : public base {
    public:
        virtual foo(int i = 12) { cout << "derived "<< i; }
    };
    
    derived d;
    base& b = d;
    b.foo(); // Outputs `derived 42`
    

L'insidia più importante per gli sviluppatori alle prime armi è evitare confusione tra C e C++.Il C++ non dovrebbe mai essere trattato come un semplice C o un C con classi perché questo ne riduce la potenza e può renderlo persino pericoloso (specialmente quando si utilizza la memoria come in C).

Guardare boost.org.Fornisce molte funzionalità aggiuntive, in particolare le implementazioni del puntatore intelligente.

PRQA hanno uno standard di codifica C++ eccellente e gratuito basato sui libri di Scott Meyers, Bjarne Stroustrop e Herb Sutter.Riunisce tutte queste informazioni in un unico documento.

  1. Non leggere il Domande frequenti su C++ Lite.Spiega molte pratiche cattive (e buone!)
  2. Non utilizzare Aumento.Ti risparmierai molta frustrazione sfruttando Boost dove possibile.

Fare attenzione quando si utilizzano puntatori intelligenti e classi contenitore.

Evitare pseudo classi e quasi classi...Fondamentalmente sovraprogettazione.

Dimenticando di definire un distruttore della classe base virtual.Ciò significa che chiamare delete su una Base* non finirà per distruggere la parte derivata.

Mantieni gli spazi dei nomi chiari (inclusi struct, class, namespace e using).Questa è la mia frustrazione numero uno quando il programma semplicemente non si compila.

Per fare confusione, usa molto i puntatori diretti.Invece, usa RAII per quasi tutto, assicurandoti ovviamente di utilizzare i giusti puntatori intelligenti.Se scrivi "elimina" ovunque al di fuori di una classe di tipo handle o puntatore, molto probabilmente stai sbagliando.

  • Blizpasta.È enorme, lo vedo spesso...

  • Le variabili non inizializzate sono un errore enorme commesso dai miei studenti.Molte persone Java dimenticano che semplicemente dicendo "int counter" non si imposta il contatore su 0.Dato che devi definire le variabili nel file h (e inizializzarle nel costruttore/configurazione di un oggetto), è facile dimenticarlo.

  • Errori off-by-one attivati for loop/accesso all'array.

  • Non pulire correttamente il codice oggetto all'avvio del voodoo.

  • static_cast abbattuto su una classe base virtuale

Non proprio...Ora riguardo al mio malinteso:Ho pensato che A in seguito era una classe base virtuale quando in realtà non lo è;è, secondo 10.3.1, a classe polimorfica.Utilizzando static_cast qui sembra che vada bene.

struct B { virtual ~B() {} };

struct D : B { };

In sintesi, sì, questa è una trappola pericolosa.

Controlla sempre un puntatore prima di dereferenziarlo.In C, di solito potresti contare su un arresto anomalo nel punto in cui dereferenzia un puntatore errato;in C++, puoi creare un riferimento non valido che si bloccherà in un punto molto lontano dall'origine del problema.

class SomeClass
{
    ...
    void DoSomething()
    {
        ++counter;    // crash here!
    }
    int counter;
};

void Foo(SomeClass & ref)
{
    ...
    ref.DoSomething();    // if DoSomething is virtual, you might crash here
    ...
}

void Bar(SomeClass * ptr)
{
    Foo(*ptr);    // if ptr is NULL, you have created an invalid reference
                  // which probably WILL NOT crash here
}

Dimenticare un & e creando così una copia invece di un riferimento.

Mi è successo due volte in modi diversi:

  • Un'istanza era in un elenco di argomenti, che causava l'inserimento di un oggetto di grandi dimensioni nello stack con il risultato di un overflow dello stack e di un arresto anomalo del sistema incorporato.

  • Ho dimenticato il & su una variabile di istanza, con l'effetto che l'oggetto è stato copiato.Dopo essermi registrato come ascoltatore della copia mi sono chiesto perché non ho mai ricevuto i callback dall'oggetto originale.

Entrambi erano piuttosto difficili da individuare, perché la differenza è piccola e difficile da vedere, e per il resto oggetti e riferimenti vengono usati sintatticamente allo stesso modo.

L'intenzione è (x == 10):

if (x = 10) {
    //Do something
}

Pensavo che non avrei mai commesso questo errore, ma in realtà l'ho fatto di recente.

Il saggio/articolo Puntatori, riferimenti e valori è molto utileSi parla di evitare insidie ​​e di buone pratiche.Puoi anche sfogliare l'intero sito, che contiene suggerimenti di programmazione, principalmente per C++.

Ho passato molti anni a sviluppare C++.Ho scritto un breve riepilogo dei problemi che ho avuto con esso anni fa.I compilatori conformi agli standard non sono più un vero problema, ma sospetto che le altre trappole delineate siano ancora valide.

#include <boost/shared_ptr.hpp>
class A {
public:
  void nuke() {
     boost::shared_ptr<A> (this);
  }
};

int main(int argc, char** argv) {
  A a;
  a.nuke();
  return(0);
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top