`new` 演算子とは
`new` または `New` は、
C++などのオブジェクト指向プログラミング言語で、
インスタンス(オブジェクト)を生成するための演算子です。主に、ヒープ領域と呼ばれるメモリ領域から動的にメモリを確保し、
インスタンスを作成する際に使用されます。
`new` 演算子による
インスタンスの生成は、大きく分けて2つのステップがあります。
1.
メモリ領域の確保:
インスタンスを格納するためのメモリ領域をヒープから確保します。この処理は通常、言語の処理系によって行われますが、
C++のようにプログラム内で独自に定義することも可能です。
2.
初期化: 確保したメモリ領域に、
コンストラクタを呼び出すことで
インスタンスを初期化します。
コンストラクタは、クラスの初期設定を行う特別なメソッドであり、プログラム内で自由に定義できます。
基本的な構文
C++や、
C++の影響を受けた言語では、`new` 演算子は概ね次のような構文で使用されます。
cpp
variable = new T;
- - `variable`: 作成されたインスタンスへの参照を保持するポインタ型または参照型の変数です。
- - `T`: 作成されるインスタンスのデータ型を指定します。クラス型の場合は、デフォルトコンストラクタが呼ばれます。基本型(整数型など)や構造体も指定できるかどうかは言語によって異なります。
初期化子の指定
インスタンスを生成する際に、初期化子を指定して初期値を設定することもできます。
cpp
variable = new T(init);
- - `init`: 初期化に使用する値を指定します。クラス型の場合、この値を引数にしてコンストラクタが呼ばれます。
C++では、初期化子を省略して `new T` と書くこともできます。この場合、デフォルト初期化が行われ、基本型などデフォルト
コンストラクタがない型の場合は、値が不定となります。`new T()` は値初期化となり、デフォルト
コンストラクタがない場合は、ゼロまたはゼロ相当の値で初期化されます。
`new` 演算子を使用すると、
配列を作成することもできます。
C++では `new[]` 演算子として区別されますが、
JavaやC#では `new` 演算子の構文の一種として扱われます。
cpp
variable = new T[size];
C++やC#では、次のように初期化子を付与することも可能です。
cpp
variable = new T[size] {init1, init2, ...};
C++では、初期化子の要素数が `size` より小さい場合、残りの要素はゼロまたはデフォルト
コンストラクタで初期化されます。C#では、初期化子の要素数と `size` は一致している必要がありますが、初期化子を指定する場合は `size` を省略できます。
Javaでは、初期化子を指定する場合は `size` を指定できません。
JavaやC#では、
配列は常に `new` 演算子でヒープに作成されます。
配列変数の初期化は、内部的には `new` 演算子を利用した構文糖衣です。
エラー処理
C++では、`new` 演算子でメモリ確保に失敗した場合、標準規格では `std::bad_alloc` 例外がスローされると規定されています。そのため、`new` の結果は常に有効なオブジェクトを指し示すとみなせます。ただし、古いコンパイラや組み込み用途など例外が無効化されている場合は、メモリ確保に失敗すると `NULL` が返されることがあるため注意が必要です。
C#および
Javaでは、`new` 演算子が `null` を返すことはなく、必ず例外がスローされます。
言語ごとの詳細
C++では、`new` 演算子と `new[]` 演算子は、まず対応する演算子関数でメモリを確保し、次に
コンストラクタを呼び出して
インスタンスを初期化します。`new` という名称は、
Simulaの同名の演算子に由来します。
`delete` と `delete[]` 演算子
`new` または `new[]` で動的に確保したメモリは、不要になった際に、それぞれ `delete` または `delete[]` 演算子で解放する必要があります。これは
C言語の `malloc` と `free` に相当しますが、`delete` の場合はメモリ解放前にデストラクタが呼ばれる点が異なります。ヒープに動的に確保されたメモリは自動的に解放されないため、解放し忘れると
メモリリークが発生します。そのため、
RAII(Resource Acquisition Is Initialization)パターンを利用し、デストラクタにメモリ解放を任せることで、例外が発生した場合でも確実に解放できるようにすることが推奨されます。
NULLや
C++11以降のnullptrに対して `delete` または `delete[]` 演算子を使用しても何も起きませんが、`new` で確保したメモリでないポインタに対して `delete` または `delete[]` を使用すると、未定義の動作を引き起こすため注意が必要です。また、動的
配列の
RAIIには、`std::vector` がよく利用されます。
`new` 演算子関数
`new` および `new[]` 演算子によるメモリ確保を制御するために、これらの演算子関数を
多重定義できます。
多重定義された演算子関数は、対応する `new` および `new[]` 演算子で使用されます。`delete` および `delete[]` 演算子のメモリ解放には、対応する `delete` および `delete[]` 演算子関数が使用されます。
クラス内に `new` 演算子関数を定義した場合、そのクラスや派生クラスの `new` 演算子によるメモリ確保に使用されます。クラス内の演算子関数は、明示的に `static` を指定しなくても、静的メンバ関数として扱われます。
`new hoge` という式は、次のように実行されます。
1. `new` 演算子関数の名前探索を行う(例:`hoge::operator new`)。
2. `sizeof(hoge)` の値を引数にして `new` 演算子関数を呼び、メモリを確保する。
3. `new` 演算子関数が返したポインタを `this` ポインタとして、
コンストラクタを呼び出して
インスタンスを生成する。
`new[]` 演算子関数と `new` 演算子関数が分かれているのは、
配列を確保する際に、
配列をオブジェクトとして扱わないという方針によるものです。
`new T[n]` を実行した際、`new[]` 演算子関数には `sizeof(T) * n` より大きい値が渡される可能性があります。これは、`delete[]` で解放する際にデストラクタを呼ぶ回数を記録するためなどといった理由があります。
`new` および `new[]` 演算子関数は、クラス外や
名前空間内にも定義できます。定義されていない場合は、標準
C++ライブラリで定義されたものが使用されます。`::new T` のように `::` を前置すると、グローバル
名前空間の `new` および `new[]` 演算子関数を強制的に使用できます。
初期の
C++では、メモリ確保と初期化が分離しておらず、クラス型の `new` で独自のメモリ確保を行うには、
コンストラクタ内で `this` に代入を行うという構文を使用していました。
デフォルトの `new` 演算子関数
デフォルトの実装では、次の処理をループで実行します。
1. メモリ確保を試みる。
2. 成功したら、確保したメモリを返す。
3. 失敗したら、newハンドラが登録されているか確認する。
4. 登録されていれば、newハンドラを呼び出す。
5. 登録されていなければ、`std::bad_alloc` 例外をスローする。
配置 `new`
配置 `new` (placement new) は、`new` 演算子関数に引数を渡す機能です。特定の
メモリアドレスにオブジェクトを配置するために使用できます。標準ヘッダ `
` には、引数としてポインタを受け取り、それをそのまま返す `new` 演算子関数が定義されており、指定したメモリアドレスにオブジェクトを配置する目的で使用されます。また、メモリ確保に失敗した場合に例外をスローする代わりに `nullptr` を返す `new` 演算子関数も提供されています。これは `std::nothrow` を引数として使用します。
cpp
new (std::nothrow) T;
`delete` および `delete[]` 演算子は、`std::nothrow_t` を引数にとる `new` が返すメモリも解放できます。`new` および `new[]` 演算子で独自のメモリ確保を行う場合は、対応する `delete` および `delete[]` 演算子を用意する必要があります。`new` 演算子でコンストラクタが例外を投げると、コンパイラは対応する `delete` 演算子関数でメモリを解放しようとするためです。
エラー処理
`new` および `new[]` でメモリ確保に失敗した場合、デフォルトでは `std::bad_alloc` 例外がスローされますが、カスタマイズすることも可能です。
newハンドラ
newハンドラは、`new` および `new[]` 演算子関数でメモリ確保に失敗した場合に呼ばれるコールバック関数です。`std::set_new_handler` 関数で登録できます。newハンドラでは、例外をスローしたり、プログラムを終了させたりするほか、メモリを解放して、再度メモリ確保を試行することも可能です。
C++/CLIでは、C++の `new` 演算子の他に、マネージヒープからメモリを確保する `gcnew` 演算子が存在します。`gcnew` には配置構文は存在せず、`new[]` に相当する配列構文もありません。CLI配列の作成には `array` キーワード (`cli::array` 型) を用います。ただし、`gcnew` には配列初期化の構文があります。
cpp
array^ arr = gcnew array { 1, 2, 3 };
C#
C#の `new` 演算子は、インスタンスを生成・初期化するという意味を持ちます。参照型の場合はヒープからメモリを確保しますが、値型(構造体や列挙型)の場合は一時的なインスタンスを初期化するだけです。
csharp
int x = new int();
上記は概念的には、一時的な `int` 型のインスタンスが生成され、それが `x` へコピーされていると扱われます。これは以下のコードと同等です。
csharp
int x = 0;
Javaの `new` 演算子もC#と同様に、オブジェクトの生成と初期化に使用されます。ただしC#とは異なり、プリミティブ型の初期化に `new` 演算子を使用することはできません。
Visual Basicには、COMクラスのインスタンス作成に使用する `New` キーワードが存在します。
vb
Dim obj As New MyClass()
また、変数の宣言と同時にインスタンスを作成し、変数を初期化することもできますが、これらは必ずしも同じ意味を持つとは限りません。
Visual Basic .NETの `New` は、概ねVisual Basicの構文を踏襲しています。しかし、CLIクラスを対象とする点が異なります。
vb.net
Dim obj As New System.Collections.ArrayList()
配列の初期化も可能です。
vb.net
Dim arr As Integer() = New Integer() {1, 2, 3}
配列の宣言と初期化はまとめて実行することもでき、この場合は `New` を使用する必要はありません。
vb.net
Dim arr As Integer() = {1, 2, 3}
配列を宣言と同時に割り当てる際に、インデックスの最大有効値を指定することもできます。この場合も `New` は使用しませんが、配列オブジェクトの割り当ては実行されます。
vb.net
Dim arr(2) As Integer
`Option Strict` が有効な場合は変数に型指定が必要ですが、Visual Basic 9.0 (2008) 以降は `Option Infer` が有効な場合、型推論によって右辺の `New` 句から変数の型を決定できます。
他の手法
オブジェクト指向言語では、オブジェクトを生成する手段は `new` 演算子に限定されるものではありません。例えばC++では、参照やポインタとしてでなく宣言されたオブジェクト型の変数は、暗黙のうちにオブジェクトを生成し、初期化されます。Objective-CやRubyのように、オブジェクトの生成をクラスメソッドで行う言語や、ファクトリメソッドに落とし込んで継承により上書き可能とする手法もあります。