解説書とかネット記事とかを読んでも、例え話が多くてイマイチ分からないかと思ったので、例え話を入れずに素朴に解説していこうという記事シリーズになっています。
今回記事はその第4回です。
第3回までのリンクは以下にまとめておきますので、気になる方はどうぞ~。
- C言語のポインタを、元々の必要性から解説してみる ~そもそも何がしたいの?~
- C言語のポインタを、元々の必要性から解説してみる ~プログラムとメモリの関係~
- C言語のポインタを、元々の必要性から解説してみる ~スコープとポインタの関係~
とは言え、すべてを読む暇がないほど忙しい方もいらっしゃるでしょうから、軽く今までの経緯を説明しときます(記事の最後にもざっくりとした流れを書いていますので、良かったらそちらも参考にしてみてください)。
今までは要するに、与えられた2つの引数を入れ替えるswap関数なるものを定義したけど、なぜか狙った通りに動作してくれないという障害にぶつかっていたわけですが、それがなぜなのかを解説していました。
流れとしては、まず第1回記事でswap関数とはどのようなもので、直観的に書くとどうなるかを確認しました。ここで、ただ直観的にswap関数を定義するだけでは狙い通りに動作しないという問題が起きることを確認しました。
続く第2回記事で、変数を定義するとメモリを使うわけだから、そのメモリ操作の部分で何か問題が起きているんじゃなかろうかということで、プログラムがメモリをどう使うかを解説しました。
第2回記事の一般論を受けて、第3回記事(前回記事)ではじゃあswap関数では何が起こっていたのかを解説しまして、この第4回記事につながっております。
今回は、上手く狙い通りの動作をしてくれるswap関数を書いていきます。といっても、巷に出回っているC言語の入門書みたいな解説ではなく、前回記事みたいなプログラムがメモリをどう使っているかという観点から解説していきます。
もし、何かC言語の入門書がお手元にあれば、その本と読み比べていただければより理解が深まるのではないかと思います(当然、なくても構いませんが)。
というわけで、本題に移りましょー٩( ‘ω’ )و
※解説中に「前回記事」という単語を使いますが、特に断らない限り、それは第3回記事のことを指しています。時系列上の前回記事(テキスト分析でこんなことが分かる! ~あなたは『ハーモニー』(映画版)を気に入るか?~)ではありませんのでご注意ください。
前回記事時点での問題
前回記事に書き洩らしがあった(正確には、説明が不十分で、話のつながりが分かりづらくなってた)ので、ここで補足しておきます。
何かというと、前回記事でスコープという概念があるからポインタを使う必要があるんだと説明したのですが、その論理が飛躍してたなと思いまして、その補足をします。
上でも軽く説明しましたが、一応、前回記事のざっくりと下内容説明を。前回記事ではプログラムがメモリをどう使っているのかを解説しまして、その中でスコープという概念も説明しました。
スコープというのは、main関数内で定義された変数はmain関数内でしか変更できない、swap関数内で定義された変数はswap関数内でしか変更できないって話でした。要するに、ある関数内で定義された変数は、その関数内でしか変更できないという話ですな。
なぜこのようなことが起きるかというと、変数にはそれぞれスコープというものが決められていて、そのスコープの中だけでしか名前を参照できないと決まっているからです。
つまり、第1回記事のswap関数のコードを修正するときに、単にswap関数内で使う変数の名前をaやbに変更してもダメだってことですな。
しかし、スコープという関数の壁を乗り越えられる方法がありまして、それがポインタになります。
というのも、先ほどの関数内で変更できるのはその関数内で定義された変数だけというルールには条件があります。どんな条件かと言うと、「変数名を介した変更はその関数内でしかできない」という条件になります。
だから、ポインタを使って変数名を介さない変数操作をすればいい → じゃあ番地を指定しよう → 番地をどうすれば指定できるか? → ポインタだ!って話になってます。
つまり、ある関数で定義された変数の”名前”はその関数内からしか見えないってことですな。それでスコープ(視認範囲)という名前になっているのだと。
正しいswap関数
前回記事で、swap関数を実現するためには呼び出し元の関数が保持している数値をコピーして操作するのではなく、元の関数が保持しているメモリの場所を直接操作する必要があると説明しました。
ということで、シリーズ第1回記事で書いたプログラム(狙った通りの動作をしてくれなかったプログラム)を修正してみます。結論から言えば、修正後のプログラムは次のようになります。
#include <stdio.h>
void swap( int *, int * );
int main()
{
int a, b;
a = 1123;
b = 2358;
printf( "a = %d, b = %d\n", a, b );
swap( &a, &b );
printf( "a = %d, b = %d\n", a, b );
return 0;
}
void swap( int *i, int *j )
{
int temp;
temp = *i;
*i = *j;
*j = temp;
return;
}
ここで、swap関数の定義部分と呼び出し部分が変わっていることに注目してください(プロトタイプ宣言もそれに伴って変わっていますが、ここでは考えません)。
まず、修正前のswap関数を確認しておきます。定義部分と呼び出し部分を抜き出してみるとそれぞれ次のようになっていました。以下に示す2つのソースコードの内、一つ目が呼び出し部分、二つ目が定義部分です。
swap( a, b );
void swap( int i, int j )
{
int temp;
temp = i;
i = j;
j = temp;
return;
}
それが修正後はそれぞれ次のようになっています。
swap( &a, &b );
void swap( int *i, int *j )
{
int temp;
temp = *i;
*i = *j;
*j = temp;
return;
}
これらのコードを見比べていただければわかる通り、修正後は変数の最初に*が付いていたり、&が付いていたりします。これらを付けられた変数がそれぞれ、「ポインタ」、「番地(アドレス)」と呼ばれているものです。
C言語では、アドレスを利用するに当たって約束事が取り決められています。ここで覚えていただきたい約束事は2つで、
- &を変数に付けると、その変数のアドレス(前回記事で言うところの通し番号)を表す
- 変数のアドレスは*を付けた変数(つまりポインタ変数)で受けなければならない
という2つです。
そのため、アドレスとポインタ変数を利用しようと思うと、このような表記にする必要があるのです。
では、次の節からプログラムがメモリをどう使うのか、前回記事のように動作を追いかけていきましょう。
ポインタはメモリ操作をどう変えるか?
前回記事では、修正前のswap関数がメモリをどう操作しているかを図で表して解説しました。なので、今回記事では修正後のswap関数について解説していきます。
まず、前回記事までと同様にプログラム実行前までの状態を考えます。すると、次の図のようになっているはずです。
※以降の図についての断り(要するに、前回記事までと同様の見方をしてくださいと言っています)
1. 本来ならメモリはもっとたくさん用意されているのですが、便宜上、その内の説明で必要な部分だけを抜き出しています。
2. 図では、各変数が隣接しているかのように描いていますが、実際には隣接することは保証されていません。隣接した変数が必要だったら配列を使う必要があります。
3. 図中の通し番号というのは、その変数がメモリの先頭から何番目にあるか(つまり、変数の番地)を表しています。
では、ここからswap関数の実行直前までプログラムが進んだとしましょう。すると、次の図になっているはずです。
ここまでは前回記事と同じです。変わってくるのがここからです。ここからが本記事のハイライト、そして本シリーズのハイライトでもあります。
swap関数が実行された直後を考えてみます。結論から言えば次の図のようになっています。
さて、何が起こったのでしょうか?その疑問は後で解決しますので、その疑問を頭に残したままとりあえずプログラムの続きを追いかけていきます。
続いてint temp;
が実行されるので、次の図のようになります。
メモリ上にtempという名前で変数が確保されただけですな。この操作は前回記事と同じです。
次の図はtemp=*i;
を実行した直後の図になります。「これはどういうことだ?」と思われる方もいらっしゃるかもしれませんが、この図の後で説明するので、その疑問は一旦頭の端に追いやっておいてください。
*iには10が入っているのに、tempに入っているのは10になっていません。先ほども、swap関数を呼び出したとき、aの値は1123だったのに、*iは1123ではありませんでした。何が起こっていたのでしょうか?
これらのもっとも端的な答えは「C言語の決まりだから」ということになります。
どういうことかと言えば、「変数名の前に*が付いていると、その変数には何か別の変数のアドレス(図で言うところの通し番号)しか入れられない」とC言語では決められているのです。なので、*iや*jにはmain関数で宣言された変数aとbの通し番号(アドレス)が入っています。
要するに、「ポインタ変数にはアドレスしか入れちゃダメよ」ってことですな。上のアドレスに関する約束事の2つ目をポインタ視点で書き表したものになります。入門書でも解説されていることをちょっと堅苦しく表現してるだけですな。
今説明した約束事は、ポインタ変数への代入時の約束事でした。変数への代入時だけではなく、ポインタ変数からの読み取り時にも約束事があります。
それは、「ポインタ変数を呼び出したとき、変数名の前に*が付いている場合は、その番地が指し示す具体的な値を読み取らせなさい。変数名の前に*が付いていない場合は、その番地そのものを読み取らせなさい」という約束事です。
要するに、ポインタ変数の呼び出し方には2種類の方法があって、*を付けて呼び出す方法と、*を付けないで呼び出す方法があると。そして、その2種類のどちらの方法で呼び出すかによって意味合いも変わってきて、
*が付いている場合 → その番地の先にある具体的な値が分かる
*が付いていない場合 → その番地そのものが分かる
というように決められているってことですな。
ここまでC言語の約束事を理解していただいたうえで、上の図で何が起こっていたのかを解説していきます。
swap関数が実行された直後、2つの変数*iと*jが確保されました。そして、それぞれに&aと&b(つまり、aとbの番地)が格納されます。そして、その後temp=*i;
が実行されたとき、iはポインタ変数で、かつ*付きで呼び出されているので、ポインタ変数呼び出し時の約束事から*iは具体的な数値を返すことになります。
ここで、*iに格納されているアドレスは10です。つまりこの場合、tempにはアドレスが10の入るということになります。アドレスが10のところの数値は1123です。
つまりこの場合は、temp=*i;
という文はtemp=1123;
と同じ働きをすることになります。
かくして、上の図のようなことが起きたわけです。
さて、ポインタ変数操作に関する約束事を解説しましたので、続きも見ていきましょー。
プログラムは全体としてメモリをどう使うか?
上の節と同様に、プログラムの動作を追いかけていきます。ただし、その際も上で書いた2つの約束事
- 変数名の前に*が付いていると、その変数には何か別の変数のアドレス、図で言うところの通し番号しか入れられない
- ポインタ変数が*付きで呼び出された場合は、その番地が指し示す具体的な値を読み取ることになる。*なしで呼び出された場合は、そのポインタ変数の番地そのものを読み取る
を忘れないようにしてください。
では、次に*i=*j;
が実行された後の状態を図で表してみます。次の図のようになります。
ポインタに関する約束事から、iのアドレスの先にある値がjのアドレスの先にある値2358で置き換えられました。
さらに*j=temp;
が実行されると次の図のような状態になります。
先ほどと同様に、jのアドレスの先にある値がtempの値1123で置き換えられています。
次に実行されるのはreturn;文です。
ここで、前回記事、前々回記事を思い出してください。何を思い出していただきたいかというと、return;
文が実行されて、処理がmain関数に戻るとメモリがどうなるかです。
return;
が実行されると、その関数(今の場合はswap関数)で定義された変数はすべて破棄されてしまうのでした。そのことを図に表してみると、次のような感じになります。
ここで、最初の状況と見比べてみてください。main関数で定義されたaとbの値が入れ替わっています。実際、2つ上の節で修正したswap関数のプログラムを実行すると、次のような結果になってaとbの数値が入れ替わることが確認できます。
a = 1123, b = 2358
a = 2358, b = 1123
当然、前回記事のメモリの模式図と比べていただいても、最終的な結果が違っていると確認できます。
これで、めでたくswap関数を定義することができました。
まとめ
さて、長らく解説してきましたポインタですが、これで一段落です。考え方自体はご理解いただけたかと思うので、後はご自身で考えながら自分の物にしていってくださればと思います。
とは言え、すべての記事にもう一度目を通すのは大変かと思いますので、解説の流れを振り返っておきます。
- swap関数を作ろうとしたが、上手くいかなかった(障害)
- そもそも変数操作はメモリ操作(命題1)なんだから、メモリ操作時に何が起きてるか確認しよう
- そのために、メモリをとても長いテープとして考えると分かりやすい(第2回記事)
- じゃあこれでメモリ操作を理解できる!…わけではない
- swap関数を上手く動作させるにはスコープなる概念を理解する必要があるから(第3回記事)
- スコープとは変数名の視認範囲のことだ!つまり、変数名を介して変数操作できる範囲のことだ!
- だったら変数名を介さずに変数を変更すればいいじゃないか
- もっと言えば、変数の場所を指定してその数値を変えればいい
- 変数の場所を指定する物とは何か? ー ポインタだ!
- じゃあ、ポインタを使うと具体的に何がどう変わるの?(第4回記事)
という感じになっています。
そして、この記事では主にアドレスとポインタの具体的な使い方と、その結果、プログラムがどう動作するかを解説しました。動作の方は書くと長くなってしまうので割愛しますが、途中で出てきた約束事はまとめておきます。
まず、約束事にはアドレスに関するものとポインタ変数に関するものという2種類がありました。
アドレスに関する約束事は、
- &を変数に付けると、その変数のアドレス(前回記事で言うところの通し番号)を表す
- 変数のアドレスは*を付けた変数(つまりポインタ変数)で受けなければならない
ポインタ変数に関する約束事は、
- 変数名の前に*が付いていると、その変数には何か別の変数のアドレスしか格納できない
- ポインタ変数が*付きで呼び出された場合は、その番地の先にある具体的な値を、*なしで呼び出された場合は、そのポインタ変数の番地そのものを返す
となっていました。まとめは以上になります。このまとめを含め、一連のシリーズ記事が皆様の学習の役に立てば幸いです。
次回記事ではこの一連の解説で長らく使っていたswap関数を(無理やり)活かしたプログラムの例を提示してみたいと思います。
とは言え、ポインタを理解するのに必要な知識や考え方はこの記事までですべて解説したので、次回記事は完全におまけなんですが。
というわけで、今回はこの辺で。ではでは~
P.S. ちなみに、ポインタ以前にC言語を勉強したいという方は、以下の「猫でもわかるC言語プログラミング」という本がおすすめです。
この本のおすすめポイントは3点ありまして、
- 中学生時代の僕でも理解できる程分かりやすく書かれてる
- 扱っている範囲が広い割に分量が少なく、かつ必要な情報はすべて書かれている
- ソースコードの例が大量
という3点です。
プログラミングを勉強したいんだけど、毎回途中で挫折しちゃうんだよなぁって方には特におすすめです。なんといっても、かなりまとまってるんで、挫折する前に次の話題が来てくれるから、飽きずに読み進められるんですよね。
しかも、分かりやすいから理解した上で次に進めるのがまたありがたいという感じの本です。
ただ、ポインタについては、僕はこの本だけでは理解できませんでした。ですが、もしポインタでつまづいたとしても大丈夫です。この記事シリーズがありますから。