July 03, 2017

[c++] malloc したメモリを初期化する

この記事は、IT エンジニア向けです。

滅多に使わないので忘れるから、個人メモ。

c 言語ではメモリを確保する時に malloc を使う。 c++ では、同様に malloc を使う事も出来るが、普通は new を使う。

malloc はメモリを確保するだけだが、new はメモリ確保の後、確保したメモリの初期化まで行ってくれるからだ。

では問題。 c++ で、あえてメモリ確保に malloc を使用した場合、どうやって初期化すればよいか?

一般的に、メモリ確保処理は負荷が高い。 なので、c や c++ では一度に大量のメモリ確保を行う事がある。 インスタンスを 100 個作成する時、100 回メモリ確保をするより、1 回で100 個分のメモリ確保をした方がパフォーマンス的には良いのだ。

ただし、この時に new を使えない事が多々ある。 new でも複数インスタンスのメモリを一度に確保する事は可能なのだが、この場合コンストラクタに渡す引数がややこしい。 「引数無しのコンストラクタで初期化しておいて、後で各種設定を行う」で済む場合は良いのだが、そうとは限らない。

こんな場合は、初期化を行わない malloc の方が都合が良かったりする。 そうすれば、malloc でメモリ確保だけをしておいて、実際に使う直前にコンストラクタを呼び、初期化すれば良いのだから。

というわけで、malloc で確保したメモリに後からコンストラクタを走らせる方法のメモ。

#include <cstdlib>
#include <iostream>
#include <new>

using namespace std;

class Foo {
public:
  Foo(const char *msg) : msg_(msg) {}

  void hello() { cout << msg_ << endl; }

  ~Foo() {
    cout << "~";
    hello();
  }

private:
  const char *msg_;
};

void new_foo() {

  auto ptr = new Foo("new");
  ptr->hello();
  delete (ptr);
}

void malloc_foo() {

  auto ptr = malloc(sizeof(Foo));
  if (!ptr) {
    auto handler = get_new_handler();
    if (!handler)
      throw bad_alloc();
    else
      handler();
  }

  new (ptr) Foo("malloc");
  static_cast<Foo *>(ptr)->hello();
  static_cast<Foo *>(ptr)->~Foo();

  free(ptr);
}

int main() {

  new_foo();
  malloc_foo();

  return 0;
}

上記において、new_foo と malloc_foo は、ほぼ同様の働きをする。

new_foo は普通に new を使う方法だ。 new Foo ("new") で Foo クラスのインスタンスのメモリ確保、初期化を行い、メソッド hello を呼び出し、delete でメモリを解放している。

malloc_foo は、あえて malloc を使う方法。 最初にエラー処理以外の部分を見てみよう。

auto ptr = malloc(sizeof(Foo)) で Foo に必要なサイズのメモリを確保する。 その後、new (ptr) Foo("malloc") とすると、ptr に確保されたメモリ上で初期化を走らせる。 この時、コンストラクタには "malloc" という const char * の引数を渡している。

その後 new_foo と同様にメソッド hello を呼び出す。

最後にメモリ解放をするのだが、malloc で確保したメモリは delete ではなく free で解放する必要がある。 free でメモリを解放する場合は明示的にデストラクタを呼ぶ必要があるので注意だ。

ついでなのでエラー処理についてもちょっと書いておく。 new にしろ malloc にしろ、メモリ確保は必ず成功するとは限らない。エラーが発生する可能性がある。

new の場合、デフォルトでは失敗時に bad_alloc クラスの例外が投げられる。 ただし、この処理は set_new_handler という関数で変更可能だ。 関数 new_foo では、この挙動に任せており特にエラー処理はしていない。

malloc の場合、失敗時は必ず null pointer が返される。 malloc_foo では、失敗時の挙動を new と同じになるようにしている。

つまり、返り値が null pointer の場合、まず set_new_handler で new のエラー処理が変更されているか確認する。 もし変更されていなければ、new のデフォルト動作である bad_alloc クラスの例外を投げる。 変更されている場合は、その関数を呼び出す。

以上、完全な俺メモでした。