<戻る  目次  進む>  
          <戻 進> 雑誌 白黒 明暗   フォント 既定 ゴシック 明朝 手書き   サイズ   標準  


   配列とポインタの奇妙な関係   

  1. 第1項:配列のような使い方
  2. 第2項:ポインタ専用の書き方
  3. 第3項:古き悪きポインタ演算
  4. 第4項:アドレスのことは忘れましょう

[1]配列のような使い方

前節では、関数に配列を渡すには配列の先頭要素のアドレスを渡していたと説明しました。
ところで、次のプログラムは、ポインタ型の引数にした前節のプログラムですが、
このプログラムを見て、どこか不自然な部分は見あたらないでしょうか?


#include <stdio.h>

int getaverage(int *data);

int main(void)
{
	int average,array[10] = {15,78,98,15,98,85,17,35,42,15};
	average = getaverage(array);
	printf("%d\n",average);
	return 0;
}

int getaverage(int *data)
{
	int i,average = 0;
	for (i = 0;i < 10;i++) {
		average += data[i]; /* ポインタ変数なのに? */
	}
	return average / 10;
}
この中で明らかに不自然なのは、コメントで示した行の data です。
だって、この変数dataはポインタ変数であって、配列ではありません。
にも関わらず、[]を使って要素番号が指定出来るとはどういうことでしょう?

このことは、3節でも簡単に説明しています。
[]の役割は、配列の要素番号を指定する演算子なのですが、
その仕組みは、単に、配列名というアドレスに足し算を行っているだけです。
つまり、別に配列でなくても、アドレス値なら何でも良いと言うことになります。

細かく説明すると、数式の中に配列名を記述した場合、[]の記号の有無にかかわらず、
配列名は、配列の先頭要素へのアドレス(ポインタ値)として扱われます。
そして、その配列名に[]をつけた場合、そのアドレスに番号の値だけ足し算を行い、
その結果として、足し算された分の番号の要素として扱われているのです。


[  宣言時と数式との違い  ]
配列を宣言する時には、[]で要素数を指定し、
配列の要素を使う時は、[]で番号を指定するのですが、
実は、この2つも全く別の記号です。

宣言時の[]は要素数を指定するという意味を持ちますが、
数式の中で使用する[]は、アドレスに足し算する演算子です。

C言語では、似た使い方には同じ記号を使いたがる傾向があり、
その為、異なる意味に同じ記号を割り当てている部分が多いようです。

このことから、使えるメモリならばポインタ変数を配列のように使えることがわかります。
次のプログラムは、ポインタ変数を配列のように使用する例です。

#include <stdio.h>

int main(void)
{
	int *data;
	int i,average = 0,array[10] = {15,78,98,15,98,85,17,35,42,15};
	
	data = array;	/* ポインタ変数に配列のアドレスを代入 */

	for (i = 0;i < 10;i++) {
		average += data[i];	/* 配列みたいに使える */
	}

	printf("%d\n",average / 10);
	return 0;
}
このプログラムの実行結果は、次の通りになります。

49
普段はこの様なややこしいことをする必要は全くありませんが、
いずれ、動的メモリ確保をするようになった時に必要になります。

[  配列とポインタは全く別物  ]
多くの人が、配列とポインタを勘違いしてしまうようです。
配列とは、多数の変数を順番つけでまとめて扱う方法であり、
ポインタとは、変数のショートカットを作る方法です。

それなのに、似たような使い方が出来るのは配列の設計と関係あります。
C言語では、配列を実現する手段として、アドレスへの足し算を利用します。
ところが、ポインタ変数もたまたま同じ機能を持っているため、
ポインタ変数を使うと配列と同等のことが出来てしまいます。

その為、ポインタと配列は混同しやすいのですが、
配列はあくまでも多数の変数の先頭を示す固定された変数であり、
ポインタ変数は、好きな変数のアドレスを代入して、
好きなメモリ領域を使うことが出来る可変的な変数です。

目次に戻る


[2]ポインタ専用の書き方

前項では、ポインタ変数に配列のアドレスを代入すると、同じように使えると説明しました。
同じように使えるとは、[]演算子で要素番号の指定が出来るという意味です。

しかし、実を言えば、ポインタ変数には、ポインタ変数用の書き方があります。
これは、ポインタ演算と呼ばれる書き方で、次のように書きます。


*(ポインタ変数 + 要素番号)
先頭にある*は、ポインタ変数を通常変数モードに切り替えるための演算子です。
かっこをつけて、ポインタ変数のアドレス値に要素番号分の足し算を行い、
その足し算されたアドレス値を通常変数モードに切り替えることで、
先頭アドレスから指定数だけ進んだ先のメモリにアクセスする方法です。

[  ポインタ演算  ]
ポインタ変数に加減算を行って配列の要素を使う書き方。
昔は、C言語らしいというくだらない理由で広く使われていた
次のプログラムは、この書き方で先ほどのプログラムを書き換えた例です。

#include <stdio.h>

int main(void)
{
	int *data;
	int i,average = 0,array[10] = {15,78,98,15,98,85,17,35,42,15};
	
	data = array;	/* ポインタ変数に配列のアドレスを代入 */

	for (i = 0;i < 10;i++) {
		average += *(data + i);	/* ポインタ演算 */
	}

	printf("%d\n",average / 10);
	return 0;
}
[]を使わずに、ポインタ演算を使って配列にアクセスしています。
もちろん、実行結果は先ほどと全く同じになります。

更に、ポインタ変数は値を変更出来ることを利用した次のような書き方もあります。
一般的にポインタ演算と言えば、こちらを指すことが多いようです。


#include <stdio.h>

int main(void)
{
	int *data;
	int average = 0,array[10] = {15,78,98,15,98,85,17,35,42,15};
	
	for (data = array;data != &array[10];data++) {	/* ここに注目 */
		average += *data;
	}

	printf("%d\n",average / 10);
	return 0;
}
このプログラムはかなりややこしい部分が多いので、説明が必要です。
まず、for文の開始時に、ポインタ変数dataに配列のアドレスを代入しています。
そして、更新として、data++、が指定されていますが、
この意味は、今までの変数の時と同じで、data内のアドレスを1つ分増やす演算です。
(正確には、そのポインタ変数の指す型のサイズ分だけ増加させる)
そして、ポインタ変数が(0から数えて)10番目の要素と同じ値になるまで繰り返します。

つまり、ポインタ変数の値そのものを増加させてアクセスすることで、
配列の要素1つ1つに順番にアクセスしていくという方法なのです。

これは、高速だとの理由から、C言語では良く使われていた書き方です。
何故なら、普通に[]で配列を使う場合、その配列にアクセスする毎に足し算が必要です。
しかし、ポインタ演算なら、足し算はループの時に1回ずつ行うだけで済むからです。

目次に戻る


[3]古き悪きポインタ演算

前項では、ポインタ変数で配列要素へアクセスする書き方のポインタ演算を説明しました。
ですが、皆さんにお聞きします。あの書き方はわかりやすいと思いますか?
少なくとも、筆者にはわかりやすいとは思えません

まず、次の2つは全く同じ意味なのですが、どちらがわかりやいすかは歴然です。


data[5]
*(data + 5)
また、++を使って増加していく方のポインタ演算などは更にひどいです。
次の2つを見て、下の方がわかりやすいと思う人類は存在しないと思います。

for (i = 0;i < 10;i++) {
  average += data[i];
}

for (data = array;data != &array[10];data++) {
  average += *data;
}
更に、++を使って増加していく方のポインタ演算は高速だと書きましたが、
実は、これは昔の話だったりします。
現在では、コンパイラの性能が飛躍的にアップしています。
そして、現代のコンパイラは、[]でアクセスしているようなループを見つければ、
自動的に、++で増加するポインタ演算のような書き方に置き換えてコンパイルします。

C言語が作られたばかりの頃は、そんなコンパイラはなかったのですが、
現代では多くのコンパイラがその程度の工夫は行ってくれます。


[  組み込みでは  ]
ほとんどのパソコン向けのコンパイラは適切な最適化を行ってくれますが、
組み込み(家電などに内蔵されるコンピュータ)はそうとは限らないようです。
また、CPUの仕組みが単純なだけ、わずかな違いが性能に直結しやすいです。
電子機器向けのプログラミングをする場合は注意して下さい。
昔は++ポインタ演算が結構使われており、その名残から現在でも使う人は多いのですが、
筆者としては、わかりやすい[]を使って配列にアクセスすることをお勧めします。

現代でも、++ポインタ演算がC言語のあるべき姿と信じて
++ポインタ演算を乱用する人は多いようです。
普通の人類であれば、[]を使用する方がわかりやすいと思うのですが、
まあ、それは各々の好みですので、仕方ありません。

目次に戻る


[4]アドレスのことは忘れましょう

ここまでで、ポインタ変数の機能はほぼ説明し尽くしましたし、
ポインタ変数はアドレスを記憶する変数であることを重視して、
それにまつわるさまざまな現象を説明してきました。

しかし、実際にプログラムを作るときには、
ポインタ変数がアドレスを記憶する変数であることはさっぱりと忘れて下さい。

何故なら、ポインタ変数の本当の使い方とは、変数のショートカットとして使うことです。
決して、アドレスを操作することではありません。
言い換えれば、ポインタ変数がアドレスを記憶するのはコンパイラの内部処理であり、
使い方さえわかっていれば、内部処理がどんな仕組みだろうと無関係だからです。
コンピュータの仕組みを全く知らなくてもコンピュータが使えるのと同じことです。

多くの人がポインタ変数でつまづくのは、アドレスを記憶することばかり意識するためです。
そんな内部の仕組みなど知らなくても、ポインタ変数は簡単に使えます。
変数に&を付けてショートカットを設定し、*記号を付けて通常変数モードにして使用する。
この手順に従って使う限り、アドレスなどなんの関係もないのです。

ただし、C言語ではときおり非常に不可解なバグがでることがあります。
この場合、その原因の多くはポインタ変数の使い方を間違えているためです。
バグ修正の時にはポインタ変数がアドレスを記憶する変数であることを思い出し
どこかで間違ったアドレスが代入されていないか調べなくてはならないでしょう。

目次に戻る


<−前に戻る  先頭に戻る  次へ進む−>