🖥️

ポインタが怖くなくなる!C言語メモリ操作の基本概念

に公開

注意

  • この文章はC言語の入門書などと一緒に読むことを念頭に書かれています。
  • この文章はC言語の本質をイメージとして捉えるためのものです。用語の正確性は二の次となっていますがご了承ください。
  • この文章はあるDiscordサーバーでC言語について説明した内容をまとめ直したものです。そのため、読みやすい文章になっていない可能性があります。また、適切な図表もありません。

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)  *(nums + 1)  *(nums + 2)

大雑把ですが、こんな感じに左から右に並んでいる様子をイメージしてみると、わかりやすいかもしれません。

なので、C言語を理解するには「メモリを操作する」ということを理解すればいいんですね。

メモリと型のイメージを掴もう

とは言ったものの、メモリの構造なんて具体的には知る必要はありません。

0 もしくは 1 のビットを記録したデータがひたすら並んでいて、それぞれに「アドレス」という住所がついているとだけ理解していれば十分です。

C言語では、それを色んな「型」で扱います。
たとえば int32_t なら符号付き32bit整数なので、32個のビットを符号付きの整数として扱っているわけです。符号に1ビット使っていて、残りは数値を表しているわけですね。

もちろん char 型なら1バイト(通常は8ビット)を1つの文字として扱うということになります(マルチバイト文字のような例外もありますが、ここでは詳しくは触れません)。

ポインタ変数

そして、ポインタなら32bit or 64bit(CPUのビット数がこれにあたります)の格納されたデータをメモリ上のアドレス(住所)として扱っているだけ、ということになります。

つまり、アドレスを変数の中に入れている。
もっと言えば、メモリに書き込んでいるわけです。

スタックとヒープ

鋭い方はお気づきになったでしょうが、このままでは、そのポインタ変数の場所もメモリ上に書き込む必要があるわけで……という感じで、ブートストラップ問題のような無限ループが発生してしまいますね。

もう少しかみ砕いて説明すると、変数の入れ物(メモリ)を用意しようにも、その入れ物を使うために別の変数を確保する必要があるとなれば、「鶏が先か、卵が先か」という話になってしまうわけです。

これを、C言語ではスタックという機能で解決します。

スタック

関数が呼び出されるときに自動的に確保され、関数が終了すると自動的に解放される領域です。

int x = 10;

既にC言語の勉強を始めている人は、こんな変数の定義を書いたことがあるのではないでしょうか。
実は、これは x という変数をスタック領域に確保しています。

スタックは自動で変数のアドレスを覚えておいて、変数名だけで使わせてくれる大変便利な領域なのですが、スタックのサイズは小さいため、全てのデータを置いておくことはできません。

なので、ここでは必要最低限の情報を覚えてもらっておいて、大きなデータは任意の場所に置いて参照するのが基本です。

ヒープ

その大きな領域が、ヒープと呼ばれるものです。

ヒープは自分で確保・解放が必要ですが、その代わりメモリ上に空きがあればいくらでも確保できる領域です。

int* p = malloc(sizeof(int));  /* int型のサイズでメモリを確保 */
*p = 20;free(p);  /* 使い終わったら必ず解放 */

このように、malloc などの関数を用いて確保します。

ヒープ領域は特に解放に関して、メモリリークや二重解放といった色々な面倒があるのですが、その辺りは mallocfree について調べれば山のように情報が出てくるので、ここでは触れません。

あとがき

ここまで理解しておけば、ポインタも malloc も怖くないし、memsetmemcpy などの関数が何をしているのかも、イメージできるようになってくると思います。

ここまで読んでくれた人ならおそらく、C言語の本を一冊か、苦Cを読めばC言語の中級者くらいまでの内容はすんなり学ぶことができるのではないでしょうか。

もし難しく感じた方は、まず配列くらいまで苦Cか書籍を見ながら勉強してみて、ポインタが出てきたあたりでもう一度この記事に戻ってきてくれれば、きっとわかりやすくなるのではないかと思います。

蛇足

Zennへの投稿はこれが初めてなのですが、緊張しますね。

脚注
  1. ChatGPTにその辺りのことを聞いてみた結果を貼り付けておきます。正確性はたしかではありません(私がC言語の仕様書を読んでいないため)が、ポインタを理解できていれば、ざっくりイメージを掴んだり調べるとっかかりにするくらいの役には立つでしょう。 ↩︎

Discussion