ikautak.log

C/C++, Python, CUDA, Android, Linux kernel, Network, etc.

More Effective C++ 項目8 newとdeleteの別の意味を理解する

新訂版 More Effective C++ (AddisonーWesley professional co)

新訂版 More Effective C++ (AddisonーWesley professional co)

  • 作者: スコット・メイヤーズ,安村通晃,伊賀聡一郎,飯田朱美,永田周一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2007/06/29
  • メディア: 単行本(ソフトカバー)
  • 購入: 8人 クリック: 129回
  • この商品を含むブログ (43件) を見る

項目8 newとdeleteの別の意味を理解する

new演算子とoperator newは違う。以下のコードはnew演算子を使っている。

string *ps = new String("Memory Management");

new演算子はsizeofと同様、その意味を変えることはできず、2つの意味を持っている。

  1. 要求された型のオブジェクトを保持するのに十分なメモリを確保する
  2. コンストラクタを呼び出し、メモリの中のオブジェクトを初期化する

new演算子は1.のメモリを割り当てるためにある関数を呼ぶが、その関数の名前がoperator newらしい。
operator newをオーバーロードして、いかにメモリを割り当てるか、だけなら変更できる。

void* operator new(size_t size); // 宣言
...
void* rawMemory = operator new(sizeof(string)); // 直接呼ぶ場合

このoperator newは文字列オブジェクトを保持するのに十分なメモリの塊へのポインタを返す。
コンストラクタは呼ばれない。

new演算子コンストラクタも呼ぶので、

string *ps = new String("Memory Management");

↑のnew演算子は、コンパイラによって以下のようなコードになる。

void* memory = operator new(sizeof(string)); // メモリを確保するだけ
string::string("Memory Management");         // コンストラクタを呼ぶ
string *ps = static_cast<string*>(memory);   // psがオブジェクトを指し示す

配置new(placement new)

既に割り当てられた素のメモリにオブジェクトを作るため、コンストラクタだけを呼びたいときにplacement newを使う。

class Widget {
public:
    Widget(int widgetSize);
    ...
};

Widget* constructWidget(void* buffer, int widgetSize) {
    return new (buffer) Widget(widgetSize);
}

この関数はWidgetオブジェクトへのポインタを返すが、そのオブジェクトはこの関数に渡されたbufferの中に作られる。
共有メモリやメモリマップされた入出力を用いたアプリケーションによっては有効。

このときのoperator newは、size_tの引数に加えて、オブジェクトが置かれるメモリを指し示したvoid*も受け取る。

void* operator new(size_t, void* location) {
    return location;
}

しかし、メモリは既に用意されており、operator newの仕事はないので、locationを返すだけとなる。

配置newのまとめ
ヒープ上にオブジェクトを作るときはnew演算子を使う。
ヒープ上にオブジェクトを作るときのメモリ割り当てをカスタマイズするときはoperator newを書いて、new演算子を使う。
既にあるメモリの中にオブジェクトを作るときは、配置new(placement new)を使う。


削除とメモリの開放

new演算子にoperator newがあるように、delete演算子にはoperator deleteがある。

string *ps;
...
delete ps;

と書くと、コンパイラは以下のようなコードを生成する。

ps->~string();       // デストラクタを呼ぶ
operator delete(ps); // メモリを解放する

operator newでメモリを確保したら、operator deleteで解放する。

void* buffer = operator new(50*sizeof(char));
...
operator delete(buffer);

配置newを使ってメモリ中にオブジェクトを生成する場合は、そのメモリに関してはdelete演算子を使ってはいけない。
delete演算子は、operator deleteを使ってメモリを解放するが、そのオブジェクトを含むメモリはoperator newで確保されたものではないからだ。

配置newではコンストラクタだけ実行されるので、デストラクタだけを明示的に呼ぶ。

// 共有メモリの割り当て、解放の関数
void* mallcoShared(size_t size);
void freeShared(void* memory);

void* sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = constructWidget(sharedMemory, 10); // 配置newを使う
...
delete pw;      // NG! operator newで確保されたメモリではない!

pw->~Widget();  // OK  デストラクタを呼ぶだけで、メモリ解放はしない。
freeShared(pw); // OK  メモリは解放するが、デストラクタは呼ばれない。

配置newで渡された素のメモリ(↑だとsharedMemory)が動的に割り当てられたものであれば、解放処理が必要だ。


配列

ここまでは、1つのオブジェクトの生成の話。

string *ps = new string[10];

このように配列を確保するとき、new演算子が使われるが、メモリの割当にはoperator newは使われず、operator new[]が使われる。

普通のopeator newとは、コンストラクタの呼び出し回数が違ってくる。

string *ps = new string[10]; // 10文字列オブジェクト分のメモリを割り当て、
                             // 各要素に対してコンストラクタを呼ぶ。


同様にdelete演算子を使った場合、各要素に対してデストラクタが呼ばれ、operator delete[]が呼ばれてメモリを解放する。

delete[] ps; // 各要素に対してデストラクタを呼び、メモリを解放

operator deleteを置き換えたり、オーバーロードしたりするのと同様に、operator delete[]も置き換えたり、オーバーロードしたりできる。

まとめ
new演算子とdelete演算子は組み込み型であり、制御の範囲外。
メモリ割り当てと解放の関数はカスタマイズできる。
カスタマイズするにも、「どのように」はできるが、何をするかは言語で定められている。

項目8は長かった。