CとC++の動的メモリー管理(1)、malloc関数とnew演算子の違いを知る
C言語とC++言語では、動的にメモリーを確保したり解放したりする手法は異なり、それぞれ長所と短所がある。もちろん、安全性を最重要視する組み込み機器では、動的に確保したメモリーを利用すべきでないという考え方には心から賛同する。リスクが利点を上回ってしまうからだ。しかし場合によっては、動的なメモリーを適切に管理することで改善できることも多いのではないかとも考えている。
C言語やC++言語に標準的に用意されているメモリー管理向け関数の動作が意図した通りでない場合は、メモリー管理関数を独自に開発するとよい。独自のメモリー管理関数の仕様と振る舞いは、できる限り標準関数と同じにすることが理想である。標準関数のメモリー管理方法と違ってしまう場合でも、関数の引数と返り値の数と型は、可能な限り標準関数にそろえるべきである。関数の仕様を同一にしておくことで、最初は標準のメモリー管理関数を使って開発し、必要に応じて独自のメモリー管理関数を使うように改変したり、場合によっては標準のメモリー管理関数を使うように戻したりすることが容易になる。
動的なメモリー管理の方法を知っておくことは、C++開発者にとって大変価値がある。C++は、メモリー・リークの発生を減少させる手段を数多く提供している。その代表的なものが、コンストラクタとデストラクタを備えるクラスである。既存のソース・コードに専用のメモリー管理機構を簡単に追加でき、メモリー・マップドI/Oを用いたデバイス・レジスタを表現するオブジェクトを配置する際にさえ、専用のメモリー管理関数と入れ替えて使うことができる。
本稿は、C言語とC++言語が標準的に備えているメモリー管理手法を比較する。C/C++のどちらを使うかにかかわらず、これらの違いを理解することは有用である。
C言語のメモリー管理
標準Cでは、2つのメモリー確保関数(mallocとcalloc)と、1つのメモリー解放関数(free)、そして確保済みメモリー領域の大きさを変更する(メモリー確保と解放を同時に行う)realloc関数を提供している。これら4つの関数は、すべて標準ヘッダ-・ファイル「stdlib.h」内でプロトタイプ宣言されている。
メモリー確保関数(mallocやcalloc、realloc)で確保した領域は、その領域のポインタ(先頭アドレス)をメモリー解放関数(freeあるいはrealloc)に渡すまで、確保されたまま残る。未確保の領域を解放しようとした場合の動作は未定義である。
malloc関数は、以下のように宣言されている。
void *malloc(size_t size);
「malloc(s)」のように呼び出すと、malloc関数はsバイトの大きさのメモリーをヒープ領域から確保しようとする。メモリーの確保に成功すると、そのメモリー領域のポインタを返す。それ以外の場合は、ヌル・ポインタが返る。
引数sizeの型はsize_tだ。size_tは、「stdlib.h」などいくつかのヘッダ-・ファイルの中で定義(typedef)されている型である。この型は、いくつかの符号なし整数型の別名となっている。主な例としてunsigned int型やunsigned long型が挙げられるが、unsigned long long型が用いられることもある。標準Cでは、size_tを、ターゲットとするプラットフォーム上で生じ得る最大のオブジェクト(変数)の大きさを表現できるよう十分なバイト数があり、しかも必要以上に大き過ぎない符号なし整数型にするよう定めている。
malloc関数の典型的な呼び出しでは、引数にsizeof式を与える(sizeof式はsize_t型の値を返す)。例えば変数pが、 widget型の値へのポインタであるとき、次のように記述すると、widget型の値を格納するメモリー領域を動的に確保し、そのポインタをpに代入する。
p = malloc(sizeof(widget));
また次のような文では、widget型の配列(要素数10)を格納するメモリー領域を動的に確保し、そのポインタをpに代入する。
p = malloc(10 * sizeof(widget));
一方、calloc関数は、malloc関数の代わりに使える関数で、以下のように宣言されている。
void *calloc(size_t nmemb, size_t size);
「calloc(n, s)」のように呼び出すと、要素数nの配列に必要なメモリーをヒープ領域から確保する。各要素の大きさはsバイトである。malloc関数と同様、メモリーの確保に成功すると、そのメモリー領域へのポインタを返す。それ以外の場合は、ヌル・ポインタを返す。
従って、10個のwidgetからなる配列を確保するには、以下の2つの方法があることになる。
p = malloc(10 * sizeof(widget));
p = calloc(10, sizeof(widget));
これら2つの関数の本質的な違いは、確保したメモリー領域に格納される値だ。malloc関数で確保した領域の値は不定である。一般に、malloc関数を呼び出す前にそのメモリー領域に格納されていた値がそのまま残る。それに対してcalloc関数では、確保したメモリー領域の全ビットがゼロにクリアされる。
realloc関数は、前述の通り、malloc関数やcalloc関数で確保したメモリー領域の大きさを変更するときに使う。以下のように宣言されている。
void *realloc(void *ptr, site_t size);
「realloc(p, s)」の結果は、pがヌル・ポインタの場合、「malloc(s)」と同じである。pがヌル・ポインタでない場合、新しいメモリー領域(大きさはs)を確保し、pが示すメモリー領域を解放した上で、新領域のポインタを返す。realloc関数を呼び出す前にpが示すメモリー領域に格納されていた内容は、新しいメモリー領域に引き継がれる。ただし引き継げるのは、新旧メモリー領域の、どちらか小さい方の領域の大きさに相当する内容だけである。新領域が旧領域よりも大きい場合、新しい領域の、古い領域を超えた部分の値は不定である。ほかのメモリー確保用関数と同様、新しいメモリー領域の確保に成功すると、そのメモリー領域へのポインタを返す。それ以外の場合は、古いメモリー領域を解放することなくヌル・ポインタを返す。
size_tが常に符号なし(unsigned)型なので、malloc/calloc関数は、確保するメモリー領域の大きさを指定する引数を負の数ではないものとして解釈する。ただし、引数としてゼロを与えることはできる。
p = malloc(n * sizeof(widget));
例えば、上記の文でnがゼロだった場合、malloc関数は要素数がゼロの配列を確保しようとする。返り値がヌル・ポインタであるか、ヌルでないポインタであるかは、処理系(コンパイラ)に依存する。どちらの場合も、返り値のポインタが示すメモリー領域に対して解放処理を実行しようとした場合、その結果は未定義である。
free関数は、以下のように宣言されている。
void free(void *ptr);
「free(p)」と呼び出したときのpがヌル・ポインタの場合、free関数は何もしない。それ以外の場合は、pが示すメモリー領域を解放する。
C++のメモリー管理
Cのメモリー管理関数は、C++でも利用可能である。しかし、メモリー管理関数を使っているCのコードがすべてC++コードになり得るわけではない。例えば、以下の典型的なCの用法を見てみよう。変数pはwidget型へのポインタである。
p = (widget *)malloc(sizeof(widget));
次のような「新方式」のキャストを利用すればより良いだろう。
p = static_cast< widget *> (malloc(sizeof(widget)));
さらに推奨するのは、動的メモリー確保用のnew演算子の利用である。new演算子では、メモリー確保時にキャストする必要がない。
p = new widget;
例えば上記のようなnew演算子を使った式は、widgetオブジェクトを格納するのに必要なメモリー領域を確保し、その領域のポインタをwidget*型として返す。もし変数pが、widget*型であれば、この代入文はコンパイル時にエラーにはならない。
前述した通り、C言語で要素数nのT型配列を格納するのに必要なメモリー領域を動的に確保するには、以下のどちらかの文を記述する。
p = malloc(n * sizeof(T));
p = calloc(n, sizeof(T));
C++では、「new T[n]」といった「配列のnew演算子」を使って、配列を格納するメモリー領域を動的に確保できる。このときのnew演算子は、確保した配列の先頭要素を指すT*型のポインタを返す。
「new T」と「new T[n]」の両者が、T*型の値を返すことに注意したい。C同様C++でも、T型オブジェクトを指し示すポインタは単独のT型オブジェクトを指すこともあるし、T型オブジェクトの配列の最初の要素を指すこともあるのだ。
malloc/calloc/realloc関数によって確保した領域は、free関数を使って解放する。それに対して、new演算子で割り当てた領域は、次のようにdelete演算子を使って解放する。
p = new T;
...
delete p;
例えば上記のdelete演算子は、T型の単独オブジェクトを解放する。
C++では、配列を解放するdelete演算子が用意されている。「配列のdelete演算子」は以下のように、キーワードdeleteの後ろに空の角括弧を記述する。この配列のdelete演算子は、pが指す配列を解放する。
p = new T[n];
...
delete [] p;
free関数と同様、ヌル・ポインタに対してdeleteを実行しても無害である。この性質は、delete演算子と、配列のdelete演算子の双方に共通である。
ただし、単独オブジェクトを配列でないnew演算子で確保した場合、以下のように配列のdelete演算子で解放してはいけない。同様に、配列の new演算子によって確保した配列は、配列でないdelete演算子で解放してはいけない。delete演算子を誤って記述した場合の、delete演算子の結果は未定義である。ほかの未定義の場合の振る舞いと同様、そのプログラムは開発者が望んだ通りに動くかもしれないが、当てにはできない。
p1 = new T;
p2 = new T[n];
...
delete [] p1; //誤り
delete p2; //誤り
C++コンパイラが、これらのエラーを検出できないことはよくある。T型へのポインタが単独のTオブジェクトを指すこともあるし、Tオブジェクトの配列の先頭要素を指すこともある。new演算子とdelete演算子がそれぞれ別のスコープに存在する場合、コンパイラはdelete演算子のオペランドを見て、それが単独のオブジェクトを指しているのか、配列の要素を指しているのかを判別できないからだ。ポインタが何を指しているのかは、[]の有無によってのみ示されている。
一般に、C++はCに比べて厳しく型をチェックしている。これは、より多くのエラーをコンパイル時に検出するためである。しかし、Cでは起き得ないエラーがC++で生じるケースがある。
次の2つのことが重要である。(1)C++では、各delete演算子に適切に[]を付けて正しい形式で記述するのは、開発者の責任である。(2)Cでは、ポインタpが単独オブジェクトと配列のどちらを指していたとしても、開発者はfree(p)を呼び出せばよい。
もしC++が本当にCよりも型の安全性を重要視しているのなら、単独オブジェクトであろうと配列オブジェクトであろうと、1つのdelete演算子だけで対応するようにすればよいのではないだろうか。C++は一般に型の安全性を重視しているが、この場合はメモリー確保処理の速度を向上させるために、型の安全性については多少目をつぶっている。
確保したオブジェクトの初期化
C++では、クラスのコンストラクタは特別なメンバー関数である。コンストラクタは、そのクラスに属するオブジェクトを初期化する役割を担う。以下のように、コンストラクタの関数名は、そのクラス名と常に同一であるとC++では規定されている。
class widget
{
public:
widget(); //コンストラクタ
...
};
コンストラクタは、保証された方法でクラス・オブジェクトを初期化する。コンストラクタを呼び出すコードはコンパイラによって自動生成されるので、開発者は明示的にそのためのコードを記述する必要はない。
保証付き初期化を真に保証付きであるようにするためには、ソース・コード上でnew演算子を使ってクラス・オブジェクトを作成するコードが記述されたときに、コンパイラは適切なコンストラクタ呼び出しを必ず生成しなければならない。
p = new widget;
widgetをクラス型とした場合、上記のようなnew演算子を使った式は、単にwidgetに必要なメモリー領域を動的に確保するだけではない。自動的にコンストラクタが呼び出され、適切にコンストラクトされたwidgetオブジェクトが生成される。
CとC++は基本的に異なる方法で動的にメモリーを確保する。malloc関数で確保した領域の値は不定になるが、new演算子で確保した領域は適切に初期化される(コンストラクタに適切な初期化処理を記述すれば)。
calloc関数をmalloc関数の代わりに用いたらどうだろうか。calloc関数はオブジェクトをゼロで埋めるので、初期化していることにならないだろうか。確かにいくつかの型については、すべてのビットをゼロにすると合理的で便利な初期値となる。しかし多くのクラス型ではそうはいかない。実際、Cの仕様に特記されているように、すべてのビットがゼロであっても、浮動小数点数のゼロやヌル・ポインタは正しく表せない。
C++のクラスでは、デストラクタと呼ばれる特別なメンバー関数を使って、動的に確保されたメモリー領域を自動的に解放する。デストラクタの名前は、コンストラクタの名前の前に「~」を付けることになっている。new演算子がコンストラクタを呼び出すように、delete演算子はデストラクタを呼び出す。
class widget
{
public:
widget(); //コンストラクタ
~widget(); //デストラクタ
...
};
PR











