はじめに
※これを書いているのは現時点(2018年11月)でプログラミング経験一年未満のペーペーですので、恐らく誤りもあるかと思われますが、その場合はご指摘頂けると有り難く思います。初心者タグは書いている本人が初心者というオチ。
※なお、コメントで訂正・アドバイス等を頂いておりますので、現在分かっている誤り等については脚注にまとめております。概ね本文で取り消し線を引いてある箇所は誤りか誤解を生む表現となっておりますので、ご注意頂きたく思います。
折角Qiitaに登録してみたということで、私も何かしら書いてみようと、散々書かれているであろうC言語のポインタについて何となくの考えを書いてみる。
事前知識として:ポインタ変数の宣言など
まず、説明の前にC言語でのポインタ周りの記法を軽く紹介。ただし、ここの文法についてはある程度分かっている(聞いたことがある)前提で話を進めるので、詳しくは説明しないこととする。
あるデータ型のデータに対するポインタ変数を宣言する時は、以下のような形式1でデータ型と変数名の間に*
をつけて宣言する。
int* ptr; /* 個人的に好き */
int *ptr; /* よく見る形式 */
int * ptr; /* たまに見る形式 */
ポインタ変数はメモリ上の場所情報(アドレス)を入れるので、データ型が一緒でも通常の変数をそのまま代入することはできない(あるいはできてもすべきではない)。通常の変数の場所を代入したいのであれば、以下のように変数名の前に&
をつけることで、その変数(以下の例であればint型変数num
)の場所を示すことができる。
int num; /* 変数宣言 */
int* ptr = # /* 変数のアドレスをポインタ変数に代入 */
逆に、ポインタ変数が指している場所の値を参照したい場合は*ptr
のような形でポインタ変数名の前に*
をつける。
また、構造体へのポインタでは、アロー演算子->
を用いてポインタ変数が指している構造体のメンバの値を参照することができる。
typedef struct sample {int member;} StSample;
StSample st_sample;
StSample* st_ptr = &st_sample;
st_ptr->member;
ポインタって何なのということ
ポインタについてのよくある(私もされた)説明として、「変数は値を入れる箱」、「ポインタ変数は値が入っている場所を入れる箱」っていうものがある。これ、すぐに分かる人がいるんだろうか。「場所なんて入れずに直接値入れんかい!」って思う人多いんじゃない?
「箱」イコール「変数」の喩えは置いといて、ポインタ変数が場所を示す変数だってところについては認識として問題ないと思っている。でも、「場所」って具体的に何なの?
変数について
まず、そもそも変数ってどういうものなのか。例えば、int型の変数を宣言して、そのサイズを出してみる。
#include <stdio.h>
int main (void)
{
int num;
printf ("sizeof(num) = %d\n", sizeof(num));
return 0;
}
むろん、int型の変数は4バイト(環境によるらしいけど……)なので、4が出力されるはずだ。
つまり、int型変数は宣言されると、4バイトのメモリを占領する……というより、適当に4バイト分のメモリを確保して、そこに"num"という名前をつけました、といった感じになる。「そこ」(つまり、変数などが保存されているはずのメモリ上の2場所)っていうのが今回の「場所」だ。
ポインタが指すモノ
ポインタ変数は、変数を指す時には、変数の指すメモリの先頭を記憶する2変数として働く。つまり、
int num = 0;
int* ptr = #
上のようにした場合、ptrが指しているメモリを先頭に、int型のデータが保存されていると思ってください、ということである。ポインタ変数のデータ型は指し先のデータをどういうデータとして取り扱いますよという目印程度の意味でしかない。
#include <stdio.h>
int main (void)
{
int num = 60000;
int* ptr = #
printf ("the number is %d\n", *((char*)ptr));
return 0;
}
だから、値を参照する時、こんな事をしても一応値は出てしまう。(ちなみに、実行してみると手元では実行結果は96になりました。)やってることは単純で、int型のポインタ変数をchar型(1バイト)のポインタ変数として扱ってもらって、その値をprintf()で出力した訳だ。当然ながらnumにchar型で取り扱えない値を入れていたら表示される値が変わってしまう。
何でそんなことができるのかと言うと、ポインタ変数はどんな型へのポインタであれ、突き詰めればメモリの場所の情報(アドレス)を保存した変数でしかないからだ。だから、ポインタ変数からポインタ変数へキャストする時にはデータ形式が一致しない、というようなことは起こらない。3
ここまでが理解できるなら、まずポインタがどういうものなのかについては、ある程度想像がつくんじゃないかと思っている。要点は、
- ポインタ変数は
メモリ上の2場所を示す変数。 - ポインタ変数のデータ型は、そこにどんな型のデータが入っているかを示す目印のようなもの。
- ポインタ変数のデータ型を別のデータ型に置き換えてから値を見ると、置き換えた後のデータ型のサイズ分しか読み込まないので、表示される値が変わることがある(キャスト後の方がデータサイズが大きい場合でも同様に読もうとするので、参照してはいけない領域まで参照しかねないことにも注意)。
といったところだ。
おわりに
書いていて意外と長くなってしまったので、結局冒頭の「直接値入れんかい!」には触れられていない。そこで、実際どういう時使うん? という点については続きを書こうと思う。ただ、その前に配列あたりにも触れなきゃならんかなぁ。(追記:もう少し詳細に学んでから書き直そうと思いますので、次書く分に関しては当分凍結したいと思います。)
-
一文で
int* a, b;
と書いた場合、bはポインタ変数ではなく通常のint型変数となる。int*
がまるで型のように見えるため、このような場合は明らかにint *a, *b;
の記法の方が分かりやすい。 ↩ -
完全に誤解していたのだが、C言語としては必ずしもアドレスが仮想アドレスを表す必要はない。ポインタ変数を介する場合でも、仮想アドレスを利用せず処理が可能であれば言語仕様上全く問題ないので、ここの記述はかなり不正確である。 ↩ ↩2 ↩3
-
ここも不正確。strict aliasing rulesというものがあり、ポインタ変数のデータ型についても、互換性がある(とC言語規格で認められた)データ型同士のデータ変換等については保証するが、そこから外れると未定義動作を引き起こす。つまり何が起こるかは全く分からないので、C言語の規格に沿ってコーディングする上でも、無闇に別のデータ型にキャストするのは控えるのが無難か。詳細はコメントでご紹介頂いたこちらが詳しい。 ↩