ポインタの話 -- part 3

ポインタを使わないと出来ない事

このページではC言語ではポインタを使わないと実現できない事をまとめておきます。逆に言うと、ここに書いてある事以外でポインタを使わなければならない場面というのは少ない筈です。

動的にメモリを取得する場合

 malloc()を使って動的にメモリを割り当てる時は、C言語の場合はポインタ変数で受けるしかありません。

int	n_hoge;
struct HogeHoge *hoge;

n_hoge = GetHogeCount();
hoge = (struct HogeHoge *)malloc( sizeof(struct HogeHoge) * n_hoge );

といった感じで、動かしてみるまでは幾つ要素を確保すれば分らないような時は動的にメモリを取得する必要があります。

 この場合、返されるポインタがNULLでない事をチェックする事には誰しも注意するのですが、malloc()系の関数を使う時は、実はfreeを確実に行なう事の方が重要だと私は思います。

 大抵のmalloc()/free()の実装は、システムから取得した大きな領域の中をmalloc()される度に分割してプログラムに渡し、残った領域とfree()で戻された領域を「未使用領域」としてリンクリストでつないでおくようになっています。次にmalloc()された時はこのリンクされている未使用領域の中から要求を満たす大きさのものを探して、そこから切り出す事になるのですが、このリンクが壊れていると飛んでもない場所を切り出した積もりになって、プログラムに渡すような事が起こります。

 調査した訳ではありませんが、このリンクが壊れる原因で多分一番多いのはプログラムがmalloc()で渡された領域の外を書き換えてしまう事で、その次に多いのがfree()に間違ったポインタを渡す事ではないかと思います。そして良くあるのが、同じポインタを2回以上渡してfree()する、二重解放です。malloc()とfree()が別々の関数で呼び出されていたりしてしっかり管理されていないと、こうなりがちです。

 この場合、未使用領域のリンクが壊れますが間違えてfree()した時にはまだエラーは起こりません。次にmalloc()した時、きっと飛んでもない場所を渡されたプログラムが*正しく*ロジックを実行しているうちに、挙動がおかしくなります。一般的に大変に原因追及が難しいタイプのバグとなります。

 こういう事にならないようにする為の簡単な習慣は、free()を直接呼び出すのはやめ、下記のような関数を使うなどして、二重解放が起きないようにポインタの値をチェックすることです。少なくとも、解放してしまったポインタの値をそのままにはしないように習慣付ける事をお勧めします。

void free_object( void **ptr )
{
	if( *ptr != NULL ) {
		free(*ptr);
		*ptr = NULL;
	}
}

 実を言うと、短時間で終了するプログラムならあえてfree()しない方が安全です。プログラムが終了すれば、まともなOSなら、プログラムが使っていたメモリは動的に割り当てられた部分を含めてシステムに回収されます。余計な事はしないでシステムに任せてしまえば良いのです。

 ちなみにWindowsが流行り出してからメモリーリークという言葉を良く聞きますが、リークが問題になるのは長時間動き続けるプログラムが時間と共にメモリーを食いつぶしていく場合だけです。OSの核とかDLLとかにこの手のバグがあるのは困りものですが、短時間で停止する一般のプログラムではさほど気にする事はないと思います。それよりは、管理されていないfree()をばらまいて余計なバグを引き起こす事の方が有害です。

勿論、きちんとプログラムを書かないのが一番いけない事なのですが :-p

引数で値を返す場合

 Cでは関数の引数は値渡しなので、引数で渡されたパラメータを関数の中で操作しても呼び出し元の値は影響を受けません。コンパイラが(大抵はスタック上に)作り出した一時的なコピーを操作するだけなので、当たり前です。が、これでは関数から複数の値を(引数として)返したくても、返せない訳です。

 この時、非常に原始的なやり方ですが、Cでは操作したい(値を書き換えたい)領域を指すポインタを渡す事によって、関数の方からその領域を操作する事で同様の効果を得る事が出来ます。例えばこんな風にやる訳です。

long string_to_long( char *string, char **next_position )
{
	long	value = 0;
	int	n;

	if( sscanf( string, "%ld%n", &value, &n ) != 1 )
		*next_position = NULL;
	else
		*next_position = string + n;
	return value;
}

 この例ではsscanf自体がポインタを受け取って値を返すので、二重にサンプルになっていますね。うん、良い例だ(自画自賛^^)

 さて、何事にも落とし穴はあるものです。

 プロトタイプがちゃんと書いてあればコンパイラが警告してくれるので問題にはなりにくいですけれども、上の例に示したsscanfのように可変引数になっている場合などは型が決まっていないので受け取り変数の型が間違っていても警告は出来ません。特にintとcharは良く間違えるので注意しましょう。

下のsscanfの間違い、ちゃんと説明出来ますか? (i='a' c=1 とはなりませんよ)

char	*string = "a1";
char	c;
int	i;

sscanf( string, "%c%d", &i, &c );

データの途中の部分を処理に渡す場合

 配列や文字列(=文字配列)の一部を関数に渡したい時など、渡したい部分の先頭の要素のポインタを使う事があります。配列の場合はインデックス番号を使えば良い事もありますが、関数側のインターフェースがポインタになっているような場合はそうもいきません。

 例えばちょっと上の例で示した文字列の一部を数値として読み出す関数をくり返し呼び出すようなコードを考えると、下記のようになります。

char	*string = "1234,5678,9012;";
char	*pos = string;
long	value;

while( (value = string_to_long( pos, &pos )), pos ) {
	printf( "%ld, %c\n", value, *pos );
	pos ++;
}

 こういうコードを書く時は、ポインタで渡した領域が呼び出した先で書き変えられない事をよく確認しておく必要があります。上記のstring_to_long関数はそういう意味では下記のようにconstを付けて宣言するべきです。

long string_to_long( const char *string, const char **next_position )


part4a(ポインタを使うと便利な局面 − その1)に続く

インデックスに戻る