Absturz beim Löschen durch Destruktor

8

Im folgenden Programm beabsichtige ich, char* lineInhalte von einem Objekt auf ein anderes durch zu kopieren strcpy. Wenn das Programm jedoch endet, obj2funktioniert der Destruktor von einwandfrei, der objAbsturz jedoch nicht. gdb zeigt unterschiedliche Adressen linefür beide Objekte an.

class MyClass {
        public:
                char *line;
                MyClass() {
                        line = 0;
                }
                MyClass(const char *s) {
                        line = new char[strlen(s)+1];
                        strcpy(line, s);
                }
                ~MyClass() {
                        delete[] line;
                        line = 0;
                }
                MyClass &operator=(const MyClass &other) {
                        delete[] line;
                        line = new char[other.len()+1];
                        strcpy(line, other.line);
                        return *this;
                }
                int len(void) const {return strlen(line);}
};

int main() {
        MyClass obj("obj");
        MyClass obj2 = obj;
anurag86
quelle
Obwohl Sie nullterminierte Zeichenfolgen im C-Stil verwenden, programmieren Sie immer noch in C ++.
Einige Programmierer Typ
5
Sie benötigen auch einen Kopierkonstruktor. Regel drei
ChrisMM
Das liegt daran, dass ich gebeten wurde, eine Kopierzeichenfolge in c ++ durch strcpy zu simulieren
anurag86
2
MyClass obj1; MyClass obj2 = obj1;Nebenbei bemerkt , sobald Sie einen Kopierkonstruktor hinzugefügt haben : wird immer noch segfault, weil Sie aufrufen, strlen(obj1.line)welches ist strlen(NULL). Wie würde MyClass obj1; obj1.len();.
Bill Lynch
2
Auch undefiniertes Verhalten: MyClass obj1; obj1.len(); Es ist undefiniertes Verhalten, strleneinen Nullzeiger aufzurufen .
PaulMcKenzie

Antworten:

13

Mit

MyClass obj2 = obj;

Sie haben keine Aufgabe, Sie haben eine Kopierkonstruktion . Und Sie befolgen nicht die Regeln von drei, fünf oder null, da Sie keinen Kopierkonstruktor haben, sodass der standardmäßig generierte nur den Zeiger kopiert.

Das heißt, danach haben Sie zwei Objekte, deren lineZeiger beide auf genau denselben Speicher zeigen. Dies führt zu undefiniertem Verhalten, sobald eines der Objekte zerstört wird und das andere mit einem ungültigen Zeiger zurückbleibt.

Die naive Lösung besteht darin, einen Kopierkonstruktor hinzuzufügen, der eine Tiefenkopie der Zeichenfolge selbst erstellt, ähnlich wie es Ihr Zuweisungsoperator tut.

Eine bessere Lösung wäre, std::stringstattdessen für Ihre Zeichenfolgen zu verwenden und der Regel Null zu folgen.

Ein Programmierer
quelle
4

Sie müssen einen Kopierkonstruktor erstellen. Dies muss die Regel 3/5 erfüllen . Sie erstellen obj2, dh ein Kopierkonstruktor wird aufgerufen, nicht der Kopierzuweisungsoperator.

Da Sie keinen Kopierkonstruktor haben, wird eine "flache" Kopie erstellt. Dies bedeutet, dass linenach Wert kopiert wird. Da es sich um einen Zeiger handelt, zeigen beide objund obj2auf denselben Speicher. Der erste Destruktor wird aufgerufen und löscht diesen Speicher einwandfrei. Der zweite Konstruktor wird aufgerufen und es kommt zu einem doppelten Löschen, das Ihren Segmentierungsfehler verursacht.

class MyClass {
public:
  char *line = nullptr;
  std::size_t size_ = 0;  // Need to know the size at all times, can't 
                          // rely on null character existing
  const std::size_t MAX_SIZE = 256;  // Arbitrarily chosen value
  MyClass() { }
  MyClass(const char *s) : size_(strlen(s)) {
    if (size_ > MAX_SIZE) size_ = MAX_SIZE;
    line = new char[size_];
    strncpy(line, s, size_ - 1);  // 'n' versions are better
    line[size_ - 1] = '\0';
  }
  MyClass(const MyClass& other) : size_(other.size_) {  // Copy constructor
    line = new char[size_ + 1];
    strncpy(line, other.line, size_);
    line[size_] = '\0';
  }
  ~MyClass() {
    delete[] line;
    line = nullptr;
  }
  MyClass& operator=(const MyClass &other) {
    if (line == other.line) return *this;  // Self-assignment guard
    size_ = other.size_;
    delete[] line;
    line = new char[other.size_ + 1];
    strncpy(line, other.line, size_);
    line[size_] = '\0';
    return *this;
  }
  int len(void) const { return size_; }
};

Wenn Sie mit C-Strings arbeiten, können Sie das Nullzeichen absolut nicht verlieren. Das Problem ist, dass es extrem leicht zu verlieren ist. Es fehlte Ihnen auch ein Selbstzuweisungsschutz in Ihrem Kopierzuweisungsoperator. Das hätte dazu führen können, dass Sie versehentlich ein Objekt zerstört haben. Ich habe ein size_Mitglied hinzugefügt und strncpy()stattdessen verwendet, strcpy()weil es unglaublich wichtig ist, eine maximale Anzahl von Zeichen angeben zu können, wenn ein Nullzeichen verloren geht. Es wird Schäden nicht verhindern, aber es wird es mildern.

Es gibt noch einige andere Dinge, die mir bei der Verwendung der Standardelementinitialisierung (ab C ++ 11) und der Verwendung einer Konstruktormitgliedsinitialisierungsliste gefallen haben . Vieles davon wird unnötig, wenn Sie es verwenden können std::string. C ++ kann "C mit Klassen" sein, aber es lohnt sich, sich die Zeit zu nehmen, um wirklich zu erkunden, was die Sprache zu bieten hat.

Ein Konstruktor und Destruktor für Arbeitskopien ermöglicht es uns, unseren Kopierzuweisungsoperator mithilfe der "Copy and Swap Idiom" zu vereinfachen.

#include <utility>

MyClass& operator=(MyClass tmp) { // Copy by value now
  std::swap(*this, tmp);
  return *this;
}

Link zur Erklärung .

sweenish
quelle
2
Eine bessere Lösung wäre eine Implementierung mit Copy and Swap Idiom.
Der Philomath
1
Ich habe etwas Neues gelernt und es gefällt mir. Ich werde ein zusätzliches Stück hinzufügen.
Zwischen
Vielen Dank für die Kopie mit Swap-Beispiel
anurag86