注意
- この文章はC言語の入門書などと一緒に読むことを念頭に書かれています。
- もし入門書を持っていない場合は、苦しんで覚えるC言語(通称:苦C)を参考にするのがおすすめです。
初心者向けの具体例も多く、理解の助けになるはずです。
- もし入門書を持っていない場合は、苦しんで覚えるC言語(通称:苦C)を参考にするのがおすすめです。
- この文章はC言語の本質をイメージとして捉えるためのものです。用語の正確性は二の次となっていますがご了承ください。
- この文章はあるDiscordサーバーでの説明を元にしているため、少し砕けた表現になっている場合があります。
C言語の本質は「メモリを操作する」こと
C言語は、良くも悪くも「メモリを操作する」ことが本質の言語です。
たとえば、C言語の配列は連続したメモリ領域を確保して、そこに順番にデータを記録したものでしかありません。
実は、配列は先頭のアドレスのポインタを間接参照(後述)しているのとほぼ同じで、アドレスを添え字表記で「データのサイズ × 個数」分動かしているだけだったんですね。
また、文字列も1バイトしか格納できない文字型の配列という、単純に文字列をそのままメモリ上に書き込んだものになっています。
具体例
もう少し具体的に見ていきましょう。
int nums[3] = {10, 20, 30};
と書き込んだらそのままメモリ上にこの値を並べている、というのがC言語の配列です。
この nums
というのはその最初の要素(今回の場合は 10
が格納されている場所)のアドレスを格納した、ポインタのようなものなんですね(厳密には多少異なりますが、そこは知りたい人だけ調べてください1)。
nums[1]
と書いて2つ目の要素を指定したら、単純に *(nums + 1)
と書いたのと同じことになります。
後者の書き方ができるのは、C言語ではポインタに加算を行うと型のサイズ分だけメモリ上を移動することができるからです。
..... [10] [20] [30] .....
^ ^ ^
| | |
nums[0] nums[1] nums[2]
*(nums + 0) *(nums + 1) *(nums + 2)
大雑把ですが、こんな感じに左から右に並んでいる様子をイメージしてみると、わかりやすいかもしれません。
なので、C言語を理解するには「メモリを操作する」ということを理解すればいいんですね。
メモリと型のイメージを掴もう
とは言ったものの、メモリの構造なんて具体的には知る必要はありません。
0
もしくは 1
のビットを記録したデータがひたすら並んでいて、それぞれに「アドレス」という住所がついているとだけ理解していれば十分です。
8ビットを1バイトとし、1バイトごとにアドレスがついている処理系では、以下のようなイメージになります。
ビット列: 0 1 1 1 0 1 0 1 0 1 1 1 0 0 0 1 0 1 1 1 0 0 0 1 1 1 0 1 0 1 0 1 ...
↑ ↑ ↑ ↑
アドレス: 0x1000 0x1001 0x1002 0x1003
C言語では、それを色んな「型」で扱います。
たとえば int32_t
なら符号付き32bit整数なので、32個のビットを符号付きの整数として扱っているわけです。符号に1ビット使っていて、残りは2進数で数値を表しているわけですね。
もちろん char
型なら1バイト(通常は8ビット)を1つの文字として扱うということになります(マルチバイト文字のような例外もありますが、ここでは詳しくは触れません)。
ポインタ変数
そして、ポインタなら32bit or 64bit(CPUのビット数がこれにあたります)の格納されたデータをメモリ上のアドレス(住所)として扱っているだけ、ということになります。
つまり、アドレスを変数の中に入れている。
もっと言えば、メモリに書き込んでいるわけです。
具体例
こちらも具体例を挙げて見ていきましょう。
まず、a
、p
、pp
の3つの変数を用意してみました。
int a = 42;
int* p = &a;
int** pp = &p;
a
は普段から見慣れている int
型の変数ですね。
&a
とすることで a
のアドレスを取得することができます。
p
は int*
となっているので int
型のアドレスを指すポインタ変数です。
入門書などで何度も見たという方もいらっしゃるかもしれません。
pp
は int**
と2つ *
がついていますが、これは int*
型のアドレスを指すポインタ変数です。
ポインタのポインタということで、ダブルポインタと呼ばれます。
では、これらの対応関係を簡単なイメージにしてみましょう。
[0x1000] => 42
↑ a のアドレス ↑ a の値(int 型の数値)
[0x2000] => 0x1000
↑ p のアドレス ↑ p の値(= a のアドレス)
[0x3000] => 0x2000
↑ pp のアドレス ↑ pp の値(= p のアドレス)
どうでしょうか。
ポインタが変数の中にアドレスを格納したものである、ということの意味がわかりやすくなったのではないでしょうか。
ポインタ変数では *p
とすることでポインタが持っているアドレスが指す先の値(今回の場合は a
の値)にアクセスすることができ、これを間接参照と呼びます。
そのため、
printf("%d\n", a );
printf("%d\n", *p );
printf("%p\n", (void*)*pp);
として実行すれば
((void*) は実行に必要なキャストにすぎないため、一旦無視してください)
42
42
0x1000
となります。
これは a = 42
、*p = a = 42
、*pp = p = &a
となるからです。
スタックとヒープ
鋭い方はお気づきになったでしょうが、このままでは、そのポインタ変数の場所もメモリ上に書き込む必要があるわけで……という感じで、ブートストラップ問題のような無限ループが発生してしまいますね。
もう少しかみ砕いて説明すると、変数の入れ物(メモリ)を用意しようにも、その入れ物を使うために別の変数を確保する必要があるとなれば、「鶏が先か、卵が先か」という話になってしまうわけです。
これを、C言語ではスタックという機能で解決します。
スタック
関数が呼び出されるときに自動的に確保され、関数が終了すると自動的に解放される領域です。
int x = 10;
既にC言語の勉強を始めている人は、こんな変数の定義を書いたことがあるのではないでしょうか。
実は、これは x
という変数をスタック領域に確保しています。
スタックは自動で変数のアドレスを覚えておいて、変数名だけで使わせてくれる大変便利な領域なのですが、スタックのサイズは小さいため、全てのデータを置いておくことはできません。
なので、ここでは必要最低限の情報を覚えてもらっておいて、大きなデータは任意の場所に置いて参照するのが基本です。
ヒープ
その大きな領域が、ヒープと呼ばれるものです。
ヒープは自分で確保・解放が必要ですが、その代わりメモリ上に空きがあればいくらでも確保できる領域です。
int* p = malloc(sizeof(int)); /* int型のサイズでメモリを確保 */
*p = 20;
︙
free(p); /* 使い終わったら必ず解放 */
このように、malloc
などの関数を用いて確保します。
ヒープ領域は特に解放に関して、メモリリークや二重解放といった色々な面倒があるのですが、その辺りは malloc
や free
について調べれば山のように情報が出てくるので、ここでは触れません。
あとがき
ここまで理解しておけば、ポインタも malloc
も怖くないし、memset
や memcpy
などの関数が何をしているのかも、イメージできるようになってくると思います。
ここまで読んでくれた人ならおそらく、C言語の本を一冊か、苦Cを読めばC言語の中級者くらいまでの内容はすんなり学ぶことができるのではないでしょうか。
もし難しく感じた方は、まず配列くらいまで苦Cか書籍を見ながら勉強してみて、ポインタが出てきたあたりでもう一度この記事に戻ってきてくれれば、きっとわかりやすくなるのではないかと思います。
蛇足
Qiitaへの投稿はこれが初めてですが、結構ブログっぽい投稿でもいいという風潮があるのが書きやすくていいですね。
-
ChatGPTにその辺りのことを聞いてみた結果を貼り付けておきます。正確性はたしかではありません(私がC言語の仕様書を読んでいないため)が、ポインタを理解できていれば、ざっくりイメージを掴んだり調べるとっかかりにするくらいの役には立つでしょう。 ↩