1. ホーム
  2. c++

C++で全てにポインターを使わない理由とは?

2023-08-27 04:08:24

疑問点

あるクラスを定義したとします。

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

そして、それを使っていくつかのコードを書いてください。なぜ次のようなことをするのでしょうか?

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

Javaの世界から来た私はいつも書いています。

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

基本的に同じことをするんですよね? 一方はスタック上にあり、もう一方はヒープ上にあるので、後で削除する必要がありますね。この 2 つの間に何か根本的な違いがあるのでしょうか?なぜ、どちらかを選ぶべきなのでしょうか?

どのように解決するのですか?

はい、一方はスタック上にあり、もう一方はヒープ上にあります。2 つの重要な違いがあります。

  • まず、明白ですが、あまり重要ではないもの。ヒープの割り当ては遅いです。スタックアロケーションは高速です。
  • 第二に、より重要なのは RAII . スタックに確保されたバージョンは自動的にクリーンアップされるため 有用 . そのデストラクタは自動的に呼び出されるので、クラスによって割り当てられたすべてのリソースがクリーンアップされることを保証することができます。これは、C++でメモリリークを回避するための基本的な方法です。メモリリークを避けるには、決して delete を自分で呼ばず、スタックに割り当てられたオブジェクトでラップし、そのオブジェクトが delete を内部的に、典型的にはそのデストラクタの中で呼び出します。もし、手動ですべての割り当てを追跡して delete を適切なタイミングで呼び出そうとすると、少なくとも 100 行のコードにつき 1 つのメモリ リークが発生することが保証されます。

小さな例として、次のコードを考えてみましょう。

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

かなり無難なコードでしょう?ピクセルを作成し、無関係な関数を呼び出し、そしてそのピクセルを削除しています。メモリ リークがあるのでしょうか?

答えは、「おそらく」です。次のような場合はどうなりますか? bar が例外を投げたらどうなるでしょうか? delete が呼び出されることはなく、ピクセルは削除されず、メモリがリークします。次に、これを考えてみましょう。

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

  bar();
}

これならメモリリークしませんね。もちろんこの単純なケースでは、すべてがスタック上にあるので、自動的にクリーンアップされますが、仮に Pixel クラスが内部で動的な割り当てを行っていたとしても、それもリークしません。そのため Pixel クラスは単にそれを削除するデストラクタを与えられ、このデストラクタは私たちがどのように foo 関数からどのように離れても、このデストラクタは呼び出されます。たとえ bar は例外を投げました。次の、少し工夫した例はこれを示しています。

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

Pixel クラスは内部でヒープメモリを確保していますが、デストラクタがそれを掃除してくれます。 を使用して を使うときに、それを気にする必要はありません。(ここで最後の例は、一般原則を示すためにかなり単純化されていることを述べておく必要があるでしょう。このクラスを実際に使用する場合、いくつかのエラーの可能性も含んでいます。y の割り当てに失敗すると x は解放されませんし、Pixel がコピーされると、両方のインスタンスが同じデータを削除しようとすることになります。ですから、この最後の例は大目に見てください。実際のコードは少しトリッキーですが、一般的な考えを示しています)

もちろん、同じ手法をメモリ割り当て以外のリソースに拡張することも可能です。たとえば、ファイルまたはデータベース接続が使用後に閉じられること、あるいはスレッドコードの同期ロックが解放されることを保証するために使用することができます。