C言語でハマると言えばポインタ!
どうも。
我が家の高校生が突然、「父さんC言語って知ってる?」と聞くので、何を突然?と思ったら高校でプログラム演習の授業があり、そこでC言語を使うのだとか。
Pythonとか今時の言語じゃあないんだと思いつつも、考えてみたらC言語ってプログラムを学ぶ上で第一歩としては結構最適な部分もあるんじゃないかなぁと思う所もあるのです。
しかし、C言語は多くの人がハマるポイントが存在しますよね。
算数で言う所の「分数」みたいなところ。
そう…ポインタです。
ここで多くの人が脱落していきます。昔プログラミングを習った人でもギャー!って思う人も多いんじゃないでしょうか?
なぜポインタは難しい(と思う)のか?
直近でポインタについて質問とかされそうな気配ではあるので、なぜポインタで詰まる人が多くいるのか?詰まるポイントを考えて見ると、教えやすいかも!と思ったので詰まるだろうと思うポイント(ポインタだけに!)をまとめてみました。
ポインタの概念
おそらくポインタが出てくるまでに通常の変数は習っているはずです。int型とかfloat型とか。charも文字型は習っていると思います。ここまで変数って値を入れる箱って扱いで教わっているのですが、ポインタの登場で少しコンピューター側の概念が登場します。
アドレスです。
まぁアドレス自体の概念は難しくないので、ここは乗り越えられると思います。
ポインタ変数
次に出てくるのがポインタ変数です。ポインタ型自体は単独でポインタ型としてある訳ではありません。必ず普通の型にくっついて普通の型のポインタ型となります。
たとえば…
int *a; /* intのポインタ型 */
の様な感じです。
ポインタの代入
続いてアドレスの代入です。「&」が出てきてさらに表記が複雑になってきます。
普通の変数からポインタを取り出してポインタ変数に代入したり、ポインタ変数に格納されているアドレスから中身を取り出したりと変幻自在。この辺りから頭がぼーってなり始めてる人もいるかと思います。
int *a; int b; b=1; a=&b; /* ポインタの代入 */ printf(“%d¥n”,*a); /* bのアドレスを参照しているので1が表示される */
こんな感じですね。
配列とポインタ
C言語は言語仕様で配列が存在します。まぁ普通ですよね。
int a[5];
これです。a[0]〜a[4]までの配列になります。
配列のアドレスをポインタ変数に入れるて利用すると配列の様に使える様になります。ちょっと複雑になります。
配列自体は連続した領域で宣言した個数分の変数がアドレス上に確保されます。ポインタ変数はアドレスを収める事ができるので、配列の先頭のアドレス=[0]のアドレスをポインタ変数に入れる事で、同じ様に扱う事が可能になります。
また配列変数のインデックスを指定しないと連続領域の先頭のアドレスが取り出せると。
なので、”&a[0]”と”a”は等価っちゅう事になります。
「配列の仕組みを理解していればどうという事はない」とシャア少佐も言っておられますが、段々と複雑化していきますね。
/* &a[0]とaが等価である事を調べる */ int i; int a[5] = {0,1,2,3,4,5}; int *b; int *c; /* a[0]のアドレスをポインタ変数に代入 */ b=&a[0]; for(i=0;i<5;i++){ printf("%d¥n",b[i]); } /* a[0]のアドレスをポインタ変数に代入 */ c=a; for(i=0;i<5;i++){ printf("%d¥n”,c[i]); }
こんな感じで出力結果から等価であることが分かると思います。
そして、シレーっと出てきていますが、ポインタ変数は[0]の様に添字演算子を利用する事が可能になっています。
ポインタ演算
さらにわけが分からなくなりまーす。
ポインタの演算です。ポインタにはアドレスを入れる事ができます。ここまでは良いです。
ポインタ型に「+1」してみたり、インクリメントしてみたりする事が出来ます。この時の加算のされ方が特徴的です。ポインタ型に入っているのはアドレスです。なので、1つ先に進む、〇〇分先に進む場合、指定された分、先にあるアドレスになるという事になります。
例えばintのポインタ変数だった場合、ポインタ変数を+1した場合の指し示すアドレスは、sizeof(int)分勝手に進んでくれてます。これどの型でも同じなのです。便利なんだけど複雑なところ。sizeof(int)分加算しなくても大丈夫なんです。
/* ポインタ演算 */ int i; int a[5] = {0,1,2,3,4,5}; int *b; /* a[0]のアドレスをポインタ変数に代入 */ b=a; for(i=0;i<5;i++){ printf("%d¥n",*(b+i)); /* *(b+sizeof(int))とかするとダメ */ }
これで出ると思います。自分が現役だったころはコチラで書いていましたが、今はPCの性能が上がったのとコンパイラが賢くなって同等に評価されるようになったみたいです。私も添字演算子を利用した方が読みやすいコードになるのでそちらを推奨しますね(後述のポインタのポインタとかになってくると(笑))
引数の受け渡し
関数の引数の受け渡しにもポインタが出てきますよね。関数の引数の受け渡しが値渡しです。変数の中身がそのまま関数先に渡るだけなんですけれど意識しないと意図的な動きをしてくれません。
関数の引数が実体だった場合、値が関数の引数にコピーされて利用されるので、関数内で引数の値に変更があっても呼び元の変数には影響がありません。
関数の引数がポインタ変数の場合、ポインタが値渡しされて利用されます。ポインタの中に格納されているアドレス自体は影響がありませんが、ポインタが指し示す先の値を関数内で書き換えると当然呼び元でも影響します。
この仕組みを利用して、インプット引数、アウトプット引数を厳密に定義する事が可能です。
void main(){ int a = 100; int b = 100; printf("a=%d b=%d\n",a,b); func_1(a, &b); printf("a=%d b=%d\n",a,b); } void func_1(int a_a, int *a_b) { a_a = a_a + 100; *a_b = *a_b + 100; }
関数func_1を呼んだ後の変数a,bがそれぞれ、 a=100 b=200に変更になっている筈です。
malloc
さて、ここまでポインタ変数はアドレスを入れる変数で、「&」もしくは配列変数から実体のアドレスを受け取って、「*」を付けてアドレスが指し示す先を参照する事が出来る事がわかりました。この仕組みを利用する事で関数の引数でインプットのみの引数と関数内で値を変更して戻す変数を明確に使い分ける事が可能だと言う事がわかりました。ここまではOKです。
ではポインタ変数って実体の変数をどこかで宣言しておいて必ずその変数のアドレスをもらわないとダメなの?と言う疑問が沸き上がります。
答えは“YES”であり”NO”です。分かりにくい答えですみません。ポインタ変数自体は必ずプログラム内で確保されている領域のアドレスが入った状態で参照をしないとダメです。ですが、必ずしもどこかで宣言されている変数のアドレスである必要はありません。
malloc関数を利用する事でプログラム実行時に動的にメモリを確保する事ができます。malloc関数は指定サイズ分の連続したメモリ領域をヒープ領域から確保して先頭のアドレスを返却してくれる関数でポインタ変数で受ける事が可能です。
さぁ本格的に難易度が上がってきます。ヒープ領域とかスタック領域、静的領域、プログラム領域とか出てきますよね。mallocで確保される領域はヒープ領域で確保されるので使いっぱなしにしておくとドンドンメモリを圧迫する事になります。C言語はJAVAやC#の様にガベージコレクタがないので、どこからも参照されなくなったメモリを自動的に解放して再利用する仕組みを持っていません。解放してやらないと再利用されません。原則として確保した領域は自分で解放する。そのための関数が存在します。free()関数がそれです。これをしないと、バンバン遠慮なしにメモリリークしまくります。おそるべしC言語。
int *a; int b[100]; a = (int *)malloc(sizeof(int) * 100); /* sizeof演算子を利用して型のサイズを求めて領域確保、代入時はキャストが必要 */ for (int i=0;i<100;i++){ a[i]=i; b[i]=i; } for (int i=0;i<100;i++){ printf("a[%d]=%d b[%d]=%d\n", a[i], b[i]); } free(a); /* 使ったら解放 */
ローカル変数とリテラル値
ローカル変数は先ほどいったスタック領域に存在します。その為、関数が終了されると解放されます。なので、ローカル変数のアドレスを返却すると関数を抜けた瞬間に解放されてしまいます。ですが、関数の内部でmallocなどで動的に確保された領域はヒープ領域で確保されているので、関数が終了しても確保されたままなので利用可能です。
また、リテラル値に関しては静的領域に配置されるので、関数の縛りを受けなくなります。静的領域に確保されるメモリに関しては全て影響を受けないというのが正しいです(グローバル変数やstatic変数なども)。
ポインタのポインタ
さぁ、来ました。ポインタのポインタ。ダブルポインタとも言われます。
ポインタのポインタと言うとパッと聞いただけでは「???」ですが、冷静に考えると理解できると思います。ポインタのポインタは、ポインタ変数のポインタを格納する変数です。
int i; int *ip; int **ipp; i=100; /* iのアドレスを代入 */ ip = &i; /* intのポインタ変数のアドレスを代入 */ ipp = &ip; /* 100と表示されます */ printf("%d\n",**ipp);
さて、実際どんなケースで使ってたかなぁと思って色々と思い出してみましたが、なんだったっけなぁと。思い出せない(笑)
例えば文字列の配列
文字数が可変なので、二次元配列で保持したくないとかそんな時。
char **cp; cp = (char **)malloc(sizeof(char *) * 3); cp[0] = (char *)malloc(sizeof(char) * 5); strcpy(cp[0],"abcd"); cp[1] = (char *)malloc(sizeof(char) * 6); strcpy(cp[1],"12345"); cp[2] = (char *)malloc(sizeof(char) * 7); strcpy(cp[2],"opqrst");
こんな感じですかね。もっと実用的に使っていた様な・・・う~んう~んどんなケースだったっけ。
C言語とポインタ
C言語を使う上でポインタを使わないと言う事は無理な話ですからねぇ。とは言え基本さえ抑えれば難しくはないんですよね。メモリの配置とかイメージ出来れば使える代物なんですが、それが無いと、コンピュータのアドレスを使ってうんぬんかんぬんみたいに難しくなってしまうので、まずはメモリとかそういうところなんだなぁと思いました。
コメント