Ich finde intelligente Zeiger viel komfortabler als rohe Zeiger. Ist es also eine gute Idee, immer intelligente Zeiger zu verwenden? (Bitte beachten Sie, dass ich aus Java stamme und daher die Idee der expliziten Speicherverwaltung nicht besonders mag. Wenn es also keine ernsthaften Leistungsprobleme mit intelligenten Zeigern gibt, möchte ich mich an diese halten.)
Hinweis: Obwohl ich aus Java stamme, verstehe ich die Implementierung von intelligenten Zeigern und die Konzepte von RAII recht gut. Sie können dieses Wissen also von meiner Seite als selbstverständlich betrachten, wenn Sie eine Antwort veröffentlichen. Ich verwende fast überall statische Zuordnung und verwende Zeiger nur bei Bedarf. Meine Frage ist nur: Kann ich immer intelligente Zeiger anstelle von rohen Zeigern verwenden?
Antworten:
Angesichts der verschiedenen Änderungen habe ich den Eindruck, dass eine umfassende Zusammenfassung nützlich wäre.
1. Wenn nicht
Es gibt zwei Situationen, in denen Sie keine intelligenten Zeiger verwenden sollten.
Das erste ist genau die gleiche Situation, in der Sie a nicht verwenden sollten
C++
tatsächlich Klasse verwenden . IE: DLL-Grenze, wenn Sie dem Client den Quellcode nicht anbieten. Sagen wir anekdotisch.Das zweite passiert viel häufiger: Smart Manager bedeutet Eigentum . Sie können Zeiger verwenden, um auf vorhandene Ressourcen zu verweisen, ohne deren Lebensdauer zu verwalten. Beispiel:
void notowner(const std::string& name) { Class* pointer(0); if (name == "cat") pointer = getCat(); else if (name == "dog") pointer = getDog(); if (pointer) doSomething(*pointer); }
Dieses Beispiel ist eingeschränkt. Ein Zeiger unterscheidet sich jedoch semantisch von einer Referenz darin, dass er auf eine ungültige Position (den Nullzeiger) verweisen kann. In diesem Fall ist es vollkommen in Ordnung, stattdessen keinen intelligenten Zeiger zu verwenden, da Sie die Lebensdauer des Objekts nicht verwalten möchten.
2. Intelligente Manager
Wenn Sie keine Smart Manager-Klasse schreiben und das Schlüsselwort verwenden, machen
delete
Sie etwas falsch.Es ist eine kontroverse Sichtweise, aber nachdem ich so viele Beispiele für fehlerhaften Code überprüft habe, gehe ich kein Risiko mehr ein. Wenn Sie also schreiben
new
, benötigen Sie einen intelligenten Manager für den neu zugewiesenen Speicher. Und du brauchst es jetzt.Das bedeutet nicht, dass Sie weniger Programmierer sind! Im Gegenteil, die Wiederverwendung von Code, der nachweislich funktioniert, anstatt das Rad immer wieder neu zu erfinden, ist eine Schlüsselkompetenz.
Nun beginnt die eigentliche Schwierigkeit: Welcher Smart Manager?
3. Intelligente Zeiger
Es gibt verschiedene intelligente Zeiger mit verschiedenen Eigenschaften.
Überspringen,
std::auto_ptr
das Sie generell vermeiden sollten (die Kopiersemantik ist verschraubt).scoped_ptr
: kein Overhead, kann nicht kopiert oder verschoben werden.unique_ptr
: kein Overhead, kann nicht kopiert werden, kann verschoben werden.shared_ptr
/weak_ptr
: Ein Teil des Overheads (Referenzzählung) kann kopiert werden.Versuchen Sie normalerweise, entweder
scoped_ptr
oder zu verwendenunique_ptr
. Wenn Sie mehrere Eigentümer benötigen, versuchen Sie, das Design zu ändern. Wenn Sie das Design nicht ändern können und wirklich mehrere Eigentümer benötigen, verwenden Sie ashared_ptr
, aber achten Sie auf Referenzzyklen, die mit a unterbrochen werden solltenweak_ptr
irgendwo in der Mitte .4. Intelligente Container
Viele intelligente Zeiger sind nicht zum Kopieren gedacht, daher ist ihre Verwendung mit den STL-Containern etwas beeinträchtigt.
shared_ptr
Verwenden Sie intelligente Container aus dem Boost Pointer Container, anstatt auf und deren Overhead zurückzugreifen . Sie emulieren die Schnittstelle klassischer STL-Container, speichern jedoch Zeiger, die sie besitzen.5. Rollen Sie Ihre eigenen
Es gibt Situationen, in denen Sie möglicherweise Ihren eigenen Smart Manager rollen möchten. Stellen Sie sicher, dass Sie nicht nur einige Funktionen in den Bibliotheken verpasst haben, die Sie zuvor verwendet haben.
Das Schreiben eines intelligenten Managers bei Ausnahmen ist ziemlich schwierig. Normalerweise können Sie nicht davon ausgehen, dass Speicher verfügbar ist (
new
möglicherweise fehlschlägt) oder dassCopy Constructor
dieno throw
Garantie besteht.Es kann einigermaßen akzeptabel sein, die
std::bad_alloc
Ausnahme zu ignorieren und aufzuerlegen, dassCopy Constructor
s einer Reihe von Helfern nicht fehlschlagen ... schließlich istboost::shared_ptr
dies der Grund für denD
Parameter zum Löschen von Vorlagen.Aber ich würde es nicht empfehlen, besonders für Anfänger. Es ist ein heikles Problem, und Sie werden die Fehler im Moment wahrscheinlich nicht bemerken.
6. Beispiele
// For the sake of short code, avoid in real code ;) using namespace boost; // Example classes // Yes, clone returns a raw pointer... // it puts the burden on the caller as for how to wrap it // It is to obey the `Cloneable` concept as described in // the Boost Pointer Container library linked above struct Cloneable { virtual ~Cloneable() {} virtual Cloneable* clone() const = 0; }; struct Derived: Cloneable { virtual Derived* clone() const { new Derived(*this); } }; void scoped() { scoped_ptr<Cloneable> c(new Derived); } // memory freed here // illustration of the moved semantics unique_ptr<Cloneable> unique() { return unique_ptr<Cloneable>(new Derived); } void shared() { shared_ptr<Cloneable> n1(new Derived); weak_ptr<Cloneable> w = n1; { shared_ptr<Cloneable> n2 = n1; // copy n1.reset(); assert(n1.get() == 0); assert(n2.get() != 0); assert(!w.expired() && w.get() != 0); } // n2 goes out of scope, the memory is released assert(w.expired()); // no object any longer } void container() { ptr_vector<Cloneable> vec; vec.push_back(new Derived); vec.push_back(new Derived); vec.push_back( vec.front().clone() // Interesting semantic, it is dereferenced! ); } // when vec goes out of scope, it clears up everything ;)
quelle
std::auto_ptr
unterscheidet sich von dem, was die meisten Leute erwarten, aber sie ist sinnvoll und führt dazu, dass Designprobleme im Code vermieden werden. Die Aussage "generell vermeiden" ist unsinnig.auto_ptr
in C ++ 0x veralten und stattdessen die Verwendung von empfehlenunique_ptr
;-). Um fair zu sein, verlassen Sieunique_ptr
sich auf die Erstellung / Zuweisung von Umzügen als Ersatz fürauto_ptr
. Wenn Sie strikt übertragbares Eigentum in C ++ 03 wünschen, haben Sie nicht viel Auswahl.Intelligente Zeiger führen eine explizite Speicherverwaltung durch. Wenn Sie nicht verstehen, wie sie ausgeführt werden, treten beim Programmieren mit C ++ große Probleme auf. Und denken Sie daran, dass Speicher nicht die einzige Ressource ist, die sie verwalten.
Um Ihre Frage zu beantworten, sollten Sie Smart-Pointer als erste Annäherung an eine Lösung bevorzugen, aber möglicherweise bereit sein, sie bei Bedarf fallen zu lassen. Sie sollten niemals Zeiger (oder irgendeine Art) oder dynamische Zuordnung verwenden, wenn dies vermieden werden kann. Zum Beispiel:
string * s1 = new string( "foo" ); // bad string s2( "bar" ); // good
Bearbeiten: Um Ihre ergänzende Frage zu beantworten : "Kann ich immer intelligente Zeiger anstelle von Rohzeigern verwenden? Nein, können Sie nicht. Wenn Sie (zum Beispiel) Ihre eigene Version des Operators new implementieren müssen, müssen Sie dies tun." Lassen Sie es einen Zeiger zurückgeben, keinen intelligenten Zeiger.
quelle
Normalerweise sollten Sie keine Zeiger (intelligent oder anderweitig) verwenden, wenn Sie sie nicht benötigen. Machen Sie lokale Variablen, Klassenmitglieder, Vektorelemente und ähnliche Elemente besser zu normalen Objekten als zu Zeigern auf Objekte. (Da Sie aus Java kommen, sind Sie wahrscheinlich versucht, alles zuzuweisen
new
, was nicht empfohlen wird.)Dieser Ansatz (" RAII ") erspart Ihnen die meiste Zeit die Sorge um Zeiger.
Wenn Sie Zeiger verwenden müssen, hängt dies von der Situation ab und warum genau Sie Zeiger benötigen. In der Regel können jedoch intelligente Zeiger verwendet werden. Es ist möglicherweise nicht immer (in Fettdruck) die beste Option, dies hängt jedoch von der jeweiligen Situation ab.
quelle
boost::scoped_ptr
könnten dann noch verwendet werden, aber vielleicht möchten Sie dann keine Abhängigkeit vom Boost haben?) - oder Sie müssen eine Schnittstelle mit einer C-API herstellen In diesem Fall sind Rohzeiger konsistenter. Wenn Sie über ein Array iterieren müssen, sind Ihre Iteratoren wahrscheinlich auch rohe Zeiger.auto_ptr
,unique_ptr
odershared_ptr
wie Sie einen Rohzeiger aus Ihrem Umfang Eigentumsübertragung passieren würde.scoped_ptr
ist der einzige intelligente Zeiger des Satzes, der nicht in der Lage sein wird, das Eigentum zu übertragen / zu teilenEine gute Zeit nicht intelligenten Zeiger zu verwenden, liegt an der Schnittstellengrenze einer DLL. Sie wissen nicht, ob andere ausführbare Dateien mit demselben Compiler / denselben Bibliotheken erstellt werden. Die DLL-Aufrufkonvention Ihres Systems gibt nicht an, wie Standard- oder TR1-Klassen aussehen, einschließlich intelligenter Zeiger.
Wenn Sie in einer ausführbaren Datei oder Bibliothek den Besitz des Pointees darstellen möchten, sind intelligente Zeiger im Durchschnitt der beste Weg, dies zu tun. Es ist also in Ordnung, sie immer lieber als roh verwenden zu wollen. Ob Sie sie tatsächlich immer verwenden können, ist eine andere Frage.
Ein konkretes Beispiel, wenn Sie nicht annehmen, dass Sie eine Darstellung eines generischen Diagramms schreiben, wobei Scheitelpunkte durch Objekte und Kanten durch Zeiger zwischen den Objekten dargestellt werden. Die üblichen intelligenten Zeiger helfen Ihnen nicht weiter: Diagramme können zyklisch sein, und kein bestimmter Knoten kann für die Speicherverwaltung anderer Knoten verantwortlich gemacht werden, sodass gemeinsam genutzte und schwache Zeiger nicht ausreichen. Sie können beispielsweise alles in einen Vektor einfügen und Indizes anstelle von Zeigern verwenden oder alles in eine Deque einfügen und rohe Zeiger verwenden. Sie können verwenden,
shared_ptr
wenn Sie möchten, aber es wird nichts außer Overhead hinzugefügt. Oder Sie suchen nach Mark-Sweep-GC.Ein Randfall: Ich bevorzuge es, wenn Funktionen einen Parameter als Zeiger oder Referenz verwenden und versprechen, keinen Zeiger oder Verweis darauf beizubehalten , anstatt einen zu nehmen,
shared_ptr
und Sie fragen sich, ob sie nach ihrer Rückkehr möglicherweise eine Referenz behalten, wenn Wenn Sie den Verweis immer wieder ändern, brechen Sie etwas usw. Wenn Verweise nicht beibehalten werden, wird dies häufig nicht explizit dokumentiert. Es versteht sich von selbst. Vielleicht sollte es nicht, aber es tut es. Intelligente Zeiger implizieren etwas über das Eigentum, und fälschlicherweise impliziert dies, dass dies verwirrend sein kann. Wenn Ihre Funktion also a übernimmtshared_ptr
, müssen Sie unbedingt dokumentieren, ob eine Referenz beibehalten werden kann oder nicht.quelle
In vielen Situationen glaube ich, dass sie definitiv der richtige Weg sind (weniger unordentlicher Bereinigungscode, geringeres Risiko von Lecks usw.). Es gibt jedoch einige sehr geringe zusätzliche Kosten. Wenn ich einen Code schreiben würde, der so schnell wie möglich sein muss (z. B. eine enge Schleife, die eine gewisse Zuweisung und eine freie Schleife durchführen muss), würde ich wahrscheinlich keinen intelligenten Zeiger verwenden, um etwas mehr Geschwindigkeit zu erreichen. Ich bezweifle jedoch, dass dies in den meisten Situationen einen messbaren Unterschied bewirken würde.
quelle
scoped_ptr
)shared_ptr
Sie müssen sowohl bei der Zuweisung des gemeinsam genutzten Informationsblocks als auch bei der Freigabe anhand dieser gemeinsam genutzten Informationen einen Overhead haben. Bei intelligenten Zeigern mit Einzelbesitz muss der Destruktor jedoch keinen Test durchführen: Löschen Sie einfach den internen Zeiger. Beispiele: libstdc ++ :~auto_ptr() { delete _M_ptr; }
, boost 1.37:~scoped_ptr() { checked_delete(ptr); }
Wochecked_delete
ist eine Überprüfung der Kompilierungszeitdelete
auf Typvollständigkeit und ein einzelner Aufruf von , der höchstwahrscheinlich inline sein wird.Im Allgemeinen können Sie nicht immer intelligente Zeiger verwenden. Wenn Sie beispielsweise andere Frameworks verwenden, die keinen intelligenten Zeiger verwenden (wie Qt), müssen Sie auch Rohzeiger verwenden.
quelle
Wenn Sie mit einer Ressource umgehen, sollten Sie immer RAII-Techniken verwenden. Bei Speicher bedeutet dies, dass Sie die eine oder andere Form eines Smart-Zeigers verwenden (Hinweis: Smart
shared_ptr
, wählen Sie nicht den Smart-Zeiger, der für Ihren speziellen Anwendungsfall am besten geeignet ist ). Nur so können Lecks bei Ausnahmen vermieden werden.Es gibt immer noch Fälle, in denen Rohzeiger erforderlich sind, wenn die Ressourcenverwaltung nicht über den Zeiger erfolgt. Insbesondere sind sie die einzige Möglichkeit, eine rücksetzbare Referenz zu haben. Stellen Sie sich vor, Sie behalten eine Referenz in einem Objekt, dessen Lebensdauer nicht explizit behandelt werden kann (Mitgliedsattribut, Objekt im Stapel). Aber das ist ein sehr spezifischer Fall, den ich nur einmal in echtem Code gesehen habe. In den meisten Fällen ist die Verwendung von a
shared_ptr
ein besserer Ansatz für die Freigabe eines Objekts.quelle
Meine Einstellung zu intelligenten Zeigern: GROSS, wenn es schwierig ist zu wissen, wann eine Freigabe erfolgen kann (z. B. in einem Try / Catch-Block oder in einer Funktion, die eine Funktion (oder sogar einen Konstruktor!) Aufruft, die Sie aus Ihrer aktuellen Funktion herauswerfen könnte). oder Hinzufügen einer besseren Speicherverwaltung zu einer Funktion, die überall im Code zurückgegeben wird. Oder Zeiger in Container stecken.
Intelligente Zeiger verursachen jedoch Kosten, die Sie möglicherweise nicht für Ihr gesamtes Programm bezahlen möchten. Wenn die Speicherverwaltung einfach von Hand durchzuführen ist ("Hmm, ich weiß, dass ich diese drei Zeiger löschen muss, wenn diese Funktion endet, und ich weiß, dass diese Funktion vollständig ausgeführt wird"), warum sollten Sie dann die Zyklen verschwenden, die der Computer ausführen muss? es?
quelle
auto_ptr
ist, oderscoped_ptr
. Es ist selten, dass sie messbaren Overhead verursachen, und in der Zwischenzeit erleichtern sie es, den richtigen Code zu finden. Wenn beispielsweise das Erfassen des zweiten dieser drei Zeiger eine Ausnahme auslöst, geben Sie den ersten frei? Wie viel Code müssen Sie dafür schreiben, verglichen mit der Verwendung intelligenter Zeiger? Wie oft erwerben Sie wirklich Ressourcen, die Sie freigeben müssen, bei denen Ihre Akquisition jedoch nicht fehlschlagen kann?Ja, aber ich habe mehrere Projekte ohne die Verwendung eines intelligenten Zeigers oder Zeiger durchgeführt. Es ist empfehlenswert, Container wie Deque, List, Map usw. zu verwenden. Alternativ verwende ich Referenzen, wenn möglich. Anstatt einen Zeiger zu übergeben, übergebe ich eine Referenz oder eine konstante Referenz und es ist fast immer unlogisch, eine Referenz zu löschen / freizugeben, damit ich dort nie Probleme habe (normalerweise erstelle ich sie auf dem Stapel durch Schreiben
{ Class class; func(class, ref2, ref3); }
quelle
Es ist. Smart Pointer ist einer der Eckpfeiler des alten Cocoa (Touch) -Ökosystems. Ich glaube, es wirkt sich immer wieder auf das Neue aus.
quelle