Dreierregel in C++

Jay Shaw 12 Oktober 2023
  1. Die Regel der Großen Drei in C++ verstehen
  2. Implizite Definition von Konstruktoren in C++
  3. Explizite Definition von Konstruktoren in C++
  4. Fazit
Dreierregel in C++

Die Regel der großen Drei ist eine der beliebtesten Programmiersprachen weltweit. Das Gesetz besagt, dass C++ einige spezielle Funktionen hat, die zusammen deklariert werden müssen, selbst wenn eine erforderlich ist.

Diese Funktionen sind Kopierkonstruktor, Kopierzuweisungsoperator und Destruktor.

Das Gesetz besagt, dass die anderen beiden auch folgen müssen, wenn eine der drei Sonderfunktionen in einem Programm deklariert wird. Wenn nicht, gerät das Programm in ernsthafte Speicherlecks.

Dieser Artikel erklärt ausführlich das Gesetz der großen Drei und wie man damit umgeht.

Die Regel der Großen Drei in C++ verstehen

Es muss bedacht werden, was passiert, wenn einer Klasse eine dynamisch zugewiesene Ressource hinzugefügt wird, um die großen Drei herauszufinden.

Im folgenden Beispiel wird eine Konstruktorklasse erstellt, die dem Zeiger etwas Speicherplatz zuweist.

class Big3 {
  int* x;

 public:
  Big3() : x(new int()) { std::cout << "Resource allocated \n"; }
};

Wie oben zu sehen ist, wird der Speicher dem Zeiger zugewiesen. Jetzt muss es auch mit einem Destruktor freigegeben werden.

~Big3() {
  std::cout << "Resource is released \n";
  delete x;
}

Die allgemeine Wahrnehmung ist, dass die Arbeit bis zu diesem Punkt getan ist. Aber die Realität ist weit davon entfernt.

Einige schwerwiegende Speicherverluste treten auf, wenn ein Kopierkonstruktor innerhalb dieser Klasse initiiert wird.

Der Grund für dieses Problem ist die implizite Definition von Konstruktoren. Wenn ein Objekt kopiert wird, ohne einen Konstruktor zu definieren, erstellt der Compiler implizit einen Kopierkonstruktor, der keinen Klon, sondern nur einen Schatten desselben Objekts erstellt.

Kopierkonstruktoren führen standardmäßig flaches Kopieren auf Objekten aus. Das Programm läuft mit dieser Kopiermethode problemlos, bis einem Objekt dynamisch eine Ressource zugewiesen wird.

Im folgenden Code wird beispielsweise ein Objekt p1 erstellt. Ein anderes Objekt, p2, ist ein Kopierobjekt von p1.

int main() {
  Big3 p1;
  Big3 p2(p1);
}

Wenn in diesem Code der Destruktor das Objekt p1 zerstört, wird p2 zu einem hängenden Zeiger. Denn Objekt p2 zeigt auf die Referenz von Objekt p1.

Der vollständige Code ist unten zum besseren Verständnis angegeben.

#include <iostream>

class Big3 {
  int* x;

 public:
  Big3() : x(new int()) { std::cout << "Resource allocated \n"; }
  ~Big3() {
    std::cout << "Resource is released \n";
    delete x;
  }
};

int main() {
  Big3 p1;
  Big3 p2(p1);
}

Um Probleme wie einen hängenden Zeiger zu vermeiden, müssen die Programmierer alle erforderlichen Konstruktoren explizit deklarieren, worum es bei der Big-Three-Regel geht.

Implizite Definition von Konstruktoren in C++

Es gibt zwei Möglichkeiten, wie eine Kopie eines Objekts erstellt wird.

  1. Flaches Kopieren - Verwenden eines Konstruktors zum Kopieren der Adresse eines Objekts und Speichern in dem neuen Objekt.
  2. Tiefes Kopieren – Verwenden eines ähnlichen Konstruktors, der in dieser Adresse gespeicherte Werte in die neue kopiert.

Wenn einem Objekt Speicher zugewiesen wird, kopiert die implizite Version des Kopierkonstruktors normalerweise die Referenz des Zeigers x, anstatt ein neues Objekt mit einem eigenen Speicherzuweisungssatz zu erstellen.

Nachfolgend finden Sie eine Darstellung, wie spezielle Elementfunktionen implizit definiert werden.

book(const book& that) : name(that.name), slno(that.slno) {}

book& operator=(const book& that) {
  name = that.name;
  slno = that.slno;
  return *this;
}

~book() {}

In diesem Beispiel werden die drei speziellen Elementfunktionen definiert. Der erste ist der Kopierkonstruktor, der die Member-Variablen über eine Member-Initialisierungsliste zuweist: name(that.name), slno(that.slno).

Der zweite Konstruktor ist der Kopierzuweisungskonstruktor, der eine Kopie der Objekte der Klasse erstellt. Hier wird das Überladen von Operatoren verwendet, um Kopien des Objekts zu erstellen.

Am Ende bleibt der Destruktor leer, da keine Ressource zugewiesen wird. Der Code gibt keine Fehler aus, da Objekte keine Speicherzuweisung benötigen.

Warum implizite Definitionen im Ressourcenmanagement versagen

Angenommen, ein Mitgliedszeiger der Klasse erhält eine Speicherzuweisung. Die Referenz dieses Elementzeigers wird in das neue Objekt kopiert, wenn das Objekt dieser Klasse mit dem Standardzuweisungsoperator und dem Kopierfunktionskonstruktor kopiert wird.

Infolgedessen wirken sich alle Änderungen, die an einem Objekt vorgenommen werden, auch auf das andere aus, da das neue und das alte Objekt beide auf denselben Speicherort zeigen. Das zweite Objekt wird weiterhin versuchen, es zu verwenden.

class person {
  char* name;
  int age;

 public:
  // constructor acquires a resource
  // dynamic memory obtained via new
  person(const char* the_name, int the_age) {
    name = new char[strlen(the_name) + 1];
    strcpy(name, the_name);
    age = the_age;
  }

  // destructor must release this resource via delete
  ~person() { delete[] name; }
};

Das obige Beispiel kopiert name, wodurch der Zeiger kopiert wird, wobei die Werte darin gespeichert bleiben.

Wenn der Destruktor deklariert wird, löscht er nur die Instanz des ursprünglichen Objekts. Aber das kopierte Objekt zeigt immer wieder auf dieselbe Referenz, die nun zerstört dasteht.

Dies ist die Hauptursache für Speicherlecks. Sie entstehen, wenn der Destruktor das ursprüngliche Objekt und seine Referenz löscht, während das vom Kopierkonstruktor erstellte Objekt weiter baumelt und auch als baumelnder Zeiger bezeichnet wird.

Wenn ein baumelnder Zeiger deaktiviert bleibt, führt dieser Speicherverweis in ähnlicher Weise zu mehreren Speicherlecks in der Zukunft.

Die einzige Lösung für dieses Problem besteht darin, Konstruktoren explizit zu deklarieren oder, in einfachen Worten, ein eigenes Modell eines Kopierkonstruktors und eines Zuweisungsoperators zu erstellen, um dies zu beheben.

Die benutzerdefinierten Konstruktoren kopieren die Werte, auf die der Anfangszeiger zeigt, anstatt seine Adresse, und weisen den neuen Objekten separaten Speicher zu.

Explizite Definition von Konstruktoren in C++

Es wurde beobachtet, dass implizit definierte Konstruktoren zu einem flachen Kopieren von Objekten führten. Um das zu beheben, definieren Programmierer explizit einen Kopierkonstruktor, der beim tiefen Kopieren von Objekten in C++ hilft.

Tiefes Kopieren ist die Methode, die neue Speicherblöcke zuweist, um den Wert eines Zeigers zu speichern, anstatt nur eine Speicherreferenz zu speichern.

Diese Methode erfordert die explizite Definition aller drei speziellen Membermethoden, damit der Compiler beim Kopieren eines Objekts neuen Speicher zuweist.

Rolle von Destruktoren in explizit definierten Konstruktoren in C++

Ein Destruktor muss erstellt werden, um den Speicher zu löschen, der einem Funktionsobjekt zugewiesen ist. Dies riskiert Speicherlecks, wenn Sie dies nicht tun.

Bei impliziten Konstruktoren bleibt das Problem auch nach der Deklaration eines Destruktors bestehen. Das Problem tritt auf, weil, wenn der dem Objekt zugewiesene Speicher kopiert wird, das kopierte Objekt auf denselben Speicher wie das ursprüngliche Objekt zeigt.

Wenn einer den Speicher in seinem Destruktor löscht, hat der andere einen Zeiger auf ungültigen Speicher, und die Dinge werden kompliziert, wenn er versucht, ihn zu verwenden.

Daher muss ein explizit definierter Kopierkonstruktor erstellt werden, der neuen Objekten ihre Speicherfragmente zum Löschen gibt.

Das folgende Programm zeigt eine Demo-Klasse, die sich an die Regel der großen Drei hält.

class name {
 private:
  int* variable;  // pointer variable
 public:
  // Constructor
  name() { variable = new int; }

  void input(int var1)  // Parameterized method to take input
  {
    *variable = var1;
  }

  // Copy Constructor
  name(name& sample) {
    variable = new int;
    *variable = *(sample.variable);

    // destructor
    ~name() { delete variable; }
  };

Implementierung von Kopierkonstruktoren in C++

Das Programm hat eine Klasse Book mit einem standardmäßig parametrisierten Konstruktor und einem Destruktor. Ein Standardkonstruktor gibt Nullwerte zurück, wenn keine Eingabe bereitgestellt wird, während ein parametrisierter Konstruktor die Werte initialisiert und kopiert.

Hier ist eine Ausnahmebehandlungsmethode (try-catch) enthalten, die Ausnahmen auslöst, wenn die Variable m_Name keine Ressource zuweisen kann.

Nach dem Destruktor wird ein Kopierkonstruktor erstellt, der eine Kopie des ursprünglichen Objekts erstellt.

#include <cstring>
#include <exception>
#include <iostream>

using namespace std;

class Book {
  int m_Slno;
  char* m_Name;

 public:
  // Default Constructor
  Book() : m_Slno(0), m_Name(nullptr) {}

  // Parametarized Constructor
  Book(int slNumber, char* name) {
    m_Slno = slNumber;
    unsigned int len = strlen(name) + 1;
    try {
      m_Name = new char[len];
    } catch (std::bad_alloc e) {
      cout << "Exception received: " << e.what() << endl;
      return;
    }
    memset(m_Name, 0, len);
    strcpy(m_Name, name);
  }

  // Destructor
  ~Book() {
    if (m_Name) {
      delete[] m_Name;
      m_Name = nullptr;
    }
  }

  friend ostream& operator<<(ostream& os, const Book& s);
};

ostream& operator<<(ostream& os, const Book& s) {
  os << s.m_Slno << ", " << s.m_Name << endl;
  return os;
}

int main() {
  Book s1(124546, "Digital Marketing 101");
  Book s2(134645, "Fault in our stars");

  s2 = s1;

  cout << s1;
  cout << s2;

  s1.~Book();
  cout << s2;

  return 0;
}

Wenn das Objekt s1 zerstört wird, verliert s2 in der Hauptfunktion nicht sein dynamisches Objekt, das die String-Variable des Objekts s ist.

Ein weiteres Beispiel unten zeigt, wie Objekte mithilfe eines Kopierkonstruktors tief kopiert werden. Im folgenden Code wird eine Konstruktorklasse design erstellt.

Die Klasse hat drei private Variablen – zwei der statischen l und h und ein dynamisches Objekt w.

#include <iostream>
using namespace std;

// A class design
class design {
 private:
  int l;
  int* w;
  int h;

 public:
  // Constructor
  design() { w = new int; }

  // Method to take input
  void set_dimension(int len, int brea, int heig) {
    l = len;
    *w = brea;
    h = heig;
  }

  // Display Function
  void show_data() {
    cout << "The Length is = " << l << "\n"
         << "Breadth  of the design = " << *w << "\n"
         << "Height = " << h << "\n"
         << endl;
  }

  // Deep copy is initialized here
  design(design& sample) {
    l = sample.l;
    w = new int;
    *w = *(sample.w);
    h = sample.h;
  }

  // Destructor
  ~design() { delete w; }
};

// Driver Code
int main() {
  // Object of class first
  design first;

  // Passing Parameters
  first.set_dimension(13, 19, 26);

  // Calling display method
  first.show_data();

  // Copying the data of 'first' object to 'second'
  design second = first;

  // Calling display method again to show the 'second' object
  second.show_data();

  return 0;
}

Ausgabe:

The Length is = 13
Breadth  of the design = 19
Height = 26

The Length is = 13
Breadth  of the design = 19
Height = 26
--------------------------------
Process exited after 0.2031 seconds with return value 0
Press any key to continue . . .

Fazit

Dieser Artikel enthält eine umfassende und detaillierte Erklärung der Big-3-Regel und wie sie sich auf die Programmierung auswirkt. Die Leser lernen die Notwendigkeit und Bedeutung der Big-3-Regel kennen.

Gleichzeitig werden einige neue Konzepte erklärt, wie das tiefe und flache Kopieren, das mehrere Implementierungen hat.