C++ の三つのルール

Jay Shaw 2023年10月12日
  1. C++ でのビッグスリーのルールを理解する
  2. C++ でのコンストラクターの暗黙的な定義
  3. C++ でのコンストラクターの明示的な定義
  4. まとめ
C++ の三つのルール

ビッグ 3 のルールは、世界中で最も人気のあるコーディングイディオムの 1つです。法律では、C++ には、必要な場合でも一緒に宣言する必要のあるいくつかの特別な関数があると規定されています。

これらの関数は、コピーコンストラクタ、コピー代入演算子、およびデストラクタです。

法律は、3つの特殊機能の 1つがプログラムで宣言されている場合、他の 2つも従わなければならないと述べています。そうでない場合、プログラムは深刻なメモリリークに遭遇します。

この記事では、ビッグ 3 の法則とそれを回避する方法について詳しく説明します。

C++ でのビッグスリーのルールを理解する

動的に割り当てられたリソースがクラスに追加されたときに何が発生するかを考慮して、ビッグ 3 について調べる必要があります。

次の例では、ポインタにメモリスペースを割り当てるコンストラクタクラスが作成されています。

class Big3 {
  int* x;

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

上記のように、メモリはポインタに割り当てられます。さて、それもデストラクタを使用して解放する必要があります。

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

一般的な認識は、作業はこの時点まで行われているというものです。しかし、現実はどこにもありません。

このクラス内でコピーコンストラクタが開始されると、深刻なメモリリークが発生します。

この問題の背後にある理由は、コンストラクターの暗黙の定義です。コンストラクターを定義せずにオブジェクトをコピーすると、コンパイラーは、クローンを作成せず、同じオブジェクトのシャドウのみを作成するコピーコンストラクターを暗黙的に作成します。

コピーコンストラクターは、デフォルトで、オブジェクトに対して浅いコピーを実行します。プログラムは、リソースがオブジェクトに動的に割り当てられるまで、このコピー方法で正常に実行されます。

たとえば、以下のコードでは、オブジェクト p1 が作成されます。もう 1つのオブジェクト p2 は、p1 のコピーオブジェクトです。

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

このコードでは、デストラクタがオブジェクト p1 を破棄すると、p2 がダングリングポインタになります。これは、オブジェクト p2 がオブジェクト p1 の参照を指しているためです。

理解を深めるために、完全なコードを以下に示します。

#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);
}

ダングリングポインタのような問題を回避するために、プログラマーは必要なすべてのコンストラクターを明示的に宣言する必要があります。これが、大きな 3つのルールです。

C++ でのコンストラクターの暗黙的な定義

オブジェクトのコピーを作成する方法は 2つあります。

  1. 浅いコピー-コンストラクターを使用してオブジェクトのアドレスをコピーし、それを新しいオブジェクトに格納します。
  2. ディープコピー-同様のコンストラクターを使用して、そのアドレス内に格納されている値を新しいアドレスにコピーします。

通常、あるメモリがオブジェクトに割り当てられると、コピーコンストラクタの暗黙的なバージョンは、独自のメモリ割り当てセットを使用して新しいオブジェクトを作成する代わりに、ポインタ x の参照をコピーします。

以下は、特別なメンバー関数が暗黙的に定義される方法を表しています。

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

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

~book() {}

この例では、3つの特別なメンバー関数が定義されています。1つ目は、メンバー初期化子リスト name(that.name), slno(that.slno) を介してメンバー変数を割り当てるコピーコンストラクターです。

2 番目のコンストラクターは、クラスのオブジェクトのコピーを作成するコピー代入コンストラクターです。ここでは、演算子のオーバーロードを使用してオブジェクトのコピーを作成します。

最終的に、リソースが割り当てられないため、デストラクタは空のままになります。オブジェクトはメモリ割り当てを必要としないため、コードはエラーをスローしません。

リソース管理で暗黙の定義が失敗する理由

クラスのメンバーポインタがメモリ割り当てを受け取ったとします。このクラスのオブジェクトがデフォルトの代入演算子とコピー関数コンストラクターを使用してコピーされると、このメンバーポインターの参照が新しいオブジェクトにコピーされます。

その結果、新しいオブジェクトと古いオブジェクトの両方が同じメモリ位置を指すため、一方のオブジェクトに加えられた変更はもう一方のオブジェクトにも影響します。2 番目のオブジェクトはそれを使用しようとし続けます。

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

上記の例では、name をコピーします。これにより、ポインタがコピーされ、値が内部に保存されたままになります。

デストラクタが宣言されると、元のオブジェクトのインスタンスが削除されるだけです。ただし、コピーされたオブジェクトは同じ参照を指し続け、現在は破棄されています。

これがメモリリークの主な原因です。これらは、デストラクタが元のオブジェクトとその参照を削除するときに発生しますが、コピーコンストラクタによって作成されたオブジェクトはダングリングを継続し、ダングリングポインタとも呼ばれます。

同様に、ダングリングポインタがチェックされていない場合、そのメモリ参照により、将来的に複数のメモリリークが発生します。

この問題の唯一の解決策は、コンストラクターを明示的に宣言するか、簡単に言うと、これを修正するためにコピーコンストラクターと代入演算子の独自のモデルを作成することです。

カスタムメイドのコンストラクターは、初期ポインターがアドレスではなく指す値をコピーし、新しいオブジェクトに個別のメモリーを割り当てます。

C++ でのコンストラクターの明示的な定義

暗黙的に定義されたコンストラクターは、オブジェクトの浅いコピーをもたらすことが観察されました。これを修正するために、プログラマーはコピーコンストラクターを明示的に定義します。これは、C++ でオブジェクトをディープコピーするのに役立ちます。

ディープコピーは、メモリ参照を保存するだけでなく、ポインタの値を格納するために新しいメモリブロックを割り当てる方法です。

このメソッドでは、コンパイラがオブジェクトのコピー中に新しいメモリを割り当てるように、3つの特別なメンバーメソッドすべてを明示的に定義する必要があります。

C++ で明示的に定義されたコンストラクターにおけるデストラクタの役割

関数オブジェクトが割り当てられているメモリを消去するには、デストラクタを作成する必要があります。そうしないと、メモリリークのリスクがあります。

暗黙のコンストラクタでは、デストラクタを宣言した後でも、問題は解決しません。この問題は、オブジェクトに割り当てられたメモリがコピーされた場合、コピーされたオブジェクトが元のオブジェクトと同じメモリを指すために発生します。

一方がデストラクタのメモリを削除すると、もう一方は無効なメモリへのポインタを持ち、それを使おうとすると事態は複雑になります。

その結果、明示的に定義されたコピーコンストラクターを作成して、新しいオブジェクトにメモリフラグメントをパージするように指定する必要があります。

以下のプログラムは、ビッグ 3 のルールに準拠したデモクラスを示しています。

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

C++ でコピーコンストラクタを実装する

プログラムには、デフォルトのパラメーター化されたコンストラクターとデストラクタを持つクラス Book があります。デフォルトのコンストラクターは、入力が提供されていない場合に null 値を返しますが、パラメーター化されたコンストラクターは値を初期化してコピーします。

ここには、変数 m_Name がリソースを割り当てることができない場合に例外をスローする例外処理メソッド(try-catch)が含まれています。

デストラクタの後に、元のオブジェクトのコピーを作成するコピーコンストラクタが作成されます。

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

main 関数では、オブジェクト s1 が破棄されても、s2 はオブジェクト s の文字列変数である動的オブジェクトを失うことはありません。

以下の別の例は、コピーコンストラクターを使用してオブジェクトをディープコピーする方法を示しています。以下のコードでは、コンストラクタークラス design が作成されています。

クラスには 3つのプライベート変数があります。静的 lh の 2つと、動的オブジェクト 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;
}

出力:

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 . . .

まとめ

この記事では、ビッグ 3 のルールと、それがプログラミングにどのように影響するかについて、包括的かつ詳細に説明します。読者はビッグ 3 のルールの必要性と重要性を学ぶようになります。

それに加えて、いくつかの実装があるディープコピーとシャローコピーなど、いくつかの新しい概念について説明します。