「あーもう。やんなっちゃう」
「お?どうした。ご機嫌斜めじゃないか」
「このループ,トレースかけて見てる限り,うまく動くんですよ。でも,1000回も待てないのでループ抜けたときに見るとダメなんですよね」
「ふーん。いかにもメモリ関係のバグっぽいな。リークしてんじゃない?」
「メモリ周り,ですかぁ。一番見つけるのがやっかいじゃないですか。でも何で,こんなことが起こるんでしょうね」
「そりゃ,C/C++は裸のポインタが見えてるからね」

 メモリ・リークという言葉が多く聞かれるようになってきたのは,WindowsプログラムをC++で作るのが普通になってからではないだろうか。それまでにもCプログラムにおけるメモリ関連のエラーは頻繁に起こっており,それに対処するツールはいくつも販売されていた。しかしこれらの場合,メモリ・リークよりもメモリ領域の境界チェックの方が重視されていた。C++でメモリ・リークが問題視されるようになってきたのは,C++の言語体系に本質的な問題があるからだ。かなり気を付けていないとメモリ・リークは発生してしまうのである。

C++は変数の利用終了を宣言する

 一般にプログラミング言語において,変数の有効無効を管理するのはプログラマの役目ではない。例えばC言語においても,サブルーチンや関数などで手続きが終了すると,自動的にローカル変数の領域は解放される。初期のBASICのように,すべてが大域変数(グローバル変数)であれば,一度作った変数はプログラムが終了するまで「生き続けた」ままである。プログラムの実行開始を指示する(RUN命令)ときや,プログラムに改変があったときに,変数領域を一気に初期化するのである。

リスト1●C++ではオブジェクトを明示的に消去する。
他のプログラミング言語には見られない不便な点である。これがメモリ・リークの主たる原因

 唯一の例外と言っていいのが,C++である。C++の場合,オブジェクトを代入した変数をプログラマが明示的に解放しなければならないことが頻繁に発生する。C++でオブジェクト指向らしいプログラムを作ると,必然的に「参照型」と呼ばれる変数を使わなければならない。この変数を使う場合,プログラマが明示的に解放を指示しなければならないのである(リスト1[拡大表示])。

 なぜこのような解放の指示が必要になったかというと,C++がC言語のプリプロセサによる拡張仕様として成立したからだ。Cには存在しない「クラス」を実装するために,Cが備えていた構造体(struct)を拡張した。ところがCは,静的にすべてをリンクしてしまう言語である。したがってオブジェクト指向の一つの特性である多義性(polymorphism)を実現するには,基本的に動的に情報を入れ替えることのできるポインタを使う必要がある。しかしポインタそのものではオブジェクトとして取り扱うときに不具合が出る。例えば型の一貫性に関する情報が失われてしまう。そこで参照型と呼ぶ,特殊なポインタを導入したのである。

 つまり参照型は見かけはともかく,中身はCのポインタなのである。したがって参照型のオブジェクトを入れる器を宣言しても,そのために確保されるデータ領域はあくまでもポインタのための領域だけである。実際のオブジェクトのデータは,別の領域に取られる注1)。

ヒープ領域とスタック領域がある

 オブジェクトの実データは,「ヒープ」と呼ばれる領域に確保される。これに対し,ポインタや整数などの単純型の変数は,「スタック」と呼ぶ領域を使う。元々ヒープ領域は,C言語のときから存在していた。動的に大きさの変化する配列などの領域を確保するためのエリアとして,通常の変数とは別に使われていたものだ(図1[拡大表示])。ヒープ(heap)という言葉は一山,あるいは一固りといった意味であり,スタックに比べ大きなデータを扱う場合に利用する領域でもある。C言語では,「malloc」と「free」という関数を使ってヒープにある領域の確保/解放をする(リスト2[拡大表示])。

図1●ヒープとスタック。
一般的な変数はスタックにその領域が確保される。これに対しC++のオブジェクトはたいていの場合,ヒープと呼ぶ別の場所に実体が置かれる。スタックに存在するのは,その実体を指し示すポインタだけである
 
リスト2●C言語でヒープを使う。
C++のオブジェクト同様,領域の確保/解放はプログラマが責任を持って実行しなければならない

 C++ではオブジェクトの実体を作成する方法が2通りある。一つが変数宣言などと同じように,オブジェクトを静的に宣言する方法。もう一つが「new」演算子を使って動的にオブジェクトを生成する方法である。前者の場合,オブジェクトの実体はスタック領域に置かれる。これに対しnew演算子を使う場合は,スタック領域にそのオブジェクトへのポインタ(または参照型の変数),ヒープ領域にその実体が確保される。C++がCのプリプロセサとして実装されていたときは,これらがmallocやfreeといった関数に置き換えられていた。

 C/C++では,スタックに取られたデータについては,変数が有効な範囲(スコープ)からはずれた時点で自動的に解放される。これに対して,ヒープはプログラマが明示的に解放する必要がある。もともとのヒープの用途からすれば,ある種特殊な領域であり,それほど領域確保/解放が繰り返されるものではなかった。その意味ではプログラマが明示的に解放することも,それほどの負荷ではなかった。プログラムによってはヒープ領域をまったく使わないことも珍しくない。しかしC++はオブジェクト指向言語であり,オブジェクトの生成は必然である。だからたくさんのオブジェクトがヒープ領域に取られる。一方プログラマは,確保した領域を解放することは意識していても,変数の管理はコンパイラ任せにしていた面がある。最初からC++でプログラミングを習得したのならともかく,他の言語ではこのような管理は不要だからだ。このことが,多くのメモリ・リークを生んだ原因ではないだろうか。

(北郷 達郎、八木 玲子)