BETA

ポインタとは「型+アドレス」であるという表現が私を助けてくれた

投稿日:2020-03-13
最終更新:2020-03-17

C言語において壁とされるポインタが、なぜ難しいのかはなかなか本人では説明できません。
みえない部分が多いからです。コレがわからない!!というのではなく、何がわかってないのかわからない!!
ポインタが暗中模索となるもっともありうるパターンではないでしょうか?なぜそうなるのでしょう。

前回の記事でC言語の一歩目はprintf()でいいのかといった記事を書きました。学習順序としては悪くないと思います。でも、ポインタには詰まります。先日はその理由として「メモリアドレスを突然、考える必要に迫られるから」としました。

しかし、アドレスはそれほど難しい概念なのでしょうか?
よくある例であれば、実際のおウチの住所でだって概念としてはつかめる訳です。問題は「だから何なの?」という点にあります。実態を説明していないが故の「モヤモヤ感」だけが残るのです。

先に結論めいたことを言ってしまうと、ポインタで把握していないのは

  • 型とアドレスの関係
  • 書き方そのもの

これだけだと言ってもよいのではないでしょうか?
そして、いわゆる #include <stdio.h> と printf() からはじまる学習の場合は、ポインタを学ぼうとする時にこの2つ(細かく見れば3つか4つ)がいっぺんに出てくることになってしまいます。だから「ポインタは壁」だと言われてしまうのです。

書き方は、知ればいいだけです(そして慣れていきましょう)。
ですのでそれほど重要な問題ではないと考えます。知ってさえいればいいからです。
ここで、先に「型とアドレスの関係」を実態から把握していくことだけに集中すれば、以後ポインタの使い方に慣れていくときの土台として活かせると考えます。また、書き方を知ろうとするときに「なぜそう書く必要があるのか」に理由付けができます。正しい認識を順序だてて深めていけるはずなのです。

ポインタは「型+アドレス」です。本来はもう少し正確な表現が必要なのですが、私はこれでも十分だと考えます。
わかりにくかったポインタを把握するために、これから少し遠回りをしましょう。
ですがこの道は着実な王道だと思います。

論理回路の初歩とメモリダンプの概念から触ってみよう

いきなり何を言ってるのかと思われるかもしれませんが、少し我慢して読んでみてください。ポインタが少し楽になるかもしれない流れを説明してみたいと思います。私が理解の大きな助けとなった「ポインタ=型+アドレス」という表現へ、最終的につながっていきます。

0と1の信号を扱う、いわゆるAND回路やOR回路といった論理回路とCPUは密接につながった知識です。CPUの中は大雑把に言えばそうした回路が集まったもの、つまり集積回路と言えます。
もうひとつ、CPUにはレジスタという高速かつ小さなメモリがあります。これらにはやはり単純な0か1かのデータが高速で入れ替わって存在しています。

そうです、論理回路からCPU、CPUからメモリは数珠つなぎの知識です。
レジスタだけではなくメインメモリにも話は直接つながっていきます。
パソコンを起動したあと、黒い画面が出てOSの準備をしている時、メインメモリの一番はじっこ(はじめ)辺りから限られた小さな範囲に、PCをうまく起動する為の小さなプログラムがロードされます。この「小さな範囲」は厳密に決まっているので、この範囲を超えたサイズのプログラムをロードできません。このプログラムがロードされ、そのプログラムを使うことで、巨大なメモリ空間を把握するための動作を実現できるようになる……そういう流れでOSが起動していきます。

すこし脱線しましたので、メモリに戻します。この時点ではほとんどアセンブリ(CPUを直接動かす低級な命令)といったプログラムしかロードできません。つまり、巨大なメモリ空間を扱う為の技術は「C言語」や「Python」といった、普段目にするようなプログラミング言語は今の所関係ないということが言えるのです。

なぜこれが大事なのでしょうか?答えは「メモリダンプ」にあります。
メモリダンプとは巨大な(2020年時点であればメインメモリ8GBでも少ないと言われますね)空間がどのようなデータになっているのかを把握するために見る表のようなものです。有り体に言えば、住所録です。何番地には何というデータが入っているのかを16進数でひたすら並べたものがメモリダンプと呼ばれます。

つまり、メモリダンプには「メモリの位置」と「その値」が書かれており、対応して閲覧できるようになっているという訳です。このメモリダンプは、メモリの中身を表示したものに過ぎませんから、やはり「C言語」や「Python」といったプログラミング言語専用のものという訳ではないことはお判りでしょうか。

以下は模式図です。

アドレス 1番地 2番地 3番地 4番地 5番地 6番地 7番地 8番地
0000 0001 00 12 A4 FF AC 82 90 10
0000 0002 11 71 1D 28 51 F6 FF 01
0000 0003 4F CB 65 91 DE 31 70 A7
0000 0004 04 16 9C 7C 3B E6 00 22

上の図で「A4」とか「28」などになっている部分が実際のメモリに格納されている値です。
一番上の行、または一番左の列はアドレスを指しています(模式図なので正確ではありません)。

値はテキトーに入れています。これは16進数で表されています。
ですので2進数に直すと「A4」は → 「1010 0100」となります。
0か1かの単位が8個あつまったデータをバイトと言います。
つまり、上の表は1バイトごとに区切ってあるというだけです。

「0000 0002丁目」の「3番地」にお住まいのデータは?
といえば「1D」という1バイトが入っている、というように見ていきます。
これがメモリダンプの概要です。それほど難しくはないと思います。

プログラミング言語は思ったより密接にメモリダンプを操作している

C言語で

char c;  

とすると、文字型の c という変数を宣言したことになります。
経験ある方は、この c という変数に文字を1つだけ格納できることをご存知でしょう。
ではこの時、メモリダンプでは何が起こっているのでしょうか?
上の表を見比べながら読んでみてください。

この宣言が行われる時、メモリのどこかに char c の為の場所が確保されます。
この時、ひとまず「0000 0003丁目の2番地」をその場所としたとしましょう。仮定の話ですが、そのまま続けて構いません。この場所を見ると「CB」が入っていますね。
C言語において char 型は「1バイトのサイズを扱える型」です。(アルファベットの)1文字だけ扱えるという意味ですね。

メモリダンプの「CB」は1バイトです。ですからC言語は char c が宣言された時には「0000 0003丁目の2番地だけ」を確保すればいいことになります。

それでは

int a;  

ではどうなるでしょうか?
C言語の int 型は「4バイトのサイズの整数を扱える型」です。
1バイトだけでは、0か1の数字を8個までしか扱えないので、多くても200程度の数字までしか表現できません。
int 型は30,000とか、999,999とか、大きい値を使える必要があります。
4バイトのサイズがあれば足りそうですね。

ですので、C言語は int a が宣言された時には「0000 0002丁目の4~7番地の4バイト分」を確保しようとします。
ここが大事な所で、メモリダンプ上では4つ並んだ場所を確保しなければなりません。

「0000 0002丁目の4~7番地」には「28 51 F6 FF」が入っています。
通常、数字を読んだり文章を読むときには繋がってなければ意味を把握できませんよね?
それと同じようにして、意味のあるデータを扱う為に、大きなデータ型を確保するには「繋がった状態」にしなければ具合が悪くなってしまうのです。

c = 'Z';  

という命令を続けた時、char 型の変数 c には「Z」という文字を代入したことになります。
「文字のZ」は数値に直すと「16進数の"5A"」です。(アスキーコードというのを調べてみてください)

先程、 char c は「0000 0003丁目」の「2番地」に住んでいると言っていました。
ですのでC言語はこれを実行すると、以下のようにメモリダンプの値を書き換えます。

アドレス 1番地 2番地 3番地 4番地 5番地 6番地 7番地 8番地
0000 0001 00 12 A4 FF AC 82 90 10
0000 0002 11 71 1D 28 51 F6 FF 01
0000 0003 4F 5A 65 91 DE 31 70 A7
0000 0004 04 16 9C 7C 3B E6 00 22

おわかりでしょうか?
「0000 0003丁目の2番地」が "5A" に変化しました。このようなことをC言語はやっている訳です。

a = 0;  

これが実行された場合はどうなるでしょうか?
int 型は4バイトの場所を確保しています。
その値を 0 にするということは、全ての値を 0 に書き換えることを意味します。
a の住所は「0000 0002丁目の4~7番地」でしたね。

アドレス 1番地 2番地 3番地 4番地 5番地 6番地 7番地 8番地
0000 0001 00 12 A4 FF AC 82 90 10
0000 0002 11 71 1D 00 00 00 00 01
0000 0003 4F 5A 65 91 DE 31 70 A7
0000 0004 04 16 9C 7C 3B E6 00 22

上記のように実際に変化する訳です。
char c の時とはやや異なる操作となりましたね。4バイト全てが操作されてしまいました。
これが「型」の役割です。

c = 256000;  

これはエラーになるのがわかると思います。
char 型である変数 c には、こんなに大きな数字を入れられませんね。
どちらにしても、コンパイルエラーでアプリとしては作成できないことでしょう。

なぜこのような判断が可能なのでしょうか?
それこそが、型の役割であり、アドレスとの関係性を把握すのに必要な考え方なのです。

操作する場所を明確にするためにアドレスがある

こんどはこの記事を読んでいるあなたが考えてみてください。

int b = 0;  

を実行した時、確保されるメモリのアドレスが「0000 0004丁目の5番地から」だったとしたら。
あなたは以下のメモリダンプのどの部分を変更するでしょうか?

アドレス 1番地 2番地 3番地 4番地 5番地 6番地 7番地 8番地
0000 0001 00 12 A4 FF AC 82 90 10
0000 0002 11 71 1D 00 00 00 00 01
0000 0003 4F 5A 65 91 DE 31 70 A7
0000 0004 04 16 9C 7C 3B E6 00 22

どこからはじめるのかはすぐ察しが付くのではないかと思います。
あなたは恐らく「0000 0004丁目の5番地」にある「3B」を見ているのではないでしょうか?
そして、その右側に続く4マス、つまり「8番地まで」をすべて 00 にしようとお考えではありませんか?

アドレス 1番地 2番地 3番地 4番地 5番地 6番地 7番地 8番地
0000 0001 00 12 A4 FF AC 82 90 10
0000 0002 11 71 1D 00 00 00 00 01
0000 0003 4F 5A 65 91 DE 31 70 A7
0000 0004 04 16 9C 7C 00 00 00 00

結果としては、それで正しいです。
それでは、なぜあなたはその4つを選択したのでしょうか?
それは「int 型だから」という理由があったから、のはずです。

つまり、型とはそのような役割をします。
指定されたアドレスから、どのくらいの大きさのメモリが確保されているのか、が……型を見るだけで分かるのです。これってスゴイことですよね?つまり、ここまで読んでくださったあなたは、少なくともC言語の char 型と int 型についてはそれなりに判断できるようになった訳です。

ここで次の問題を考えてみます。
同じ int b = 0; を実行しようとしたとき、先程私は「アドレス」を指定しました。
「0000 0004丁目の5番地から」という情報がそれです。
もしこの指示がなかったら、恐らく困ってしまったことでしょう。
自由に自分で場所を決めていいものだろうか?と悩むかもしれませんね。
スタート地点が欲しい!? と思いませんでしたか?

いまこそアドレスの出番という訳です。先ほど「0000 0004丁目の5番地から」と指定したように、アドレスさえわかれば、メモリの確保や操作を始められるようになります。なぜなら、型と値は分かっているからです。

この時、必要な範囲のアドレス全てを知る必要があるでしょうか?
「5番地から8番地まで」と指示する必要があるでしょうか?
スタート地点だけでも十分ではありませんか?

それこそが「ポインタ」

「int という型」と「アドレスのスタート地点」が分かれば、必要な操作ができることが分かりました。
記事の冒頭の話題にようやく戻ります。

ポインタ = 型 + アドレス(の開始地点)

int の4マス全てを表現するのは、面積的ですよね?ちょっとあやふやになります。
これが double 型だとかになるともっと広くなってしまいます。

ですから、スタート地点という「大事なポイント」その一点が欲しいですよね?
「ポイント」してほしいですよね?

そうです!! 「ポインタ」です。
大事なスタート地点を指すものこそ「ポインタ」なんです。
そして、そのスタート地点から「どれ程のサイズまで許されるのか」という情報が型なのです。

だからポインタには型とアドレスが必要なんです。こう考えると、かなり見えてきませんか?

「ポインタはアドレスのこと」という説明では不十分ということになります。
先程エラーの例として出した

c = 256000;  

がなぜダメなのかは、もうお分かりだと思います。
メモリダンプをもう一度見てみましょう。

アドレス 1番地 2番地 3番地 4番地 5番地 6番地 7番地 8番地
0000 0001 00 12 A4 FF AC 82 90 10
0000 0002 11 71 1D 00 00 00 00 01
0000 0003 4F 5A 65 91 DE 31 70 A7
0000 0004 04 16 9C 7C 00 00 00 00

「0000 0003丁目の2番地」から、1バイトまでしか確保していないのが char 型です。
256000 という数値は、1バイトでは足りません。隣の3番地も使う必要が出てきます。
いや、もしかしたら更にとなりの4番地も……でもそんなことはどうでもいいんです。

char 型だと既に宣言しているのだから、1バイトを超えたら問答無用でエラーなんです。

ポインタの使い方は別の話

ここまでの説明では、ポインタを実際に活用する場面までは出しませんでした。
具体的なソースコードすらありません。
それでも、「ポインタ=型+アドレス」という認識に立ったことで、書き方を知らなくてもそれらの関係性を想像できるようになったはずです。もしこれからポインタを使った範囲へ勉強を進めようというのであれば、何も知らない状態に比べるとかなりやりやすくなっているのではないかと思います。

そしてもう一歩だけ踏み込んだことを紹介して、おしまいにしたいと思います。
プログラムで記述された存在は、ほぼすべて「メモリに格納」されていきます。
関数を作ったり、構造体を準備したり。そのすべてが「どこか」にないと困りますよね?
プログラムの中で、関数って何度も「呼び出す」と思います。
一体どこから呼び出しているのでしょうか?

その答えもまた、メモリダンプにあります。
あなたが作った変数も、関数も、構造体も、メモリのなかのとある範囲にしっかりと格納されており、そしてそれぞれが「スタート地点のアドレス」をきちんと持っているのです。

更に、メモリダンプでの確保の話は「C言語」だろうと「Python」だろうと限られたものではないとも言及しました。どんなプログラミング言語であれ、メモリを扱う以上は似たような何らかの手法は取っています。厳密にこれと答えられるものではありませんが、このあたりを切り分けて考えられると良いのではないかなと思います。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

最近ちょっとプログラミングをはじめてみたので怒られない範囲の場所に来ました。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう