スポンサーリンク

C言語のポインタを、元々の必要性から解説してみる ~スコープとポインタの関係~

記事内に広告が含まれています。

研究室の同僚に、「C言語ポインタが分かるんだけど分からないから教えて」と言われたことで始まった本シリーズですが、今回の記事は第3弾になっています。

第1、2弾は以下の通りになってます。

この記事の最初の節で上2つのざっくりとしたまとめを書いているので、最初の節を読んでみてもっと詳しく知りたいと思ったら、上記の記事をどうぞ~。

スポンサーリンク

今までの話

まず、シリーズ第1回目の記事ではswap関数を作ってみたものの、上手くいかないという現象に遭遇しました。C言語の解説書を読んでいる方には既視感のあるプログラムだったかと思います。ソースコードは後ろで再掲してるので、どんなプログラムだったか忘れたって方は、次の節か第1回目の記事でご覧ください。

そして、これがポインタの解決したい問題の一つでした。

その解説を進める中で、変数を操作するとはつまりどういうことなのかも解説しました。そこで、変数操作とはメモリ操作のことであるという意味の命題を定義しました。

以上が第1回記事の内容でした。swap関数が上手くいかなかったということは、変数操作に問題があるはずだろうということで、第2回記事に続きます。

第2回記事では、変数操作とはメモリ操作なのですから、変数操作の問題を解決したければ、そもそもプログラムがメモリをどう使っているのかを理解しないといけません。

ということで、変数を宣言したときにメモリ上のある領域が変数名と紐づけられて、その後はその変数名を介してメモリを操作するということを、メモリを長いテープに見立てて解説しました。

そして、関数が新しく呼び出されたときに、変数がメモリ上に新しく確保されて、呼び出し元に処理が戻るときに、その変数は破棄されるのでした。

ここまで来れば、メモリの使われ方とswap関数との間にはどんな関係があるのかが気になる方も多いかと思います。というわけで、それを解説するのがこの第3回記事です。

swap関数が上手く動作してくれなかった理由

ここで、第2回記事の結果を踏まえて、第1回記事で命題1などと仰々しく名前を付けた一文「変数への数値の代入や、変数からの数値の読み取りとは、メモリ操作である」をもう少し書き直してみます。

命題1’「変数の数値の読み書きとは、変数名を介したメモリ操作である」

かなり短くなったように感じますが、本質的には、命題1に”変数名を介した”という部分を付け加えただけです。前回記事をお読みの方なら、変数名を介してメモリを操作するということの意味を理解できるかと思います。

では、前回記事と同じようにswap関数ではどのようなことが起きていたのかを見ていきます。プログラムと一緒に解説した方が分かりやすいと思うので、第1回記事のswap関数のプログラムを再掲します。

#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;
}

このプログラムを実行すると、メモリ上でどのようなことが起きるのかを模式図で見ていきます。

プログラム実行前は前回記事と同様に、プログラム中のどの変数もメモリ上にないという状況です。次の図のような感じ。

図1. プログラム実行前のメモリの状態

今回は、合計で5つの変数が必要なので、それに応じてメモリ上から5つ分を抜き出しているのだとお考え下さい。

ここから、変数の宣言や変数の変更があったときにメモリ上の数値がどう変わるのかを見ていきます。プログラムが実行されて、main関数中のswap関数の直前まで処理が進むと、メモリは次のような状況になります。

図2. swap関数直前のメモリの状態

前回記事ではmain関数内で宣言されていた変数は1つだったのに対し、こちらのプログラムでは2つの変数が宣言されているというような個数の違いや、そこに確保されている数値の違いなどはありますが、ここまでは前回記事と同じです。

今回の模式図でも前回記事と同様に、変数がメモリ上で連続しているように描きますが、実際には各変数がメモリ上で連続しているという保証はないのでご注意ください。前回記事でも同様の注意をしましたが念のため。

次に、swap関数が呼び出された時点で次のような状況になります。引数であるiとjがメモリ上に確保されるわけですね。swap関数が呼び出された時点を考えているので、まだint temp;まで処理が進んでいない状況を考えています。

図3. swap関数実行直後のメモリの状態

次に、int temp;からreturn文まで実行されたとします。そのときの変化は次のようになります。矢印の上に書いてある文を実行したら、メモリの状況が矢印の先のようになると思ってください。要するに、一つ矢印の先に進む度に処理が一行ずつ進んでるってことですな。

図4. swap関数実行中のメモリの状態の変化

メモリの最後の状況を見ていただくと分かりますが、本来なら入れ替わっておいてほしい変数aとbが入れ替わっていません。そして、次に実行される文はreturn文になるので、main関数に処理が戻されることになります。

前回記事で、times_two関数からmain関数へと処理が戻されたときに、times_two関数で宣言された変数が破棄されると説明しました。

swap関数でも同じことが起きて、処理がswap関数からmain関数に戻されるときに、swap関数で宣言されていた変数がすべて破棄されます。すると、次の図のようになります。

図5. main関数に処理が戻された直後のメモリの状態

本来ならaとbの値を入れ替えてたかったのですが、この図を見てみるとswap関数が終了しても実際にはそうならないことが分かります。

これが、swap関数がこちらの想定した通りに動作しなかった理由になります。

では、どうすればこの問題を解決できるでしょうか?

スポンサーリンク

変数はどこから変更できる?

一番単純な解決策は、上のaとbを直接入れ替えるということでしょう。

しかし、ここで問題になるのが”スコープ”という概念です。

変数には、その変数名を介して数値を読み書きできる関数が決められています。その関数というのは基本的には、その変数を宣言した関数のみです。

このような、ある変数の変数名を介した変更が可能な場所のことを”スコープ”と呼びます。「変数名を介したアクセスが可能な場所」と言った方が分かりやすいかもしれませんね。

例えば、前回記事でのtimes_two関数の例で考えてみます。前回記事のプログラムを次に再掲しておきます。

#include <stdio.h>

int times_two( int i )
{
  return i * 2;
}

int main()
{
  int a;
  a = 10;

  printf("a = %d\n", a);

  a = times_two( a );

  printf("a = %d\n", a);

  return 0;
}

このプログラムを見てみると、変数aはmain関数で、変数iはtimes_two関数で宣言されていることが分かります。なので、先ほどのスコープの考え方を適用すれば、aを変更できるのはmain関数だけで、iを変更できるのはtimes_two関数だけだということになります。それを図示すると次のような感じ。

図6. times_two関数のスコープ

前回記事でも使った模式図の一番右側に、各変数に対するスコープを追加しました。

同じ考え方をswap関数にも適用してみると、次のように図示できて、変数aとbはmain関数から、変数iとjとtempはswap関数からでないと変更できないことが分かります。

図7. swap関数のスコープ

ここで、次のような問題が生まれます。「変数aとbの値を変更するにはどうすればいいか?」

この問題をもう少し詳しく言い換えてみます。

そもそも変数aとbを変更できない原因は、変数aとbとは全く別の変数を新しく確保して、swap関数ではその新しい変数のみを操作していたことでした。なので、swap関数は、変数aとbとは別の新しい変数iとjを確保するのではなく、main関数が宣言した変数aとbを直接操作するようにしなければいけません。

では、main関数が宣言した変数をswap関数から直接操作するにはどのようにすればいいでしょうか?

これが解決すべき問題だということになります。

具体的にどうすればいいか、30秒ほどでいいので少し考えてみてください。swap関数内に直接aとかbと書いてみたところで、先ほどのスコープの話がありますから、コンパイルエラーになるだけです。ということは、それ以外の方法でaとbを変更しなければいけません。

解決策

先ほどスコープの説明をしたときに「変数名を介した変更が可能な場所」と説明したことを思い出してください。つまり、変数名を介さない変更であればスコープ外からでも可能なのです。

別の言い方をすれば、スコープとはある変数を”変数名を介して”変更できる範囲を表しているということです。

もう少し詳しく説明します。

命題1’「変数の数値の読み書きとは、変数名を介したメモリ操作である」を思い出してください。

本来やりたいことは変数aとbの数値を入れ替えることなわけですが、その本質は変数aとbが表すメモリ上の数値を入れ替えたいということと同義なわけです。

ここでまた思い出していただきたいことがあります。それは、前回記事でのテープの例えです。メモリとは区切り線の引いてあるテープのようなもので、メモリ上の数値を読み書きするとはテープ上のひと箱ひと箱の数値を読み書きすることだという説明のことです。

例えばa=10;などと書けば、「aという変数名に紐づけられた通し番号を探して、その通し番号の示す位置に10と書きこめ」という意味になるのでした。

このことから考えて、メモリ上の数値を操作する方法としては、原理的に2通り存在するはずです。

1つ目は、上の例えで書いた通り、a=10;というように変数名を指定することでメモリ上の位置を指定して、その位置に数値を書き込む(もしくは読み取る)方法です。

そして2つ目は、メモリ上の位置を変数名で指定するのではなく、位置(前回記事で言うところの通し番号)を直接指定して数値を読み書きする方法です。

この2つ目の方法を実現できれば目標達成なわけですが、そのためには、変数の確保されている位置を知る必要があります。そして、変数の確保されている位置に対して操作を出来る必要があります。

それらがアドレスとポインタと呼ばれるものです。

つまり、ポインタとは、変数名ではなく位置を介したメモリ操作を実現するためのものなのです。

まとめ

この記事では、swap関数では何が起こっていたのか、そしてなぜこちらが望んだとおりに動作しなかったのかを解説しました。

そこから、swap関数スコープとは何か、ポインタとはどのようなもので、なぜ必要なのか、ポインタとスコープの間の関係

結局言いたかったことは単純で、「swap関数を上手く動作させるためにはポインタを使えばいい」ということなのですが、そんなことはこの記事をお読みの皆様ならご存知かと思います。

ですが、どうしてswap関数を作ろうとするとポインタなるものを使わないといけないのか、どうしてポインタを使わないと意図した通りの動きにならないのかはあまり理解できていないという方もいらっしゃったのではないでしょうか。

ということで、この記事では結局言いたかったことは何かというよりも、そこに至るまでの過程の方を重視していただければと思います。

というわけで、理屈の方は大体説明できたので、次回はswap関数の具体的な修正版と、そのプログラムを実行しているときに、メモリ上では何が起きているかを見ていこうかと思います。

というわけで、今回はここまで。ではでは~

P.S.
この記事の内容を理解しておけば、なぜ文字列を扱うときにはポインタが出てくるのかも理解できるかと思います。

タイトルとURLをコピーしました