Frage

Ich erinnere mich, dass ich zum ersten Mal etwas über Vektoren in der STL gelernt habe und nach einiger Zeit einen Vektor aus Bools für eines meiner Projekte verwenden wollte.Nachdem ich einige seltsame Verhaltensweisen beobachtet und einige Nachforschungen angestellt hatte, habe ich das herausgefunden Ein Bool-Vektor ist nicht wirklich ein Bool-Vektor.

Gibt es in C++ noch weitere häufige Fallstricke, die es zu vermeiden gilt?

War es hilfreich?

Lösung

Eine kurze Liste könnte sein:

  • Vermeiden Sie Speicherlecks, indem Sie gemeinsam genutzte Zeiger verwenden, um die Speicherzuweisung und -bereinigung zu verwalten
  • Benutzen Sie die Ressourcenbeschaffung ist Initialisierung (RAII)-Redewendung zur Verwaltung der Ressourcenbereinigung – insbesondere bei Ausnahmen
  • Vermeiden Sie den Aufruf virtueller Funktionen in Konstruktoren
  • Setzen Sie nach Möglichkeit minimalistische Codierungstechniken ein – zum Beispiel die Deklaration von Variablen nur bei Bedarf, die Festlegung des Gültigkeitsbereichs von Variablen und ein möglichst frühzeitiges Design.
  • Verstehen Sie die Ausnahmebehandlung in Ihrem Code wirklich – sowohl im Hinblick auf Ausnahmen, die Sie auslösen, als auch auf Ausnahmen, die von Klassen ausgelöst werden, die Sie möglicherweise indirekt verwenden.Dies ist besonders wichtig, wenn Vorlagen vorhanden sind.

RAII, Shared Pointer und minimalistische Codierung sind natürlich nicht spezifisch für C++, aber sie helfen, Probleme zu vermeiden, die bei der Entwicklung in der Sprache häufig auftreten.

Einige ausgezeichnete Bücher zu diesem Thema sind:

  • Effektives C++ – Scott Meyers
  • Effektiveres C++ – Scott Meyers
  • C++-Codierungsstandards – Sutter & Alexandrescu
  • C++-FAQs – Cline

Das Lesen dieser Bücher hat mir mehr als alles andere dabei geholfen, die von Ihnen genannten Fallstricke zu vermeiden.

Andere Tipps

Fallstricke in absteigender Reihenfolge ihrer Bedeutung

Zunächst sollten Sie den Preisträger besuchen C++-FAQ.Es gibt viele gute Antworten auf Fallstricke.Wenn Sie weitere Fragen haben, besuchen Sie ##c++ An irc.freenode.org In IRC.Wir helfen Ihnen gerne weiter, wenn wir können.Beachten Sie, dass alle folgenden Fallstricke ursprünglich geschrieben wurden.Sie werden nicht einfach aus zufälligen Quellen kopiert.


delete[] An new, delete An new[]

Lösung:Wenn Sie die oben genannten Schritte ausführen, kommt es zu undefiniertem Verhalten:Alles könnte passieren.Verstehen Sie Ihren Code und seine Funktionsweise, und zwar immer delete[] was du new[], Und delete was du new, dann wird das nicht passieren.

Ausnahme:

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

Du brauchst delete[] obwohl du new, da Sie ein Array neu erstellt haben.Also, wenn Sie mit arbeiten typedef, Seien Sie besonders vorsichtig.


Aufrufen einer virtuellen Funktion in einem Konstruktor oder Destruktor

Lösung:Durch den Aufruf einer virtuellen Funktion werden die übergeordneten Funktionen in den abgeleiteten Klassen nicht aufgerufen.Anruf bei a rein virtuelle Funktion in einem Konstruktor oder Destruktor ist undefiniertes Verhalten.


Berufung delete oder delete[] auf einen bereits gelöschten Zeiger

Lösung:Weisen Sie jedem Zeiger, den Sie löschen, 0 zu.Berufung delete oder delete[] auf einem Nullzeiger passiert nichts.


Nehmen Sie die Größe eines Zeigers, wenn die Anzahl der Elemente eines „Arrays“ berechnet werden soll.

Lösung:Übergeben Sie die Anzahl der Elemente neben dem Zeiger, wenn Sie ein Array als Zeiger an eine Funktion übergeben müssen.Nutzen Sie die vorgeschlagene Funktion Hier Wenn Sie die Größe eines Arrays annehmen, soll das tatsächlich ein Array sein.


Verwenden eines Arrays, als wäre es ein Zeiger.Also verwenden T ** für ein zweidimensionales Array.

Lösung:Sehen Hier Warum sie unterschiedlich sind und wie Sie damit umgehen.


Schreiben in ein String-Literal: char * c = "hello"; *c = 'B';

Lösung:Ordnen Sie ein Array zu, das aus den Daten des String-Literals initialisiert wird, und schreiben Sie dann darauf:

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

Das Schreiben in ein String-Literal ist ein undefiniertes Verhalten.Wie auch immer, die obige Konvertierung von einem String-Literal in char * ist veraltet.Daher werden Compiler wahrscheinlich eine Warnung ausgeben, wenn Sie die Warnstufe erhöhen.


Ressourcen schaffen und dann vergessen, sie freizugeben, wenn etwas aus dem Ruder läuft.

Lösung:Verwenden Sie intelligente Zeiger wie std::unique_ptr oder std::shared_ptr wie aus anderen Antworten hervorgeht.


Ein Objekt zweimal ändern, wie in diesem Beispiel: i = ++i;

Lösung:Das Obige sollte zuordnen i der Wert von i+1.Aber was es tut, ist nicht definiert.Anstatt zu erhöhen i und wenn man das Ergebnis zuordnet, ändert es sich i auch auf der rechten Seite.Das Ändern eines Objekts zwischen zwei Sequenzpunkten ist ein undefiniertes Verhalten.Sequenzpunkte umfassen ||, &&, comma-operator, semicolon Und entering a function (nicht erschöpfende Liste!).Ändern Sie den Code wie folgt, damit er sich korrekt verhält: i = i + 1;


Sonstige Probleme

Vergessen, Streams zu leeren, bevor eine Blockierungsfunktion wie aufgerufen wird sleep.

Lösung:Spülen Sie den Stream entweder durch Streaming std::endl anstatt \n oder telefonisch stream.flush();.


Deklarieren einer Funktion anstelle einer Variablen.

Lösung:Das Problem entsteht, weil der Compiler beispielsweise interpretiert

Type t(other_type(value));

als Funktionsdeklaration einer Funktion t zurückkehren Type und einen Parameter vom Typ haben other_type Was heisst value.Sie lösen es, indem Sie das erste Argument in Klammern setzen.Jetzt erhalten Sie eine Variable t vom Typ Type:

Type t((other_type(value)));

Aufrufen der Funktion eines freien Objekts, das nur in der aktuellen Übersetzungseinheit deklariert ist (.cpp Datei).

Lösung:Der Standard definiert nicht die Reihenfolge der Erstellung freier Objekte (im Namespace-Bereich), die über verschiedene Übersetzungseinheiten hinweg definiert sind.Das Aufrufen einer Memberfunktion für ein noch nicht erstelltes Objekt ist undefiniertes Verhalten.Sie können stattdessen die folgende Funktion in der Übersetzungseinheit des Objekts definieren und sie von anderen aus aufrufen:

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

Dadurch würde das Objekt bei Bedarf erstellt und Sie hätten zum Zeitpunkt des Aufrufs von Funktionen ein vollständig konstruiertes Objekt.


Definieren einer Vorlage in a .cpp Datei, während es in einer anderen verwendet wird .cpp Datei.

Lösung:Fast immer erhalten Sie Fehler wie undefined reference to ....Fügen Sie alle Vorlagendefinitionen in einen Header ein, damit der Compiler bei deren Verwendung bereits den benötigten Code erzeugen kann.


static_cast<Derived*>(base); wenn base ein Zeiger auf eine virtuelle Basisklasse von ist Derived.

Lösung:Eine virtuelle Basisklasse ist eine Basis, die nur einmal vorkommt, auch wenn sie in einem Vererbungsbaum mehr als einmal indirekt von verschiedenen Klassen geerbt wird.Das oben Genannte ist laut Standard nicht zulässig.Verwenden Sie dazu „dynamic_cast“ und stellen Sie sicher, dass Ihre Basisklasse polymorph ist.


dynamic_cast<Derived*>(ptr_to_base); wenn die Basis nicht polymorph ist

Lösung:Der Standard erlaubt kein Downcast eines Zeigers oder einer Referenz, wenn das übergebene Objekt nicht polymorph ist.Es oder eine seiner Basisklassen muss eine virtuelle Funktion haben.


Lassen Sie Ihre Funktion akzeptieren T const **

Lösung:Sie denken vielleicht, das sei sicherer als die Verwendung T **, aber tatsächlich wird es Leuten, die passieren wollen, Kopfschmerzen bereiten T**:Der Standard lässt es nicht zu.Es gibt ein schönes Beispiel dafür, warum es nicht zulässig ist:

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

Akzeptiere immer T const* const*; stattdessen.

Ein weiterer (geschlossener) Fallstrick-Thread zu C++, den Leute, die danach suchen, auch finden werden, ist die Frage zum Stapelüberlauf C++-Fallstricke.

Einige müssen über C++-Bücher verfügen, die Ihnen helfen, häufige C++-Fallstricke zu vermeiden:

Effektives C++
Effektiveres C++
Effektive STL

Das Buch „Effective STL“ erklärt das Problem mit dem Vektor von Bools :)

Brian hat eine tolle Liste:Ich würde hinzufügen: „Markieren Sie Konstruktoren mit einzelnen Argumenten immer als explizit (außer in den seltenen Fällen, in denen Sie eine automatische Umwandlung wünschen).“

Eigentlich kein konkreter Tipp, sondern eine allgemeine Richtlinie:Überprüfen Sie Ihre Quellen.C++ ist eine alte Sprache und hat sich im Laufe der Jahre stark verändert.Die Best Practices haben sich dadurch geändert, aber leider gibt es immer noch viele alte Informationen.Es gab hier einige sehr gute Buchempfehlungen – ich kann jedes C++-Bücher von Scott Meyer als zweites kaufen.Machen Sie sich mit Boost und den in Boost verwendeten Codierungsstilen vertraut – die an diesem Projekt beteiligten Personen sind auf dem neuesten Stand des C++-Designs.

Erfinden Sie das Rad nicht neu.Machen Sie sich mit STL und Boost vertraut und nutzen Sie deren Möglichkeiten, wann immer möglich, um Ihre eigenen zu rollen.Verwenden Sie insbesondere STL-Strings und -Sammlungen, es sei denn, Sie haben einen sehr, sehr guten Grund, dies nicht zu tun.Lernen Sie auto_ptr und die Boost-Smart-Pointer-Bibliothek sehr gut kennen, verstehen Sie, unter welchen Umständen jeder Smart-Pointer-Typ verwendet werden soll, und verwenden Sie dann Smart-Pointer überall dort, wo Sie sonst möglicherweise Rohzeiger verwendet hätten.Ihr Code ist genauso effizient und viel weniger anfällig für Speicherverluste.

Verwenden Sie static_cast, Dynamic_cast, const_cast und reinterpret_cast anstelle von Casts im C-Stil.Im Gegensatz zu Besetzungen im C-Stil werden Sie darüber informiert, ob Sie wirklich nach einer anderen Art von Besetzung fragen, als Sie denken.Und sie stechen optisch hervor und machen den Leser darauf aufmerksam, dass eine Besetzung stattfindet.

Die Webseite C++-Fallstricke von Scott Wheeler behandelt einige der wichtigsten C++-Fallstricke.

Zwei Fallstricke, von denen ich wünschte, ich hätte sie nicht auf die harte Tour gelernt:

(1) Viele Ausgaben (z. B. printf) werden standardmäßig gepuffert.Wenn Sie abstürzenden Code debuggen und gepufferte Debug-Anweisungen verwenden, wird möglicherweise die letzte Ausgabe angezeigt nicht wirklich die letzte print-Anweisung sein, die im Code vorkommt.Die Lösung besteht darin, den Puffer nach jedem Debug-Druck zu leeren (oder die Pufferung ganz auszuschalten).

(2) Seien Sie vorsichtig bei Initialisierungen – (a) vermeiden Sie Klasseninstanzen als globale/statische Instanzen;und (b) versuchen Sie, alle Ihre Mitgliedsvariablen auf einen sicheren Wert in einem Ctor zu initialisieren, auch wenn es sich um einen trivialen Wert wie NULL für Zeiger handelt.

Argumentation:Die Reihenfolge der globalen Objektinitialisierung ist nicht garantiert (globale Variablen enthalten statische Variablen). Daher erhalten Sie möglicherweise Code, der scheinbar nicht deterministisch fehlschlägt, da er davon abhängt, dass Objekt X vor Objekt Y initialisiert wird.Wenn Sie eine Variable vom primitiven Typ, etwa eine Member-Bool oder eine Enumeration einer Klasse, nicht explizit initialisieren, erhalten Sie in überraschenden Situationen unterschiedliche Werte – wiederum kann das Verhalten sehr nichtdeterministisch erscheinen.

Ich habe es schon ein paar Mal erwähnt, aber die Bücher von Scott Meyers Effektives C++ Und Effektive STL sind wirklich Gold wert, wenn sie mit C++ helfen.

Wenn ich darüber nachdenke, Steven Dewhursts C++-Fallstricke ist auch eine ausgezeichnete Ressource „aus den Schützengräben“.Sein Artikel über das Rollen eigener Ausnahmen und deren Aufbau hat mir bei einem Projekt wirklich geholfen.

Verwendung von C++ wie C.Einen Erstellungs- und Veröffentlichungszyklus im Code haben.

In C++ ist dies nicht ausnahmesicher und daher kann die Freigabe nicht ausgeführt werden.In C++ verwenden wir RAII um dieses Problem zu lösen.

Alle Ressourcen, die manuell erstellt und freigegeben werden müssen, sollten in ein Objekt eingeschlossen werden, damit diese Aktionen im Konstruktor/Destruktor ausgeführt werden.

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

    // Use the plop

    releaseMyPlopResource(plop);
}

In C++ sollte dies in ein Objekt eingeschlossen werden:

// 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.
}

Das Buch C++-Fallstricke kann sich als nützlich erweisen.

Hier sind ein paar Fallgruben, in die ich unglücklicherweise fallen musste.Das alles hat gute Gründe, die ich erst verstand, nachdem ich von einem Verhalten gebissen wurde, das mich überraschte.

  • virtual Funktionen in Konstruktoren sind nicht.

  • Verstoßen Sie nicht gegen die ODR (One Definition Rule), dafür sind (unter anderem) anonyme Namespaces da.

  • Die Reihenfolge der Initialisierung der Mitglieder hängt von der Reihenfolge ab, in der sie deklariert werden.

    class bar {
        vector<int> vec_;
        unsigned size_; // Note size_ declared *after* vec_
    public:
        bar(unsigned size)
            : size_(size)
            , vec_(size_) // size_ is uninitialized
            {}
    };
    
  • Standardwerte und virtual haben unterschiedliche Semantik.

    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`
    

Die größte Gefahr für angehende Entwickler besteht darin, eine Verwechslung zwischen C und C++ zu vermeiden.C++ sollte niemals einfach nur als besseres C oder C mit Klassen betrachtet werden, da dies seine Leistung einschränkt und es sogar gefährlich machen kann (insbesondere, wenn Speicher wie in C verwendet wird).

Kasse boost.org.Es bietet viele zusätzliche Funktionen, insbesondere ihre Smart-Pointer-Implementierungen.

PRQA haben ein ausgezeichneter und kostenloser C++-Codierungsstandard Basierend auf Büchern von Scott Meyers, Bjarne Stroustrop und Herb Sutter.Es fasst alle diese Informationen in einem Dokument zusammen.

  1. Ich lese das nicht C++ FAQ Lite.Es erklärt viele schlechte (und gute!) Praktiken.
  2. Wird nicht verwendet Schub.Sie ersparen sich viel Frust, wenn Sie Boost nach Möglichkeit nutzen.

Seien Sie vorsichtig, wenn Sie intelligente Zeiger und Containerklassen verwenden.

Vermeiden Pseudoklassen und Quasiklassen...Überdesign im Grunde.

Vergessen, einen virtuellen Basisklassendestruktor zu definieren.Das bedeutet, dass man anruft delete on a Base* wird den abgeleiteten Teil nicht zerstören.

Halten Sie die Namensräume klar (einschließlich Struktur, Klasse, Namespace und Using).Das ist meine größte Frustration, wenn das Programm einfach nicht kompiliert werden kann.

Um es zu vermasseln, verwenden Sie häufig gerade Zeiger.Verwenden Sie stattdessen RAII für fast alles und achten Sie dabei natürlich darauf, dass Sie die richtigen Smart Pointer verwenden.Wenn Sie „delete“ irgendwo außerhalb einer Handle- oder Zeigerklasse schreiben, machen Sie es höchstwahrscheinlich falsch.

  • Blizpasta.Das ist ein riesiges Ding, das ich oft sehe ...

  • Nicht initialisierte Variablen sind ein großer Fehler, den meine Schüler machen.Viele Java-Leute vergessen, dass das bloße Sagen von „int counter“ den Zähler nicht auf 0 setzt.Da Sie Variablen in der h-Datei definieren (und im Konstruktor/Setup eines Objekts initialisieren) müssen, vergisst man das leicht.

  • Off-by-one-Fehler an for Schleifen / Array-Zugriff.

  • Objektcode wird beim Start von Voodoo nicht ordnungsgemäß gereinigt.

  • static_cast auf eine virtuelle Basisklasse niedergeschlagen

Nicht wirklich...Nun zu meinem Missverständnis:Ich dachte, dass A im Folgenden handelte es sich um eine virtuelle Basisklasse, obwohl dies in Wirklichkeit nicht der Fall ist;es handelt sich laut 10.3.1 um a polymorphe Klasse.Benutzen static_cast hier scheint alles in Ordnung zu sein.

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

struct D : B { };

Zusammenfassend lässt sich sagen, dass dies eine gefährliche Falle ist.

Überprüfen Sie immer einen Zeiger, bevor Sie ihn dereferenzieren.In C kann man normalerweise an der Stelle, an der man einen fehlerhaften Zeiger dereferenziert, mit einem Absturz rechnen;In C++ können Sie eine ungültige Referenz erstellen, die an einer Stelle abstürzt, die weit von der Ursache des Problems entfernt ist.

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
}

Vergessen ein & und dadurch eine Kopie statt einer Referenz erstellen.

Das ist mir zweimal auf unterschiedliche Weise passiert:

  • Eine Instanz befand sich in einer Argumentliste, was dazu führte, dass ein großes Objekt auf den Stapel gelegt wurde, was zu einem Stapelüberlauf und einem Absturz des eingebetteten Systems führte.

  • Ich habe das vergessen & auf eine Instanzvariable, mit der Wirkung, dass das Objekt kopiert wurde.Nachdem ich mich als Zuhörer für die Kopie registriert hatte, fragte ich mich, warum ich nie die Rückrufe vom Originalobjekt erhielt.

Beides war eher schwer zu erkennen, da der Unterschied gering und schwer zu erkennen ist und Objekte und Referenzen ansonsten syntaktisch auf die gleiche Weise verwendet werden.

Absicht ist (x == 10):

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

Ich dachte, ich würde diesen Fehler nie selbst machen, aber ich habe ihn kürzlich tatsächlich gemacht.

Der Aufsatz/Artikel Zeiger, Referenzen und Werte ist sehr nützlich.Es geht darum, Fallstricke zu vermeiden und bewährte Vorgehensweisen anzuwenden.Sie können auch die gesamte Website durchsuchen, die Programmiertipps, hauptsächlich für C++, enthält.

Ich habe viele Jahre mit der C++-Entwicklung verbracht.Ich habe ein geschrieben kurze Zusammenfassung von Problemen, die ich vor Jahren damit hatte.Standardkonforme Compiler stellen kein wirkliches Problem mehr dar, aber ich vermute, dass die anderen beschriebenen Fallstricke immer noch bestehen.

#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);
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top