C言語のポインタはメモリを想像できれば理解できる

広告

最近、C言語のポインタは難しいって話をプライベートでして、そのときにした説明をまとめてみた。

コンピュータのメモリがどのようになっているか想像する

これはHex Fiendというソフトを使ってあるファイルのHexダンプを見たものだけど、コンピュータのメモリも同じような構造になっているのでこれを使う。類似のソフトはHex Editorなどで検索すれば出てくるだろう。

メモリイメージ

コンピュータのメモリはこうしたずらずらと数値が記録されたマス目の連続のような構造をしている。Hex Editorなのでこれは16進数で表現されている。1バイトは16進数2桁で表現される。Hex Editorでは普通は1バイトごと、あるいは4バイトごとに区切って表示する。このエディタは一区画ごとに8桁の16進数があるので4バイトごと。

ポインタを使うのに重要なのはたぶん、こうしたメモリの内容を頭の中で思い浮かべることができることだと思う。

ポインタは矢印

ポインタはそのリニアなメモリに対して矢印をつけること。

メモリイメージ2

ポインタはポインタ型という型

ややこしくしているのはint *aとか、char *bといった「intのポインタ」のような言い方かも知れない。しかしポインタはポインタ型があるだけで、intだろうとcharだろうとstd::stringだろうとポインタはポインタである。

int main()
{
	cout << sizeof(char *) << endl;
	cout << sizeof(int *) << endl;
	cout << sizeof(string *) << endl;
	cout << sizeof(vector <string> *) << endl;
}

を実行したら

8
8
8
8

となった。環境はMac OS X Mountain Lionのx64環境。昔はポインタは全部4バイトだと思っていたけど最近は8バイトのようだ。charは1バイトなのにポインタだと8バイトになる。

32ビットOSではメモリ空間が32ビット(4バイト)なので、メモリ空間に矢印をつけるポインタも4バイトになるが、64ビットOSあるいはメモリ空間が64ビットの場合はポインタも64ビットぶん必要になる。

指すものが複数あっても矢印は1つで済むことも

ポインタは矢印であって実体ではない。ポインタが1つしかなくても、メモリ上のどこかに実体があって、必要に応じて矢印の向きを変えてやれば済む場合がある。例えばOSが管理するウィンドウがたくさんあるとする。メモリのどこかにはウィンドウの情報がたくさん記録されている。もしOSが常に先頭のウィンドウの実体に矢印が向くようにしてくれれば、プログラマはたくさんあるウィンドウの管理をしなくて済む。

具体例

class taco
{
	static set<taco *> chain; // 自分自身のポインタを入れるstaticなコンテナ

public:
	// コンストラクタ
	taco()
	{
		// コンストラクタで自分自身のポインタをチェーンに追加
		chain.insert(this);
	}
	
	static size_t count()
	{
		return chain.size();
	}
	
	static void release()
	{
		for( set<taco *>::iterator it = chain.begin(); it != chain.end(); it++ )
		{
			if( *it != NULL )
			{
				delete *it;
				chain.erase(it);
			}
		}
	}
};

// 静的メンバの実体
set<taco *> taco::chain;


int main()
{
	// ポインタに紐付けしない
	new taco();
	new taco();
	new taco();
	new taco();
	new taco();
	
	cout << "There are " << taco::count() << " objects in the memory." << endl;
	
	taco::release();

	cout << "There are " << taco::count() << " objects in the memory." << endl;
}

実行結果

There are 5 objects in the memory.
There are 0 objects in the memory.

このようなクラスを作る。そうすると taco *t = new taco(); のようなことをいちいち書かなくても好きな数の taco インスタンスを発生して、あとで必要なときに管理できる。

例えばシューティングゲームを作っているとする。ボタンを押すと弾を発射する。弾が1つのインスタンスになるように設計して、このような方法をやればメインのルーチンからはキーが押されたら new Bullet(); するだけでいいので楽。Bulletクラスには、例えば定期的に弾の座標を動かす(アニメーション)、何かにぶつかるか画面から出たら自分自身を削除みたいなルーチンを書いておけばシンプルなコードになる。

このように、ポインタで指す先の数だけポインタが要るわけではない。もちろん、OSなりライブラリがメモリリークしないようにどこかで管理しているのだけれど。もしメインのルーチンから指している先が必要なら、OSなりライブラリに依頼して矢印の向きを変えて貰えばいいのだ。

ついでに。このようなオブジェクト指向のテクニック集をデザインパターンと呼ぶ。特に有名なのはGoFのデザインパターン。しかし、デザインパターンというのはGoF本を暗記するものではなく、入門書とか他の誰かが書いたコードを真似して書いて手が覚えたころに後になって「ああ、これは○○パターンと言われるのか」と本で確認するものだと思う。だから、GoFパターンでも自分があまり使わないものは知らなくていい。

charのポインタは指している先がcharであるということ

char *a自体はただのポインタだが、int *bとの違いは、ポインタの指している先がchar型であるということを意味している。

メモリイメージ3

ポインタの指している先は、*をつけることで扱うことができる。

int main()
{
	char str[] = "ABCD";
	char *a = str;
	*a = 0x70;
	cout << *a << endl;
}

このように書くと、上記の図では0x2D(16進数で2D)だった矢印で指している先が0x70になり、coutの結果は0x70('p')が表示され、strは"pBCD"に書き換わる。

int *bの場合

int *bにしたら、ポインタの指している先がint(ここでは4バイトを想定)であるという意味になる。

メモリイメージ4

先ほどと同様に

int main()
{
	char str[] = "ABCD";
	int *b = (int *)str;
	*b = 0x70;
	cout << *b << endl;
	cout << str << endl;
}

のようにすると、今回は先頭の1バイトだけではなく、4バイトまるごと書き換わる。現在使用しているCPUはリトルエンディアンであるため、0x70 00 00 00がメモリに書き込まれる。

オブジェクト指向もどき

OSがウィンドウの情報(位置、大きさ、描かれている内容など)を管理する構造体を持っているとする。例えばWindowRecordとでもしておく。

このウィンドウを拡張したものをWindowRecordPlusとでもしよう。拡張ウィンドウもウィンドウの一種だから、ウィンドウの情報(位置、大きさ、描かれている内容など)は持っているはずだ。そうすると、例えばこんな構造体を書ける。

struct WindowRecordPlus
{
	WindowRecord Window;
	int extension; // 追加する情報
};

ここで、WindowRecordを指すポインタを引数にとる関数 void ShowWindow( WindowRecord * )があるとする。その場合には適切にキャストしてやれば WindowRecordPlus * の変数を渡すことはできる。なぜなら WindowRecordPlus * は「指している先のデータはWindowRecordPlus型ですよ」と言っているだけで、実体はただの矢印である。そして矢印を受け取るShowWindow()関数は矢印の指している先にはWindowRecord型があると思っている。WindowRecordPlus型の先頭はWindowRecord型なので正しく動作する。

このようにして、C言語でもオブジェクト指向っぽいことはできるし、昔のOSのAPIではこういうことはよく用いられていた。

NULLポインタ

NULLポインタはポインタとして無効なものを表すために使うことが多い。int *a = NULL; としている場合はこのポインタは無効ですよということを指す。

NULL = 0

NULLはドイツ語のゼロという意味だが、C言語では単に #define NULL 0 みたいに宣言されていることが多い。(void *)がついている場合もある。void型のポインタについては後述。

NULLは単に0、あるいは(void *)0でしかないので、自動的にはチェックされない。つまり

int main()
{
	char *a = NULL;
	a = (char *)malloc( 64 );
	free(a);
	a = NULL; // もう使えないのでNULLを入れる癖を付けることが推奨される
	
	if( a == NULL )
	{
		// NULL の場合の処理
	}
}

使ってはいけないポインタにはNULLを入れておき、ポインタを操作する前にはNULLでないかを確認する。そしてポインタが無効になったら直ちにNULLを入れる癖を付けるほうがいい。

if( a )

よく、ポインタ型であるaに対して、if( a )と書く人がいるけど、これはあまり好きじゃない。ほとんどの場合はNULLは0で、ifは0か非0かを判定するだけなので、こういう書き方ができるし、これで動作する。

しかし、if()の中には論理値を書くべきなのにポインタを入れるというのは、動くからいいやという話であって自分は好みではない。また、最近のコンパイラならif( a == NULL )とか書いても最適化されるから、冗長に書いておいても別に問題はない。

ポインタではない変数をポインタに変える、その1

普通の変数をポインタに変えるには、&を付ける。void taco( int *a )という関数はintへのポインタを引数にとるので、この関数に普通のintを渡すには、taco( &i ); のようにして呼び出す。

C言語の配列はポインタ

C言語には配列というものがあるが、これは実はポインタとほぼ同一である。char a[]はchar *aと同じ。これによってややこしいことがいくつか起きる。

配列を受け取るはずの関数にポインタを渡せる(またはその逆)

関数のプロトタイプがvoid taco( const char *str )だとしても、char a[]を渡しても問題ない。なお、char *はconst char *に暗黙の型変換される(逆はダメ)。

const char *a

const char *a; だとポインタの指しているもの(*a)は変更できないが、ポインタ自体(a)は変更できる。関数のプロトタイプが void taco( char *a )の場合は、この関数を呼ぶと中身が変更される可能性がある。というか、自分がコードを書く場合にconstを付けていない場合には「値を変更しますよ」というメッセージと考えている。値を変更しませんよ、と他の人に伝えたい場合には必ずconstを付ける。

例:

void swap( char *a, char *b )
{
	char tmp = *a;
	*a = *b;
	*b = tmp;
}


int main()
{
	char a = 'a';
	char b = 'b';

	cout << a << ", " << b << endl;
	
	// aとbの値を入れ替える
	swap( &a, &b );

	cout << a << ", " << b << endl;
}

実行結果:

a, b
b, a

参照

C++には参照というのがある。参照は関数のプロトタイプに & をつければよい。

参照は普通は関数呼び出しに使う。void taco( const string &str )のような形。これは別に void taco( const string *str )でもいいが、受け取った関数内でいちいち *str のようにしなくて済むのと、前述のように str が NULL であることを気にしなくていい。

参照にもconstをつけられる。constは後述するが、constがついていない参照の場合、うっかりその関数に値を渡すと中身が書き換えられる恐れがある、というか書き換えることを目的としていると考えた方がいい。

void swap( char &a, char &b )
{
	char tmp = a;
	a = b;
	b = tmp;
}


int main()
{
	char a = 'a';
	char b = 'b';
	
	cout << a << ", " << b << endl;
	
	// a, bはポインタではないのに値が書き換わる
	swap( a, b );

	cout << a << ", " << b << endl;
}

実行結果:

a, b
b, a

書き換えないのに参照を使うのは、ポインタが面倒くさいのと、コピーのコストを抑えたいから。void taco( const string &str )という関数の場合、constが付いているから中身を書き換えられることはない。こういう風に書くのは、普通に void taco( const string str )の場合は関数を呼び出したときにstring型のコピーを作ってそれを関数に渡す。コピーを作るのに時間がかかるようなもの、つまりintとかcharとかポインタのような小さいものではなく、クラスのような大きなものを渡すときには参照渡しにしたほうがいい。

ポインタではない変数をポインタに変える、その2

ややこしい話。char a[]は配列なので、aはchar *型。a[0]は配列の先頭要素で、*aと同等。&a[0]は配列の先頭要素を指すポインタなのでaと同等。

a[1]は*(a + 1)と同等。&a[1]は(a + 1)と同等。

char型へのポインタであるaをint型へのポインタにキャストして、一つ進めて、またchar型のポインタに戻すような操作もできる。(char *)((int *)a + 1); その場合はchar型のポインタを4バイトぶん進めたものと同等。

ポインタを使ったややこしい操作が嫌いな人はポインタをできるだけ使わず配列を扱うといい。しかし(すぐに思いつく用途としては)一つだけポインタを使った方が便利かなというものがある。配列の場合は添え字を覚えておかなければならない。a[i]ならばaのi番目の要素を指す。つまりaとiという二つの変数を使う必要がある。しかしポインタならaだけでいい。

int main()
{
	// 配列aと配列の添え字counterの2つの変数が必要
	char a[] = "ABCDE";
	int counter = 0;
	cout << a[counter++] << endl;
	cout << a[counter++] << endl;
	cout << a[counter++] << endl;
	cout << a[counter++] << endl;
	cout << a[counter++] << endl;
	
	// 矢印を動かしていくだけなので、ポインタbだけあればいい
	const char *b = "ABCDE";
	cout << *b++ << endl;
	cout << *b++ << endl;
	cout << *b++ << endl;
	cout << *b++ << endl;
	cout << *b++ << endl;
}

実行結果

A
B
C
D
E
A
B
C
D
E

char *a = "aaaaa";

char a[] = "aaaaa"; はOKだが、char *a = "aaaaa"; はダメ。これは単に"aaaaa"とした場合にはconst char *型で、それをaに代入している。char a[] = "aaaaa"; の場合はa[]の中身が"aaaaa"であると初期化している。ポインタと配列はほぼ同じものだが、細部では異なる。

イテレータ

C言語のポインタはイテレータ(反復子と書かれることが多いが、巡回のほうがしっくりくるかな)でもある。

前述のように、ポインタ変数は a++; とか a += 10; のような足したり引いたりができる。これはJavaのポインタのように純粋にポイントするものだけではなく、C言語のポインタはイテレータとしても使えるということ。

メモリイメージ5

char型のポインタを+1すると、指している場所がchar型1つぶん先に進む。つまりchar *なら1バイト、int *なら4バイト先に進む。メリットは前述のようにポインタ変数1つあれば、配列のようにカウンタを用いなくても巡回できること。

void型のポインタ

void型という変数はないけれど、void型のポインタはある。関数のプロトタイプを void taco( void *a )としておくと、この関数tacoにはどんな形のポインタでも放り込むことができる。ポインタ自体はどの型でも同じポインタ型なので、void *でもなんでもいいけれど、void *のポインタは指している先が何であるかの情報が欠落している。だから使う際には(char *)などにキャストしてつかわなくてはいけない。

なぜこんなことをするかというと、よく使うなーというケースは関数コールバック。コールバックは、よく使われるのはOSのAPIに関数を渡しておいて何かあったらあとで呼び出してもらうようなケース。たとえば「アプリケーションが終了する場合にはこの関数を呼んでね」とOSにお願いしておくと、OSがシャットダウンするなどのときに登録しておいた関数をコールしてくれる。ただ、OSからするとそのアプリケーションがどういう値を引数に付けて呼ぶかは知ったことではないので、コールバック関数の引数には大抵void *がついている。OSからすると、箱に使いそうなデータをごちゃごちゃ突っ込んで、中身は知らないけど、ホレという感じで渡す。被コールバック関数はOSからvoid *の引数を受け取って自分に都合のよいように処理をする。

特にC++のクラスのメンバ変数をコールバックで呼ぼうとすると面倒くさい。staticなメンバ関数はコールバックできるが、非staticなメンバ関数はインスタンスが発生しているときにしか呼べないのでそもそもコールバックできない。ではどうするかというと、コールバック用のstaticなメンバ関数を作っておく。この引数はAPIに併せるが、前述のように大抵の場合は void * を渡せるようになっている。だから、自分自身を指す this ポインタを渡して登録しておいて、staticなコールバック用関数では void * を受け取ったら、自分自身のクラスのポインタにキャストしてから、非staticメンバ変数を呼ぶという手続きをする。

余談、プログラミングをこれからやろうという人にC言語とかC++を勧めるのは反対。なぜならC/C++にはこうした面倒くさいテクニックを知らないとやってらんない点が多すぎるのも一因。他には標準ライブラリだけではネットワークもGUIも扱えないから教科書の中身がつまらなくなること、本格的なオブジェクト指向のフレームワークがついていないからGoF本をやるとかつまらないことになりがち。例えばJavaの入門書だったらネットワークとかスレッドを使う例も書けるし、Threadを使う場合にはrunnableをimplementするとかJavaのフレームワークの作法をまず有無を言わさず強制される。この強制によってJavaのフレームワークを作った人(プログラミングの上級者)が色々苦労して身につけたノウハウを無理なく覚えるのに役立つ。

C++の「こうするべし、こうするべからず」集は

が定番。でもこんな本を読まないと先に進めないC/C++は初心者、いやC/C++ではないと仕事ができない人(組み込みプログラマとか)以外はやるべきでないと思う。

ポインタのポインタ

ポインタも大きさが8バイトとか4バイトのただの変数であるから、ポインタへのポインタも作れる。

ポインタへのポインタは二次元配列を動的に確保するときによく用いられる。二次元配列というのは、配列の各要素が配列であるものである。例えば10x10の二次元配列を作るとする。この場合、ポインタをまず10個動的に確保する。そして、さらに10回メモリの動的確保をして、10個確保したポインタにそれぞれ結びつけていく。

他の用途にはメモリの再配置があった。昔はコンピュータのメモリが乏しかったため、OSがときどきメモリの再配置を行ってフラグメントを解消していた。ところがOS側に勝手にメモリを動かされるとポインタは役に立たなくなってしまうため、ポインタへのポインタをよく用いた。たとえて言うのなら、よく引っ越しをする借家暮らしの知人がいるとする。その知人が毎回引っ越したことを把握するのが面倒なので、その知人に用があるときは実家(持ち家だから引っ越しはしない)に電話して「いま彼はどこにいますか?」と尋ねるようなイメージ。

まとめ

ポインタなんて簡単だろうと思ったけど、書いてみるとこれでも書き足りないくらいあれこれあって、そりゃ混乱もするかなー。

追記

みなさま、コメントありがとうございます。

tacoクラスは自身でメモリ管理するクラスであり、使う側でdeleteがいらないというなら、new自体を隠すほうがいいと思います。
taco::createInstance()のようなstaticメンバ関数を公開して、そのなかで自分をnewしてコンストラクタを呼んで、ポインタを呼び出し元に返すほうがいいです。ついでにデストラクタもprivateにしてしまうほうがいいですね。(他から勝手に削除されないように)

これはおっしゃるとおりです。特に自分以外の人がこのクラスを使う場合は勝手にdeleteできないようにコンストラクタやデストラクタを隠しちゃうべきです。ただ、今回はポインタを自分で管理しておかなくても、あとで「矢印の向きを変えてやる」方法があればOKということでこうしました。

そしてfor文なのですが、set::iteratorという書き方はできないため、set::iteratorと書く必要があります。

これは誤植というか、HTMLの特殊文字をエスケープしていなかったので消えてしまいました。ご指摘ありがとうございます、直しておきました。

chain.erase(it)はそのiteratorが指す要素をchainから削除しますが、その時点でitが無効になるため、そのあとのfor文のit++が未定義動作を引き起こします。

うっかりしていました。これもおっしゃるとおりで、サンプルとしてはよくないコードです。訂正例まで載せて下さって感謝。ちなみに、こういうやっちゃダメな例を多く集めているのがEffectiveなんとかって本ですが、あちこちに落とし穴のある言語って言語自体に欠陥があるんじゃないのかと。

また、リンクにあるEffective C++は第3版が出版されているため、紹介されるのであればそちらのほうがよいかと思われます。

直しておきました。第三版は検索には出てきたけど「原著」とあったので、日本語版ではないのかと早とちり。自分の持っているのは第二版のほうです。

またオブジェクト指向もどきの部分ですが、あのような例であれば、WindowRecordPlusの変数(例えばwrp)から&wrp.WindowのようにしてWindowRecord型のオブジェクトへのポインタを取得し、それをShowWindowに渡したほうが適切かと思います。
というか関連のない型同士のキャストは使い方を誤るとメモリの破壊を引き起こす可能性がありますし、あのような感じでキャストによってオブジェクト指向っぽいことをするというネタは推奨されないということも合わせて紹介しておくと親切だと思います。

これもそうですね。深掘りしていくと色々とあります。

● 小池邦人のMac OS Xへの道 2000/07/26
~ Carbon対応の実際 その2 ~

さて、今までの環境では、SetPort( window )といった具合に、SetPortに直接WindowPtrを渡してもエラーにはなりませんでした。ところがCarbon環境では、こうした処理は許されていません。それを補うために、以下のようなAPIが用意されています。

void SetPortWindowPort(WindowPtr window);
WindowPtr GetWindowFromPort(CGrafPtr port);

同様に、DialogPtrもWindowPtrとほとんど区別無しで扱われてきましたが、Carbon環境ではきっちり区別します。という事は、当然CGrafPtrとも区別する必要がありますので、以下のようなAPIが用意されています。

void SetPortDialogPort(DialogPtr dialog);
DialogPtr GetDialogFromWindow(WindowPtr window);

時代を経るとこういうのは止めようって流れですね。

概念はそうだけど、それをCの構文とマッチさせるのがむずかしいと思ってる
int a = b * c; // 掛け算
int* d; // intへのポインタ変数宣言

記号の使い方には議論の余地があるかも知れませんね。参照のための&なのか、アドレスを得るための&なのかわかりにくいという意見もあるかも知れません。

余談、記号は慣れた人が書くには平易ですが、Googleで検索しにくいので初心者には向かないかな。例えば*の意味がわからなくてもGoogleで "C言語 *"と検索しても期待するようには動きません。もし、例えばintのポインタはintPtrとPtrを後ろに付けると決まっているとすれば、検索には便利です。でも書く度に3回もタイプしなきゃいけないのは面倒か、あるいは最近のIDEは補完があるから気にしなくていいのか。

6 件のコメント

  • いろいろと気になったのですが、とりあえず・・・

    tacoクラスは自身でメモリ管理するクラスであり、使う側でdeleteがいらないというなら、new自体を隠すほうがいいと思います。
    taco::createInstance()のようなstaticメンバ関数を公開して、そのなかで自分をnewしてコンストラクタを呼んで、ポインタを呼び出し元に返すほうがいいです。ついでにデストラクタもprivateにしてしまうほうがいいですね。(他から勝手に削除されないように)

    そしてfor文なのですが、set::iteratorという書き方はできないため、set::iteratorと書く必要があります。

    chain.erase(it)はそのiteratorが指す要素をchainから削除しますが、その時点でitが無効になるため、そのあとのfor文のit++が未定義動作を引き起こします。

    というわけであのままではサンプルが動きませんので、それを踏まえるとこのようになります。( http://ideone.com/6YekPO )

    また、リンクにあるEffective C++は第3版が出版されているため、紹介されるのであればそちらのほうがよいかと思われます。

    またオブジェクト指向もどきの部分ですが、あのような例であれば、WindowRecordPlusの変数(例えばwrp)から&wrp.WindowのようにしてWindowRecord型のオブジェクトへのポインタを取得し、それをShowWindowに渡したほうが適切かと思います。
    というか関連のない型同士のキャストは使い方を誤るとメモリの破壊を引き起こす可能性がありますし、あのような感じでキャストによってオブジェクト指向っぽいことをするというネタは推奨されないということも合わせて紹介しておくと親切だと思います。

    以上検討よろしくお願いします。

  • 概念はそうだけど、それをCの構文とマッチさせるのがむずかしいと思ってる
    int a = b * c; // 掛け算
    int* d; // intへのポインタ変数宣言

  • >char a[]はchar *aと同じ。

    いろいろありますが、少なくともここは相当問題のある表現かと。

    いわゆるANSI-Cにおいて、a[]のように空の[]が書けるところは
    3か所ありますが、その中で、char a[]がchar *aと同じになるのは
    関数定義の仮引数という限られた文脈でしかありません。
    Cにおいて、ポインタと配列は別物なのに、それを混同してしまうことで
    多くの悲劇が生まれているように思います。

    • それはごもっともです。仕様と実装の違いについてはもう少し丁寧に触れるべきだったかも。おっしゃるとおり、C言語では配列とポインタは別物ですが、実装がどちらもアドレス指定という同じようなものになっているということですね。

  • コメントを残す

    メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

    日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)