デストラクタで削除するとクラッシュする


8

次のプログラムでは、char* lineを介してオブジェクト間でコンテンツをコピーするつもりstrcpyです。ただし、プログラムが終了すると、デストラクタはobj2正常に動作しますが、objクラッシュは発生しません。gdbはline、両方のオブジェクトのの異なるアドレスを表示します。

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;

Cスタイルのnullで終了する文字列を使用していても、C ++でプログラミングしています。
一部のプログラマー、

5
コピーコンストラクタも必要です。3つのルール
ChrisMM

これは、c ++でstrcpyを介して文字列のコピーをシミュレートするように依頼されたためです
anurag86

2
ただ、余談として、あなたはコピーコンストラクタを追加した後:MyClass obj1; MyClass obj2 = obj1;あなたが呼び出しますので、まだセグメンテーションフォールトうstrlen(obj1.line)としますstrlen(NULL)。のようにMyClass obj1; obj1.len();
ビル・リンチ

2
また、未定義の動作:nullポインターMyClass obj1; obj1.len(); を呼び出すのは未定義の動作strlenです。
PaulMcKenzie

回答:


13

MyClass obj2 = obj;

あなたには割り当てがなく、あなたにはコピー構築があります。また、コピーコンストラクターがないため、3、5、または0規則に従っていないため、デフォルトで生成されたコンストラクターはポインターをコピーするだけです。

つまり、この後、2つのオブジェクトのlineポインターが両方ともまったく同じメモリを指していることになります。それはにつながる未定義の動作、それが無効なポインタで他を残してオブジェクトの一つが破壊された後。

素朴な解決策は、代入演算子が行うのと同様に、文字列自体のディープコピーを行うコピーコンストラクタを追加することです。

より良い解決策はstd::string、代わりに文字列を使用し、ゼロの規則に従うことです。


4

コピーコンストラクタを作成する必要があります。これは3/5ルールを実行する必要があります。を作成obj2しています。つまり、コピー代入演算子ではなく、コピーコンストラクターが呼び出されます。

コピーコンストラクタがないため、「浅い」コピーが作成されます。つまり、line値によってコピーされます。ポインタなので、objobj2は同じメモリを指しています。最初のデストラクタが呼び出され、そのメモリを正常に消去します。2番目のコンストラクターが呼び出され、二重削除が発生して、セグメンテーション違反が発生します。

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_; }
};

C-Stringを扱う場合、null文字を失うことは絶対にありません。問題は、失うのが非常に簡単なことです。また、コピー割り当て演算子に自己割り当てガードがありませんでした。それが原因で、誤ってオブジェクトを削除した可能性があります。null文字を失う場合に最大文字数を指定できることが非常に重要であるため、代わりにsize_メンバーを追加して使用しました。ダメージは防げませんが軽減されます。strncpy()strcpy()

デフォルトメンバーの初期化(C ++ 11現在)を使用したり、コンストラクターメンバーの初期化リストを使用したりするのが好きなものがあります。使えるようになればこれは不要になりますstd::string。C ++は「クラスを伴うC」にすることもできますが、時間をかけて実際に言語が提供するものを探索する価値があります。

作業コピーのコンストラクタとデストラクタが実行できることの1つは、「コピーとスワップのイディオム」を使用してコピー割り当て演算子を単純化することです。

#include <utility>

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

説明へのリンク


2
より良い解決策は、コピーとスワップのイディオムを使用した実装です。
フィロマス

1
新しいことを学び、私はそれが好きです。私は余分なビットを追加します。
スウィーニッシュ

スワップの例を使用してコピーをありがとう
anurag86
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.