C++のデストラクタの利用法

皆さん、こんにちは。技術開発グループのn-ozawanです。
新年あけましておめでとうございます。本年もよろしくお願いいたします。

本題です。
今年も新年あけて早々に実用性0のネタ会です。C++から離れて20年近く経ちます。最近はJavaやTypeScriptに触れる機会が多いせいか、デストラクタの存在をすっかり忘れています。今回はそんなデストラクタを思い出しながら、昔はこういうのに使ってたなと思い出に浸ります。

デストラクタ

デストラクタとは?

デストラクタは、オブジェクト指向言語でそのオブジェクトを削除する際に呼び出される関数です。C/C++はメモリを確保したら、きちんと解放してあげる必要があります。そのため、デストラクタでは、その確保したメモリを解放するなどの後片付けを行います。

Javaもオブジェクト指向言語ですが、ガベージコレクタにてメモリを自動開放する仕組みがあるため、デストラクタは存在しません。その代わりファイナライザという、メモリが解放される直前によばれる処理は存在します。デストラクタはメモリを自分で管理するような言語によく見られる仕組みです。

以下は、デストラクタの動きを説明したサンプルコードです。

class Example {
public:
  Example() {
    printf("This is the constructor.\n");
  }
  ~Example() {
    printf("This is the destructor.\n");
  }
};

// メイン処理
int main() {
  {
    Example example;
    printf("before\n");
  }
  printf("after\n");
  return 0;
}
This is the constructor.
before
This is the destructor.
after

Example exampleでコンストラクタが処理されます。このローカル変数exampleのスコープは{...}で囲ったところまでで、{...}から外れるとローカル変数は解放されます。つまり、afterが呼ばれる前にデストラクタが処理されます。

デストラクタによるメモリの自動開放

そんなデストラクタを利用して、メモリの自動開放を実装してみます。先ほども述べた通り、C言語では確保したメモリは、責任もって開放してあげる必要があります。しかし、メモリの解放忘れは往々にしてよくあります。出来ればメモリの解放は自動でやって欲しいですよね。ということで、デストラクタでメモリを解放してみました。

template <class T>
class MemPointer {
public:
  T* ptr;

  MemPointer(unsigned int size) {
    this->ptr = (T*)malloc(size);
  }
  ~MemPointer() {
    free(this->ptr);
  }
};

クラスMemPointerは、コンストラクタで確保したメモリを、デストラクタで解放します。このクラスMemPointerを共通クラスとして定義し、みんながこのクラスでメモリを確保すれば、解放忘れということは無くなるでしょう。クラスMemPointerは以下のように使います。

typedef struct _Person {
  char name[256];
} Person;

// メイン処理
int main() {

  {
    MemPointer<Person> person(sizeof(Person));
    memcpy(person.ptr->name, "n-ozawan", sizeof(Person::name));
    printf("My name is %s\n", person.ptr->name);
  } // ここでメモリが解放される

  return 0;
}

ただ、C++11からスマートポインタという仕様が導入されていますので、上記のようなやり方はやめましょう。素直にstd::shared_ptrを使った方が良いです。

typedef struct _Person {
  char name[256];
} Person;

int main(){

  {
    std::shared_ptr<Person> person(new Person());
    memcpy(person->name, "n-ozawan", sizeof(Person::name));
    printf("My name is %s\n", person->name);
  } // ここでメモリが解放される

   return 0;
}

デストラクタでトレースログ

その関数の始まりと終わりにトレースログを出力する場合もデストラクタは便利です。

class LogTrace {
private:
  const char* message;

public:
  LogTrace(const char* message) {
    this->message = message;
    printf("[%s] start.\n", this->message);
  }
  ~LogTrace() {
    printf("[%s] end.\n", this->message);
  }
};

クラスLogTraceはコンストラクタでstartを、デストラクタでendのログを出力しています。クラスLogTraceは以下のように使います。

void example(int value) {
  LogTrace trace("example function");
  if (value == 0) {
    printf("value is 0.\n");
    return ;
  }
  return ;
} 

// メイン処理
int main() {
  example(1);
  example(0);
  return 0;
}
[example function] start.
[example function] end.
[example function] start.
value is 0.
[example function] end.

トレースしたい関数の冒頭にLogTrace trace("example function");を書くだけです。if分岐で途中終了しても、問題なくendログが出力されています。

おわりに

C/C++を投稿する際、いろんなサイトで勉強しなおすのですが、私がC/C++から離れている間に随分いろんな機能が追加されていますね。私が書いているコードは古臭いかもしれません。

ではまた。

Recommendおすすめブログ