前節では、関数に配列を渡すには配列の先頭要素のアドレスを渡していたと説明しました。
ところで、次のプログラムは、ポインタ型の引数にした前節のプログラムですが、
このプログラムを見て、どこか不自然な部分は見あたらないでしょうか?
この中で明らかに不自然なのは、コメントで示した行の data です。#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; }
このことは、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言語では、配列を実現する手段として、アドレスへの足し算を利用します。
ところが、ポインタ変数もたまたま同じ機能を持っているため、
ポインタ変数を使うと配列と同等のことが出来てしまいます。
その為、ポインタと配列は混同しやすいのですが、
配列はあくまでも多数の変数の先頭を示す固定された変数であり、
ポインタ変数は、好きな変数のアドレスを代入して、
好きなメモリ領域を使うことが出来る可変的な変数です。
前項では、ポインタ変数に配列のアドレスを代入すると、同じように使えると説明しました。
同じように使えるとは、[]演算子で要素番号の指定が出来るという意味です。
しかし、実を言えば、ポインタ変数には、ポインタ変数用の書き方があります。
これは、ポインタ演算と呼ばれる書き方で、次のように書きます。
先頭にある*は、ポインタ変数を通常変数モードに切り替えるための演算子です。*(ポインタ変数 + 要素番号)
次のプログラムは、この書き方で先ほどのプログラムを書き換えた例です。
[ ポインタ演算 ]
ポインタ変数に加減算を行って配列の要素を使う書き方。
昔は、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; }
つまり、ポインタ変数の値そのものを増加させてアクセスすることで、
配列の要素1つ1つに順番にアクセスしていくという方法なのです。
これは、高速だとの理由から、C言語では良く使われていた書き方です。
何故なら、普通に[]で配列を使う場合、その配列にアクセスする毎に足し算が必要です。
しかし、ポインタ演算なら、足し算はループの時に1回ずつ行うだけで済むからです。
前項では、ポインタ変数で配列要素へアクセスする書き方のポインタ演算を説明しました。
ですが、皆さんにお聞きします。あの書き方はわかりやすいと思いますか?
少なくとも、筆者にはわかりやすいとは思えません。
まず、次の2つは全く同じ意味なのですが、どちらがわかりやいすかは歴然です。
また、++を使って増加していく方のポインタ演算などは更にひどいです。data[5] *(data + 5)
更に、++を使って増加していく方のポインタ演算は高速だと書きましたが、for (i = 0;i < 10;i++) { average += data[i]; } for (data = array;data != &array[10];data++) { average += *data; }
現代のコンパイラは、非常に様々な工夫を凝らしてコンパイルを行います。
そんなコンパイラにとって、[]を使ったループを++ポインタ演算に書き換えるのは、
非常に簡単なことであり、ほとんどのコンパイラにはこの機能が付いています。
C言語が作られたばかりの頃は、そんなコンパイラはなかったのですが、
現代では多くのコンパイラがその程度の工夫は行ってくれます。
昔は++ポインタ演算が結構使われており、その名残から現在でも使う人は多いのですが、
[ 組み込みでは ]
ほとんどのパソコン向けのコンパイラは適切な最適化を行ってくれますがが、
組み込み(家電などに内蔵されるコンピュータ)はそうとは限らないようです。
電子機器向けのプログラミングをする場合は注意して下さい。
現代でも、++ポインタ演算がC言語のあるべき姿と信じて、
++ポインタ演算を乱用する人は多いようです。
普通の人類であれば、[]を使用する方がわかりやすいと思うのですが、
まあ、それは各々の好みですので、仕方ありません。
ここまでで、ポインタ変数の機能はほぼ説明し尽くしましたし、
ポインタ変数はアドレスを記憶する変数であることを重視して、
それにまつわるさまざまな現象を説明してきました。
しかし、実際にプログラムを作るときには、
ポインタ変数がアドレスを記憶する変数であることはさっぱりと忘れて下さい。
何故なら、ポインタ変数の本当の使い方とは、変数のショートカットとして使うことです。
決して、アドレスを操作することではありません。
言い換えれば、ポインタ変数がアドレスを記憶するのはコンパイラの内部処理であり、
使い方さえわかっていれば、内部処理がどんな仕組みだろうと無関係だからです。
コンピュータの仕組みを全く知らなくてもコンピュータが使えるのと同じことです。
多くの人がポインタ変数でつまづくのは、アドレスを記憶することばかり意識するためです。
そんな内部の仕組みなど知らなくても、ポインタ変数は簡単に使えます。
変数に&を付けてショートカットを設定し、*記号を付けて通常変数モードにして使用する。
この手順に従って使う限り、アドレスなどなんの関係もないのです。
ただし、C言語ではときおり非常に不可解なバグがでることがあります。
この場合、その原因の多くはポインタ変数の使い方を間違えているためです。
バグ修正の時にはポインタ変数がアドレスを記憶する変数であることを思い出し、
どこかで間違ったアドレスが代入されていないか調べなくてはならないでしょう。