自由研究

例え話をしないC言語のポインタの説明

投稿日:

まえおき

巷では「プログラマーになりたい人に初学者にとって、ポインタという考え方がわけわかめ」という話がよくあります。
そこでいろいろな人が「ポインタは住所だ」とか「変数がハコで」とか手を変え品を変え分かりやすいように説明してくれています。
それでもなお「ポインタがわかりづらい」という人が後を絶ちません。
もういっそのこと、例え話をやめてド直球で攻めたらいいんじゃないでしょうか。

Hello, Worldより簡単に

サンプルコード

以下のコードを考えます。

void main()
{
  int a;
  int b;
  int c;

  a = 1;
  b = 2;
  c = a + b;
}

「#include <stdio.h>」なんていう謎のオマジナイはこの際ナシです。あんなもの無くたってC言語は成り立ちます。
まぁ見ての通り、どこにも何も出力されませんが。

このプログラムは、「c = a + b」という計算を行います。このとき、変数aが1で変数bが2です。ここまでは良いですね?
さて、この「変数c」というのは、いったいどこにあるでしょうか

変数の置き場所

答えはメモリです。
コンピューターの世界なんて、突き詰めればほとんどのものがCPUかメモリかストレージかネットワークのどれかです(暴論)。

「変数c」というものが、メモリのどこかにあります。
「メモリってなんなのかわかんないよー」という人は、パソコン屋さんに行ってメモリの実物を見てくるといいでしょう。DDR4だのDIMMだの難しい言葉と一緒に売られています。
とにかくあの、パソコンショップで売られている緑の板に黒い四角いのが付いたアレ、あのメモリの中に「変数c」というのが入るわけです。

ふだん、「変数cがどこにあるか」というのは、プログラマーが気にするべき部分ではないです。
pythonだのなんだの最近の流行の言語では特にそうです。そういう低レベルなお話を気にせずにプログラミングできるようになっています。
でも、ポインタの話をしたいなら別です。
変数cがどこに入っているか気になる!」というのがポインタの本質であり、ポインタの存在意義です。

変数が入っているところを見てみる

せっかくです。変数cが実際におかれている場所を見てみましょう。
以下の手順は別にどんな操作をやっているかわからなくても良いです。
さっきのコードを適当に保存して、gccに-gオプションをつけてコンパイルしてしまいます。
$ gcc -g test.c

できたバイナリをgdbで開いて、main関数にブレークポイントを張ってから実行しましょう。
$ gdb a.out
(gdb) b main
Breakpoint 1 at 0x4004f1: file test.c, line 8.
(gdb) run
Starting program: /home/givemegohan/tmp/a.out

Breakpoint 1, main () at test.c:8
8 a = 1;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64

main関数のアタマで止まりました。
さて、変数cの場所を確認します。
(gdb) p &c
$1 = (int *) 0x7fffffffe494

変数cは、メモリ上の「0x7fffffffe494」という場所にあるようです。
メモリ上の場所のことを「アドレス」と言います。

さあ、混乱してきましたね。
とりあえず初学者の皆さんは以上のことを深く考えなくても良いです。
とにかく変数がメモリ上にある、ということを覚えてください。

C言語とメモリ

変数がメモリ上にあることがわかりました。
さて、C言語は非常に低次元のプログラミング言語です。
プログラムの管理下にあるメモリは好きにいじることができます
いいですか、メモリは好きにいじれます

変数cの「まわり」を見てみる

変数cは、変数cだけがぽつんと置いてあるわけではありません。
メモリというのは前にも後ろにも連続しています。
まわりを見てみましょう。
(gdb) x/16wx 0x7fffffffe480
0x7fffffffe480: 0x00400510 0x00000000 0x00400400 0x00000000
0x7fffffffe490: 0xffffe580 0x00000003 0x00000002 0x00000001
0x7fffffffe4a0: 0x00000000 0x00000000 0xf7a39c05 0x00007fff
0x7fffffffe4b0: 0x00000000 0x00000000 0xffffe588 0x00007fff

上から2行目、左から2列目に「0x00000003」と書いてありますね。ここが変数cです。
前後にもいろいろ書いてありますね。ひとつ右の「0x00000002」が変数bで、もうひとつ右が変数aです。
それ以外にもいろいろありますが、まあよくわからないので深く考えないでおきましょう。

このように、プログラミング言語で書いた変数は、プログラムを実行するときにはメモリのどこかに割り当てられるわけです。

変数ってなんだ

変数a,b,cがメモリ上では隣り合ったアドレスに割当たっていることがわかりました。
ではここで質問です。変数というのは、コンピュータにとって何でしょう

教科書なんかには「変数っていうのはハコみたいなものでー」とか書いてありますね?
でもメモリはハコみたいなものではないです。緑の板に黒いナンカが付いたやつです。教科書に書いてあるのは例え話です。

C言語にとっての答えは、「変数とは、メモリのアドレスに付けられた名前のこと」です。
いいですか、「変数c」があるのではなくて、「0x7fffffffe494」という場所(アドレス)に「変数c」という名前を付けているんです。

アドレスはどう決まるの?

じゃあ「0x7fffffffe494」が「変数c」だと誰が決めたんでしょうか。
プログラマーではないですね?だって最初に書いたc言語のコードには、「0x7fffffffe494」なんて書いてません。

正解はコンパイラです。コンパイラが、「変数cという場所が必要なようだから、そのぶんメモリを用意しておこう。0x7fffffffe494を変数cと名付けよう」と決めているんです。なので、コンパイラの種類によっては他のメモリアドレスが割当たることもあります。
コンパイラがこういったことを考えてくれるおかげで、われわれプログラマーは「変数c」というものの正体を深く考えずにプログラミングできるわけです。

ポインタを使ってみよう

あたらしいコードを考えます。

void main()
{
  int a;
  int b;
  int c;
  int*p;

  a = 1;
  p = 0x7fffffffe48c;
  *p = 2;
  c = a + b;
}

ポインタ変数pが出てきました。
そしてこのコードは最後に「c = a + b」を計算していますが、bはどんな値も代入していませんね。さて、cには何が入るでしょう。
答えは3です

なぜ答えが3なのか

c = a + bを計算するとき、aは1です。そこは皆さん疑問がないでしょう。
そしてbは2です。なぜでしょうか。
答えは、ポインタ変数pを使って、変数bという名前のついたメモリアドレスに”2″を代入したからです。

いいでしょうか。変数とは結局のところ、メモリアドレスに名前がついているだけなんです。
メモリアドレスを別の方法で特定することができれば、C言語は好きなアドレスを書き換えることができます

今回の例では、「変数b」はメモリ上で「0x7fffffffe48c」というアドレスに割り当てられています。
なので、「0x7fffffffe48c」のアドレスのメモリを書き換えれば、変数bの値を書き換えたのと同じことになります。

…まぁ、ふつうはプログラミングの段階で変数に割り当てられるメモリのアドレスを把握するのはムリなので、以下のように書くのが自然です。

void main()
{
  int a;
  int b;
  int c;
  int*p;

  a = 1;
  p = &b;
  *p = 2;
  c = a + b;
}

ポインタとは

ポインタは、「メモリ上の他のアドレスを指す変数」です。
アドレスと言ってもしょせんは数値です。数値なので、アドレスそのものをメモリに代入することができます。
何をいっているかわかりにくいでしょうか?でも、さっき書いたコードがそのままの意味です。
p = 0x7fffffffe48c;
メモリには数値が代入できます。メモリのアドレスも数値です。なので、メモリには他の変数のアドレスを代入できます。

この仕組みは便利なので、C言語はアドレスを指すための変数(ポインタ)を特別扱いします

  1. “int*p”のように宣言するときに*をつければ、それはポインタ変数になります。
  2. “p = &b”のように&をつければ、ほかの変数のアドレスを参照することができます。
  3. “*p = 2″のように*をつければ、ポインタ変数の指すアドレスの中身を参照/更新することができます。

1と3で同じ”*”という記号を使っているくせにイミが違うのは少しヒドいんですが、まぁ、そこは慣れるしかないところです。

なぜポインタという仕組みがあるのか

それは皆さんで考えてほしいところです。
なぜなら、ポインタというのは用意された仕組みであって、それをどう使うかはプログラマーの自由であるからです。
ここでは主な用途を軽くだけ説明しておきます。

1つの関数で複数の値を返す

C言語は、関数の返り値が一つしかありません。
そのため、2つ以上の値を返却したい場合に、ポインタを利用して値を返すことになります。
例えばこんな感じに。

void warizan(int warareru, int waru, int* syou, int* amari)
{
  *syou = warareru / waru;
  *amari = warareru % waru;
}

void main ()
{
  int a;
  int b;
  int c;
  int d;
  a = 17;
  b = 5;
  warizan(a, b, &c, &d);
  // c = 3, d = 2になる
}

サイズの大きな変数を効率よく扱う

C言語は、関数の引数として値を渡す際に、「引数用のアドレス」に変数をコピーします。
int型やchar型のような小さい変数であれば問題ないですが、大きなサイズの構造体を引数に取るような関数の場合、メモリコピーにCPUを使ってしまって性能が落ちてしまいます。
どれだけ大きな構造体でも、先頭のアドレスだけ渡してしまえば効率よくやり取りできます。

変数として名前のついていないメモリを確保して、そこを操作する

C言語は基本的に、変数はすべて事前にサイズを決めて宣言しておきます。
しかし、ファイルを読み込む際など、必要なサイズがあらかじめ分からないことがあります。
そんなときmallocコールなどで動的にメモリを確保することになりますが、動的に確保したメモリには名前がついていません。
そこで、事前にポインタ変数だけは用意しておいて、あとで動的に確保したメモリをポインタ変数を経由して参照する、ということをします。

おわりに

プログラミング言語はどんどん進歩し、ポインタを必要としない言語が多くなってきています。
純粋なC言語を必要とする場面は極めて少なく、ポインタを理解しなければならない場面はほとんどなくなってきました。
しかしポインタという考え方が消えたわけではなく、どの言語にもその根底としてポインタの概念が存在し、「プログラマーにポインタを意識させないようにする」努力をしています。
ポインタを理解することで、ポインタの無い言語においてもより微細なプログラミングが可能となるでしょう。
そのためにはポインタをあやふやな理解で留めず、コンピュータのアーキテクチャから真に理解することが必要だと思います。
本記事が読者の理解の一助となれば幸いです。

-自由研究

執筆者:


  1. 匿名 より:

    参照されるのはアドレス+型なんだから、ポインターは、実体の参照。例えが必要になっちゃうけど、実体名を呼んで召喚魔法的な話にしてあげてくれると嬉しい。

  2. 匿名 より:

    素晴らしい記事だと思います。
    正直言って全部は分からないのですが、最後の三行に深く感銘を受けます。
    「とにかくそうなるんだからこう書け! だからお前はできないんだ。とにかくそう覚えて使えばいいんだよ!」とか言うやつが大嫌いです。
    意味不明な迄に長ったらしい関数とか、どう動いてるか全く知らないクエリ使いをバイナリでどんどん撲殺して欲しいです。…分かんないんですけどね。

  3. […] 例え話をしないC言語のポインタの説明 | 右や左の旦那様 […]

  4. 匿名 より:

    >じゃあ「0x7fffffffe494」が「変数c」だと誰が決めたんでしょうか。
    >(中略)
    >正解はコンパイラです。
    バイナリ実行時にOSが決めるわけではないんですね。
    勉強不足でした。有益な情報ありがとうございました。

    • 匿名 より:

      これ、自分も実行時にOSが決めていると思っていました。
      コンパイル時と実行時でメモリの状態が一致しているとは限らないので。

      • 匿名 より:

        auto変数 (staticとかregisterとかの記憶域に関する修飾がないローカル変数) であれば、
        ・リンカがメモリの配置を決め
        ・OSが実際にどこにロードするかを決め
        その2つの結果から auto 変数の場所が決まります。auto変数は実際にはスタック上に取られることが多いです。スタックの領域はOSが決めることが多いですがリンカが決めたりランタイムの初期化コードが決めたりすることもあります。そのスタックの伸縮など実際にどう利用するかはコンパイラ (が吐き出したコード) が決めます。で、一番最後の意味で「コンパイラ」が決めているということでしょう。

  5. […] – – – – – – – – – – – – 例え話をしないC言語のポインタの説明 | 右や左の旦那様 まえおき […]

  6. […] 例え話をしないC言語のポインタの説明 | 右や左の旦那様 […]

  7. […] – – – – – – – – – – – – 例え話をしないC言語のポインタの説明 | 右や左の旦那様 まえおき […]

  8. kmaebashi より:

    言及記事を書きました。よろしければ読んでみてください。
    http://kmaebashi.hatenablog.com/entry/2018/02/13/013607

  9. […] 前回、C言語のポインタに関する解説記事を書きまして、そこそこの反響を貰うとともにいくつかの指摘を受けました。 前回の記事では幾つか「ウソではないけど真実と違う」記述がありまして、その点を補足としていくつか説明します。 今回、初学者向けではなく中級者以降の人でないとナンノコッチャ度が高いのでお付き合いください。 […]

  10. […] (wepli.2) ●例え話をしないC言語のポインタの説明 (右や左の旦那様 | 哀れなぎんしゃりにお恵みを) […]

comment

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

関連記事

no image

GW自由研究 – なぜ日本の路面電車は廃れたのか

路面電車と言えば古き良き昭和の乗り物、というイメージですよね。 広島や函館など、一部ではまだ運行が続いている路線もあるものの、それらは「昭和の臭いが残っている」というような扱いです。 以前ブラタモリで …

no image

コラボカフェとコメントと私

こんにちは!バーチャルYoutuber大好きぎんしゃりさんです! きょう、以前書いたキズナアイコラボカフェの記事にコメントが来たのでご紹介して、それに関わるお話をしていきますね。 元記事の要約 前回の …

まんがタイムきらら展のイラストボード一覧

まんがタイムきらら展面白かったですね! みなさんはイラストボード見ましたか? 撮影可だったので全部写真に撮りました!どうぞ!!!! 展示での配置は公式のツイートをご参照下さい。 せっかくですので、スペ …

no image

MagicaVoxelとDMM.makeでオリジナルフィギュアを作ろう

もくじ1 読まなくても良い前書き2 概要3 MagicaVoxel3.1 フィギュアの自立の有無3.1.1 基本的に自立しません3.1.2 どうしても自立させたいなら3.2 折れる可能性を考慮する3. …

no image

Unityでメッシュをさわるノウハウ

ヒマなので覚書。ウラを取っていない経験則なので話半分で読んでください。 あと、3Dの基本概念とUnity固有の話の区別が付いていないのでごめんなさい。 もくじ1 Meshクラスの基本2 ポリゴンの読み …