Warum nicht Zeiger für alles in C ++ verwenden?


75

Angenommen, ich definiere eine Klasse:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

Dann schreiben Sie einen Code damit. Warum sollte ich Folgendes tun?

Pixel p;
p.x = 2;
p.y = 5;

Aus einer Java-Welt kommend schreibe ich immer:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

Sie machen im Grunde das Gleiche, oder? Einer befindet sich auf dem Stapel, während der andere auf dem Heap liegt, sodass ich ihn später löschen muss. Gibt es einen grundlegenden Unterschied zwischen den beiden? Warum sollte ich eins dem anderen vorziehen?

Antworten:


188

Ja, einer liegt auf dem Stapel, der andere auf dem Haufen. Es gibt zwei wichtige Unterschiede:

  • Erstens die offensichtliche und weniger wichtige: Die Heap-Zuweisungen sind langsam. Stapelzuordnungen sind schnell.
  • Zweitens und viel wichtiger ist RAII . Da die vom Stapel zugewiesene Version automatisch bereinigt wird, ist dies hilfreich . Der Destruktor wird automatisch aufgerufen, sodass Sie sicherstellen können, dass alle von der Klasse zugewiesenen Ressourcen bereinigt werden. Dies ist wichtig, um Speicherverluste in C ++ zu vermeiden. Sie vermeiden sie, indem Sie sich niemals deleteselbst aufrufen , sondern sie in deletestapelzugewiesene Objekte einschließen, die intern aufgerufen werden, wie dies in ihrem Destruktor typisch ist. Wenn Sie versuchen, alle Zuordnungen manuell zu verfolgen und deletezum richtigen Zeitpunkt anzurufen , garantiere ich Ihnen, dass pro 100 Codezeilen mindestens ein Speicherverlust auftritt.

Betrachten Sie als kleines Beispiel diesen Code:

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

Ziemlich unschuldiger Code, oder? Wir erstellen ein Pixel, rufen dann eine nicht verwandte Funktion auf und löschen das Pixel. Gibt es ein Speicherleck?

Und die Antwort ist "möglicherweise". Was passiert, wenn bareine Ausnahme ausgelöst wird? deletewird nie aufgerufen, das Pixel wird nie gelöscht und wir verlieren Speicherplatz. Betrachten Sie nun Folgendes:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

Dadurch wird kein Speicher verloren. In diesem einfachen Fall befindet sich natürlich alles auf dem Stapel, sodass es automatisch bereinigt wird, aber selbst wenn die PixelKlasse intern eine dynamische Zuordnung vorgenommen hätte, würde dies auch nicht auslaufen. Die PixelKlasse würde einfach einen Destruktor erhalten, der sie löscht, und dieser Destruktor würde aufgerufen, egal wie wir die fooFunktion verlassen . Auch wenn wir es verlassen, weil bareine Ausnahme geworfen hat. Das folgende, leicht erfundene Beispiel zeigt dies:

class Pixel {
public:
  Pixel(){ x=new int(0); y=new int(0);};
  int* x;
  int* y;

  ~Pixel() {
    delete x;
    delete y;
  }
};

void foo() {
  Pixel p;
  *p.x = 2;
  *p.y = 5;

  bar();
}

Die Pixel-Klasse weist jetzt intern etwas Heap-Speicher zu, aber ihr Destruktor kümmert sich um die Bereinigung, sodass wir uns bei der Verwendung der Klasse keine Sorgen machen müssen. (Ich sollte wahrscheinlich erwähnen, dass das letzte Beispiel hier stark vereinfacht ist, um das allgemeine Prinzip zu zeigen. Wenn wir diese Klasse tatsächlich verwenden, enthält sie auch mehrere mögliche Fehler. Wenn die Zuordnung von y fehlschlägt, wird x nie freigegeben Wenn das Pixel kopiert wird, versuchen beide Instanzen, dieselben Daten zu löschen. Nehmen Sie also das letzte Beispiel hier mit einem Körnchen Salz. Realer Code ist etwas kniffliger, zeigt aber die allgemeine Idee.

Natürlich kann dieselbe Technik auf andere Ressourcen als Speicherzuweisungen erweitert werden. Beispielsweise kann damit sichergestellt werden, dass Dateien oder Datenbankverbindungen nach der Verwendung geschlossen werden oder dass Synchronisierungssperren für Ihren Threading-Code freigegeben werden.


5
+1. Obwohl 1Leak / 100loc zu viel ist. Vielleicht 1 pro 1000 Codezeilen.
Milan Babuškov

10
@Milan: Angesichts von Ausnahmen würde ich sagen, 100 ist wahrscheinlich näher als 1000.
mghie

5
Ja, Sie werden wahrscheinlich in der Lage sein, die ersten 500 Zeilen ohne Lecks zu schreiben. Und dann fügen Sie weitere 100 Zeilen hinzu, die 6 verschiedene Möglichkeiten enthalten, um dieselben Daten in derselben Funktion zu verlieren. Natürlich habe ich das nicht gemessen, aber es klang gut. :)
Jalf

3
@ Matt: Oh, wirklich? Sie müssen sich keine Gedanken über die Speicherverwaltung machen, wenn Sie keine Ausnahmen verwenden? Das sind Neuigkeiten für mich. Ich stelle mir vor, eine große Anzahl von C-Programmierern wünschte, sie hätten das auch gewusst. Ich glaube, viele große Softwareprojekte, die in C geschrieben wurden, könnten erheblich vereinfacht werden, wenn sie nur dieses kleine Nugget der Weisheit gekannt hätten: Solange es keine Ausnahmen gibt, ist es nicht notwendig, Ihr Gedächtnis zu verwalten.
Jalf

3
@ Matt: Ich bin nicht. Ich interpretiere sie absichtlich. Es gibt kein "Mis". Wenn Sie sich die Kommentare ansehen, die Sie zu all meinen Antworten hinterlassen, ist ziemlich klar, wie viel sie wert sind. Jedenfalls sehe ich in meinem Beitrag keine "obsessive Boilerplate". Ich sehe auch nichts, was vor Funktionen schützen soll. Ich sehe eine sehr einfache Redewendung, die verwendet wird, um sehr einfachen Code zu schreiben, der sehr einfach zu verwenden ist. Ohne sie würde der Client-Code komplexer und fragiler werden, und die Implementierung der Klasse selbst würde möglicherweise einige Codezeilen einsparen.
Jalf

30

Sie sind erst identisch, wenn Sie das Löschen hinzufügen.
Ihr Beispiel ist zu trivial, aber der Destruktor enthält möglicherweise tatsächlich Code, der echte Arbeit leistet. Dies wird als RAII bezeichnet.

Fügen Sie also das Löschen hinzu. Stellen Sie sicher, dass dies auch dann geschieht, wenn sich Ausnahmen verbreiten.

Pixel* p = NULL; // Must do this. Otherwise new may throw and then
                 // you would be attempting to delete an invalid pointer.
try
{
    p = new Pixel(); 
    p->x = 2;
    p->y = 5;

    // Do Work
    delete p;
}
catch(...)
{
    delete p;
    throw;
}

Wenn Sie etwas Interessanteres wie eine Datei ausgewählt haben (eine Ressource, die geschlossen werden muss). Dann machen Sie es richtig in Java mit Zeigern, die Sie dazu brauchen.

File file;
try
{
    file = new File("Plop");
    // Do work with file.
}
finally
{
    try
    {
        file.close();     // Make sure the file handle is closed.
                          // Oherwise the resource will be leaked until
                          // eventual Garbage collection.
    }
    catch(Exception e) {};// Need the extra try catch to catch and discard
                          // Irrelevant exceptions. 

    // Note it is bad practice to allow exceptions to escape a finally block.
    // If they do and there is already an exception propagating you loose the
    // the original exception, which probably has more relevant information
    // about the problem.
}

Der gleiche Code in C ++

std::fstream  file("Plop");
// Do work with file.

// Destructor automatically closes file and discards irrelevant exceptions.

Obwohl die Leute die Geschwindigkeit erwähnen (wegen des Findens / Zuweisens von Speicher auf dem Heap). Persönlich ist dies für mich kein entscheidender Faktor (die Allokatoren sind sehr schnell und wurden für die C ++ - Verwendung kleiner Objekte optimiert, die ständig erstellt / zerstört werden).

Der Hauptgrund für mich ist die Lebensdauer des Objekts. Ein lokal definiertes Objekt hat eine sehr spezifische und genau definierte Lebensdauer, und der Destruktor wird garantiert am Ende aufgerufen (und kann daher spezifische Nebenwirkungen haben). Ein Zeiger hingegen steuert eine Ressource mit einer dynamischen Lebensdauer.

Der Hauptunterschied zwischen C ++ und Java ist:

Das Konzept, wem der Zeiger gehört. Es liegt in der Verantwortung des Eigentümers, das Objekt zum richtigen Zeitpunkt zu löschen. Aus diesem Grunde ist man sehr selten sieht rohe Zeiger wie in realen Programmen (da es keine Eigentümerinformationen ist mit einem zugehörigen rohen Zeiger). Stattdessen werden Zeiger normalerweise in intelligente Zeiger eingeschlossen. Der Smart Pointer definiert die Semantik, wem der Speicher gehört und wer für die Bereinigung verantwortlich ist.

Beispiele sind:

 std::auto_ptr<Pixel>   p(new Pixel);
 // An auto_ptr has move semantics.
 // When you pass an auto_ptr to a method you are saying here take this. You own it.
 // Delete it when you are finished. If the receiver takes ownership it usually saves
 // it in another auto_ptr and the destructor does the actual dirty work of the delete.
 // If the receiver does not take ownership it is usually deleted.

 std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr
 // A shared ptr has shared ownership.
 // This means it can have multiple owners each using the object simultaneously.
 // As each owner finished with it the shared_ptr decrements the ref count and 
 // when it reaches zero the objects is destroyed.

 boost::scoped_ptr<Pixel>  p(new Pixel);
 // Makes it act like a normal stack variable.
 // Ownership is not transferable.

Da sind andere.


9
Ich vergleiche gerne die Verwendung von C ++ - Dateien mit Java (bringt mich zum Lächeln).
Martin York

2
einverstanden. Und Bonuspunkte, weil RAII zur Verwaltung anderer Ressourcentypen als nur der Speicherzuweisungen verwendet wird.
Jalf

25

Logischerweise machen sie dasselbe - außer beim Aufräumen. Nur der von Ihnen geschriebene Beispielcode weist im Zeigerfall einen Speicherverlust auf, da dieser Speicher nicht freigegeben wird.

Aus einem Java-Hintergrund stammend, sind Sie möglicherweise nicht vollständig darauf vorbereitet, wie viel von C ++ sich darauf konzentriert, zu verfolgen, was zugewiesen wurde und wer für die Freigabe verantwortlich ist.

Wenn Sie gegebenenfalls Stapelvariablen verwenden, müssen Sie sich keine Gedanken über die Freigabe dieser Variablen machen, sie verschwindet mit dem Stapelrahmen.

Wenn Sie sehr vorsichtig sind, können Sie natürlich immer manuell auf dem Heap und kostenlos zuweisen. Ein Teil der guten Softwareentwicklung besteht jedoch darin, die Dinge so zu erstellen, dass sie nicht kaputt gehen können, anstatt Ihrem übermenschlichen Programmierer zu vertrauen. fu, um niemals einen Fehler zu machen.


24

Ich bevorzuge die erste Methode, wenn ich die Chance dazu bekomme, weil:

  • es ist schneller
  • Ich muss mir keine Sorgen um die Freigabe des Speichers machen
  • p ist ein gültiges Objekt für den gesamten aktuellen Bereich

14

"Warum nicht Zeiger für alles in C ++ verwenden?"

Eine einfache Antwort - weil es ein großes Problem beim Verwalten des Speichers wird - Zuweisen und Löschen / Freigeben.

Automatische / Stapelobjekte entfernen einen Teil der geschäftigen Arbeit davon.

Das ist nur das erste, was ich zu dieser Frage sagen würde.


11

Eine gute allgemeine Faustregel ist, NIEMALS neue zu verwenden, es sei denn, Sie müssen dies unbedingt tun. Ihre Programme sind einfacher zu warten und weniger fehleranfällig, wenn Sie keine neuen Programme verwenden, da Sie sich keine Gedanken darüber machen müssen, wo Sie sie bereinigen müssen.


11

Der Code:

Pixel p;
p.x = 2;
p.y = 5;

führt keine dynamische Speicherzuweisung durch - es gibt keine Suche nach freiem Speicher, keine Aktualisierung der Speichernutzung, nichts. Es ist völlig kostenlos. Der Compiler reserviert zur Kompilierungszeit Speicherplatz auf dem Stapel für die Variable - er hat viel Speicherplatz zu reservieren und erstellt einen einzelnen Opcode, um den Stapelzeiger um die erforderliche Menge zu bewegen.

Die Verwendung von new erfordert den gesamten Speicherverwaltungsaufwand.

Die Frage lautet dann: Möchten Sie Stapel- oder Heap-Speicherplatz für Ihre Daten verwenden? Stapelvariablen (oder lokale Variablen wie 'p' erfordern keine Dereferenzierung, während die Verwendung von new eine Indirektionsebene hinzufügt.


10

Ja, das macht zunächst Sinn, da es aus Java oder C # stammt. Es scheint keine große Sache zu sein, sich daran zu erinnern, den von Ihnen zugewiesenen Speicher freizugeben. Aber wenn Sie dann Ihr erstes Gedächtnisleck bekommen, kratzen Sie sich am Kopf, weil Sie schwören, dass Sie alles befreit haben. Dann, wenn es das zweite Mal passiert und das dritte Mal, werden Sie noch frustrierter. Nach sechs Monaten Kopfschmerzen aufgrund von Speicherproblemen werden Sie es langsam satt und der vom Stapel zugewiesene Speicher wird immer attraktiver. Wie schön und sauber - legen Sie es einfach auf den Stapel und vergessen Sie es. Ziemlich bald werden Sie den Stapel verwenden, wenn Sie damit durchkommen können.

Aber - es gibt keinen Ersatz für diese Erfahrung. Mein Rat? Probieren Sie es jetzt aus. Du wirst sehen.


6
Sie haben vergessen, seinen bösen Zwilling zu erwähnen, der doppelt frei ist. :) Nur wenn Sie glauben, dass Sie Ihren gesamten Speicher freigegeben haben, treten Fehler auf, weil Sie Speicher verwenden, nachdem er freigegeben wurde, oder wenn Sie versuchen, bereits freigegebenen Speicher freizugeben.
Jalf

6

Meine Bauchreaktion besteht nur darin, Ihnen zu sagen, dass dies zu ernsthaften Speicherlecks führen kann. Einige Situationen, in denen Sie möglicherweise Zeiger verwenden, können zu Verwirrung darüber führen, wer für das Löschen dieser Zeiger verantwortlich sein sollte. In einfachen Fällen wie Ihrem Beispiel ist es leicht zu erkennen, wann und wo Sie delete aufrufen sollten. Wenn Sie jedoch Zeiger zwischen Klassen übergeben, kann es etwas schwieriger werden.

Ich würde empfehlen, in der Boost Smart Pointers-Bibliothek nach Ihren Zeigern zu suchen .


6

Der beste Grund, nicht alles neu zu machen, ist, dass Sie sehr deterministisch bereinigen können, wenn sich Dinge auf dem Stapel befinden. Bei Pixeln ist dies nicht so offensichtlich, aber bei einer Datei wird dies vorteilhaft:

  {   // block of code that uses file
      File aFile("file.txt");
      ...
  }    // File destructor fires when file goes out of scope, closing the file
  aFile // can't access outside of scope (compiler error)

Wenn Sie eine Datei neu erstellen, müssen Sie daran denken, sie zu löschen, um das gleiche Verhalten zu erzielen. Scheint im obigen Fall ein einfaches Problem zu sein. Betrachten Sie jedoch komplexeren Code, z. B. das Speichern der Zeiger in einer Datenstruktur. Was ist, wenn Sie diese Datenstruktur an einen anderen Code übergeben? Wer ist für die Bereinigung verantwortlich? Wer würde alle Ihre Dateien schließen?

Wenn Sie nicht alles neu erstellen, werden die Ressourcen nur vom Destruktor bereinigt, wenn die Variable den Gültigkeitsbereich verlässt. So können Sie sicherer sein, dass Ressourcen erfolgreich bereinigt werden.

Dieses Konzept wird als RAII - Resource Allocation Is Initialization bezeichnet und kann Ihre Fähigkeit zur Beschaffung und Entsorgung von Ressourcen drastisch verbessern.


6

Der erste Fall ist nicht immer stapelweise zugeordnet. Wenn es Teil eines Objekts ist, wird es überall dort zugewiesen, wo sich das Objekt befindet. Zum Beispiel:

class Rectangle {
    Pixel top_left;
    Pixel bottom_right;
}

Rectangle r1; // Pixel is allocated on the stack
Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap

Die Hauptvorteile von Stapelvariablen sind:

  • Sie können das RAII-Muster verwenden zum Verwalten von Objekten verwenden. Sobald das Objekt den Gültigkeitsbereich verlässt, wird sein Destruktor aufgerufen. Ein bisschen wie das "using" -Muster in C #, aber automatisch.
  • Es gibt keine Möglichkeit einer Nullreferenz.
  • Sie müssen sich nicht um die manuelle Verwaltung des Objektspeichers kümmern.
  • Es verursacht weniger Speicherzuordnungen. Speicherzuweisungen, insbesondere kleine, sind in C ++ wahrscheinlich langsamer als in Java.

Sobald das Objekt erstellt wurde, gibt es keinen Leistungsunterschied zwischen einem auf dem Heap zugewiesenen und einem auf dem Stapel zugewiesenen Objekt (oder wo auch immer).

Sie können jedoch keinen Polymorphismus verwenden, es sei denn, Sie verwenden einen Zeiger. Das Objekt hat einen vollständig statischen Typ, der zur Kompilierungszeit festgelegt wird.


4

Objektlebensdauer. Wenn die Lebensdauer Ihres Objekts die Lebensdauer des aktuellen Bereichs überschreiten soll, müssen Sie den Heap verwenden.

Wenn Sie die Variable andererseits nicht über den aktuellen Bereich hinaus benötigen, deklarieren Sie sie auf dem Stapel. Es wird automatisch zerstört, wenn es den Gültigkeitsbereich verlässt. Seien Sie nur vorsichtig, wenn Sie die Adresse weitergeben.


4

Ich würde sagen, es geht viel um Geschmackssache. Wenn Sie eine Schnittstelle erstellen, über die Methoden Zeiger anstelle von Referenzen verwenden können, können Sie dem Aufrufer erlauben, null zu übergeben. Da Sie dem Benutzer erlauben, null zu übergeben, wird der Benutzer dies tun in Null übergeben.

Da Sie sich fragen müssen: "Was passiert, wenn dieser Parameter Null ist?", Müssen Sie defensiver codieren und sich ständig um Nullprüfungen kümmern. Dies spricht für die Verwendung von Referenzen.

Manchmal möchten Sie jedoch wirklich in der Lage sein, Null zu übergeben, und dann kommen Referenzen nicht in Frage :) Zeiger geben Ihnen mehr Flexibilität und ermöglichen es Ihnen, fauler zu sein, was wirklich gut ist. Niemals zuweisen, bis Sie wissen, dass Sie zuweisen müssen!


Er bezog sich nicht auf Funktionsargumente, sondern sprach darüber, wo die Dinge zugeordnet sind (Heap vs Stack). Er bemerkte, dass Java nur alle Objekte auf dem Haufen (ich habe von einigen cleveren Tricks in modernen Versionen gehört, um einige Objekte automatisch auf Stapel zu legen).
Evan Teran

Ich denke, Sie beantworten eine andere Frage zu Zeigern als zu Referenzen. eher als die Frage des OP nach stapelbasierten oder heapbasierten Objekten.
Saw-Lau

4

Das Problem sind nicht Zeiger an sich (abgesehen von der Einführung von NULLZeigern), sondern die manuelle Speicherverwaltung.

Der lustige Teil ist natürlich, dass jedes Java-Tutorial, das ich gesehen habe, erwähnt hat, dass der Garbage Collector so cool ist, weil Sie nicht daran denken müssen, anzurufen delete, wenn C ++ in der Praxis nur benötigt, deletewenn Sie anrufen new(und delete[]wenn Sie anrufen) new[]).


2

Verwenden Sie Zeiger und dynamisch zugewiesene Objekte NUR, WENN SIE MÜSSEN. Verwenden Sie nach Möglichkeit statisch zugewiesene (globale oder Stapel-) Objekte.

  • Statische Objekte sind schneller (kein Neu / Löschen, keine Indirektion, um darauf zuzugreifen)
  • Keine Objektlebensdauer, über die Sie sich Sorgen machen müssen
  • Weniger Tastenanschläge Besser lesbar
  • Viel robuster. Jedes "->" ist ein potenzieller Zugriff auf NIL oder ungültigen Speicher

Zur Verdeutlichung meine ich in diesem Zusammenhang "statisch" nicht dynamisch zugeordnet. IOW, alles NICHT auf dem Haufen. Ja, sie können auch Probleme mit der Objektlebensdauer haben - in Bezug auf die Reihenfolge der Singleton-Zerstörung -, aber wenn sie auf den Haufen geklebt werden, wird normalerweise nichts gelöst.


Ich kann nicht sagen, dass mir der "statische" Rat gefällt. Erstens löst es das Problem nicht (da statische Objekte zur Laufzeit nicht zugewiesen werden können), und zweitens haben sie viele eigene Probleme (z. B. Thread-Sicherheit). Das heißt, ich habe dich nicht -1.
Jalf

Sie sollten auch beachten, dass die Statik sowohl Probleme mit der Start- als auch mit der Stopplebensdauer aufweist (Google für "Fiasko bei der Reihenfolge der statischen Initialisierung"). Das heißt, ich habe dich auch nicht -1. Also tu mir bitte nichts an! :)
Johannes Schaub - litb

1
@Roddy - Meinten Sie "automatisch" (Stapel zugewiesen) statt "statisch"? (Und ich habe dich auch nicht -1.)
Fred Larson

@ Jalf- vielleicht war 'statisch' nicht das beste Wort. Denken Sie an das Problem der Sperrung von Singleton-Konstruktionen aus mehreren Threads?
Roddy

Ich denke an alle Variablen, die mit dem Schlüsselwort "static" deklariert wurden. Wenn Sie das nicht gemeint haben, sollten Sie dieses Wort wahrscheinlich vermeiden. :) Wie Fred sagte, haben Objekte auf dem Stapel eine "automatische" Speicherklasse. Wenn Sie das gemeint haben, ist Ihre Antwort viel sinnvoller.
Jalf

2

Warum nicht für alles Zeiger verwenden?

Sie sind langsamer.

Compiler-Optimierungen sind mit Zeigerzugriffssymantiken nicht so effektiv. Sie können sie auf einer beliebigen Anzahl von Websites nachlesen, aber hier ist ein anständiges PDF von Intel.

Überprüfen Sie die Seiten 13,14,17,28,32,36;

Erkennen unnötiger Speicherreferenzen in der Schleifennotation:

for (i = j + 1; i <= *n; ++i) { 
X(i) -= temp * AP(k); } 

Die Notation für die Schleifengrenzen enthält den Zeiger oder die Speicherreferenz. Der Compiler hat keine Möglichkeit vorherzusagen, ob der Wert, auf den der Zeiger n verweist, durch eine andere Zuweisung mit Schleifeniterationen geändert wird. Dies verwendet die Schleife, um den Wert, auf den n verweist, für jede Iteration neu zu laden. Die Codegenerator-Engine kann auch das Planen einer Software-Pipeline-Schleife verweigern, wenn ein potenzielles Zeiger-Aliasing gefunden wird. Da der Wert, auf den der Zeiger n verweist, nicht innerhalb der Schleife angelt und für den Schleifenindex unveränderlich ist, muss das Laden von * ns außerhalb der Schleifengrenzen durchgeführt werden, um die Planung und die Disambiguierung der Zeiger zu vereinfachen.

... eine Reihe von Variationen zu diesem Thema ....

Komplexe Speicherreferenzen. Mit anderen Worten, die Analyse von Referenzen wie komplexen Zeigerberechnungen belastet die Fähigkeit von Compilern, effizienten Code zu generieren. Stellen im Code, an denen der Compiler oder die Hardware eine komplexe Berechnung durchführen, um festzustellen, wo sich die Daten befinden, sollten im Mittelpunkt der Aufmerksamkeit stehen. Zeiger-Aliasing und Code-Vereinfachung unterstützen den Compiler beim Erkennen von Speicherzugriffsmustern, sodass der Compiler den Speicherzugriff mit der Datenmanipulation überlappen kann. Das Reduzieren unnötiger Speicherreferenzen kann dem Compiler die Möglichkeit geben, die Software weiterzuleiten. Viele andere Datenpositionseigenschaften, wie z. B. Aliasing oder Ausrichtung, können leicht erkannt werden, wenn die Speicherreferenzberechnungen einfach gehalten werden.


Link ist in die Irre gegangen. :-(
derM

1

Die Frage aus einem anderen Blickwinkel betrachten ...

In C ++ können Sie Objekte mit Zeigern ( Foo *) und Referenzen ( Foo &) referenzieren . Wo immer möglich, verwende ich eine Referenz anstelle eines Zeigers. Wenn Sie beispielsweise als Referenz auf eine Funktion / Methode übergeben, können Sie mithilfe von Referenzen (hoffentlich) die folgenden Annahmen treffen:

  • Das Objekt, auf das verwiesen wird, gehört nicht der Funktion / Methode und sollte es daher nicht sein delete das Objekt sein. Es ist wie zu sagen: "Hier, verwenden Sie diese Daten, aber geben Sie sie zurück, wenn Sie fertig sind."
  • NULL-Zeigerreferenzen sind weniger wahrscheinlich. Es ist möglich, eine NULL-Referenz zu übergeben, aber zumindest ist dies nicht die Schuld der Funktion / Methode. Eine Referenz kann nicht einer neuen Zeigeradresse zugewiesen werden, sodass Ihr Code sie möglicherweise nicht versehentlich NULL oder einer anderen ungültigen Zeigeradresse zugewiesen hat, was zu einem Seitenfehler geführt hat.

1

Die Frage ist: Warum würden Sie Zeiger für alles verwenden? Stapelzugeordnete Objekte sind nicht nur sicherer und schneller zu erstellen, sondern es wird auch noch weniger eingegeben, und der Code sieht besser aus.


0

Etwas, das ich nicht erwähnt habe, ist die erhöhte Speichernutzung. Angenommen, 4-Byte-Ganzzahlen und Zeiger

Pixel p;

wird 8 Bytes verwenden, und

Pixel* p = new Pixel();

wird 12 Bytes verwenden, eine 50% ige Erhöhung. Es klingt nicht nach viel, bis Sie genug für ein 512x512-Bild zugewiesen haben. Dann sprechen Sie 2 MB statt 3 MB. Dadurch wird der Aufwand für die Verwaltung des Heapspeichers mit all diesen Objekten ignoriert.


0

Auf dem Stapel erstellte Objekte werden schneller erstellt als zugewiesene Objekte.

Warum?

Da das Zuweisen von Speicher (mit dem Standard-Speichermanager) einige Zeit in Anspruch nimmt (um einen leeren Block zu finden oder diesen Block sogar zuzuweisen).

Außerdem haben Sie keine Probleme mit der Speicherverwaltung, da sich das Stapelobjekt automatisch selbst zerstört, wenn es sich außerhalb des Gültigkeitsbereichs befindet.

Der Code ist einfacher, wenn Sie keine Zeiger verwenden. Wenn Sie in Ihrem Entwurf Stapelobjekte verwenden können, empfehle ich Ihnen, dies zu tun.

Ich selbst würde das Problem mit intelligenten Zeigern nicht komplizieren.

OTOH Ich habe ein wenig im eingebetteten Feld gearbeitet und das Erstellen von Objekten auf dem Stapel ist nicht sehr intelligent (da der für jede Aufgabe / jeden Thread zugewiesene Stapel nicht sehr groß ist - Sie müssen vorsichtig sein).

Es ist also eine Frage der Wahl und der Einschränkungen, es gibt keine Antwort, die zu allen passt.

Und vergessen Sie wie immer nicht , es so einfach wie möglich zu halten.


0

Wenn Sie Rohzeiger verwenden, haben Sie grundsätzlich kein RAII.


0

Das hat mich sehr verwirrt, als ich ein neuer C ++ - Programmierer war (und es war meine Muttersprache). Es gibt viele sehr schlechte C ++ - Tutorials, die im Allgemeinen in eine von zwei Kategorien zu fallen scheinen: "C / C ++" - Tutorials, was wirklich bedeutet, dass es sich um ein C-Tutorial handelt (möglicherweise mit Klassen), und C ++ - Tutorials, die C ++ für Java mit Löschen halten .

Ich glaube, ich habe ungefähr 1 - 1,5 Jahre (mindestens) gebraucht, um irgendwo in meinem Code "neu" einzugeben. Ich habe häufig STL-Container wie Vektor verwendet, was sich für mich darum gekümmert hat.

Ich denke, viele Antworten scheinen entweder zu ignorieren oder es einfach zu vermeiden, direkt zu sagen, wie man dies vermeidet. Im Allgemeinen müssen Sie im Konstruktor nicht mit new zuordnen und im destructor mit delete aufräumen. Stattdessen können Sie das Objekt selbst direkt in die Klasse einfügen (anstatt einen Zeiger darauf) und das Objekt selbst im Konstruktor initialisieren. Dann erledigt der Standardkonstruktor in den meisten Fällen alles, was Sie brauchen.

In fast allen Situationen, in denen dies nicht funktioniert (z. B. wenn der Stapelspeicher knapp wird), sollten Sie wahrscheinlich ohnehin einen der Standardcontainer verwenden: std :: string, std :: vector und std :: map sind die drei, die ich am häufigsten benutze, aber std :: deque und std :: list sind auch ziemlich häufig. Die anderen (Dinge wie std :: set und das nicht standardmäßige Seil ) werden nicht so oft verwendet, verhalten sich aber ähnlich. Sie alle weisen aus dem freien Speicher zu (C ++ - Sprache für "den Heap" in einigen anderen Sprachen), siehe: C ++ STL-Frage: Allokatoren


-2

Der erste Fall ist am besten, es sei denn, der Pixel-Klasse werden weitere Mitglieder hinzugefügt. Wenn immer mehr Mitglieder hinzugefügt werden, besteht die Möglichkeit einer Stapelüberlaufausnahme


Ich meinte Mitglieder bedeutet Mitgliedsvariablen. keine Methoden. Entschuldigung, wenn ich nicht klar war.
Uday
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.