概要
新人研修を見ていると、C言語のポインタは初学者にとってはやはり難しい概念なのでしょうね。
世の中には厳密さを優先した 細かくてややこしい 丁寧な説明をされている記事がありますが、多少大雑把でもまずはイメージを掴む事が後々の理解が捗るという自身の経験から、脳内イメージの棚卸しをしつつ説明したいと思います。
変数はメモ帳のページ、ポインタは付箋
メモリ(空間)
メモリ(空間)とは、何か情報を格納する記憶領域の事ですね。つまり現実世界での メモ帳 の事です。
→ページが複数枚ありそれを束ねたものであれば何でも良いですが、名前も「メモ」とついているのでちょうど良い例かなと思います。
変数
変数とは、メモ帳内の 1以上のページ を使ってデータを管理する作業領域の事です。1ページは1Byte(8bit)で、char
型なら1Byteで1ページ使用しますが、int
型なら4Byteで4ページ使用します。
(ホントにintが4Byteかは知りません・・・OSやコンパイラによって違うかも)
ポインタ
ポインタとは、変数の場所(アドレス)を格納する領域の事です。ここでは、厳密さよりわかりやすさを最優先にして 付箋 の様なものと考えてください。1
付箋は変数の最初のページに貼って使用するものと覚えてください。
また、付箋には以下の特徴があるものとします。
- 付箋にはメモ帳のページ番号(=アドレス)が書き込まれています。
- 付箋には色があり、貼ってあるページの変数の型ごとに違っています。
- 付箋自身にも付箋番号があります(実はアドレスと全く同じもの)。
変数とポインタの関係
変数の宣言
まずは、メモ帳の空いているページを探して「ここから〜ページ使用します」という具合にシステムにおしらせします。
// 変数を宣言
int a;
データ格納
宣言した変数に値を代入することは、ページに値を書き込むことです。
// 変数に値を代入
a = 10;
ポインタの宣言
ここでは付箋をポインタとするとしていますので新しい付箋を用意します。
この時、ポインタの型毎に色分けしているので、例えばint
型は青とします。
// ポインタの宣言
int *p;
アドレス格納
新しい付箋をメモ帳のa
のページに貼り付けます。
// ポインタにアドレスを代入
p = &a;
ここが重要なポイントですね。
変数の先頭に&
をつけると、メモ帳のa
のページの先頭ページ番号を取得することができます。(int型の場合は4ページ使用しているので、その先頭ページ番号を取得することができます。)
そして、付箋p
はページ番号を書き込めますので、代入を行うとそこにa
のページ番号が書き込まれました。
これで、ページに付箋を貼り付けたイメージとなります。
ポインタから実体へアクセス
付箋(ポインタ)からページ(変数)へのアクセスは、貼ってあるページを見れば一発ですよね。2
プログラムも同様に実体へのアクセスを行う方法があります。
// ポインタが指しているアドレスの内容を別変数に代入
int b = *p;
ここで重要になってくるのが付箋の色です。
もし付箋に色がなく全てが同じだった場合を考えて欲しいのですが、付箋はページ番号は書かれていますが、何ページ使っているかはわからないのです。
ということは、内容を何ページ分取り出して代入して良いかわからないため、システムにお知らせした領域外にアクセスしようとしてしまい危険な動作をしてしまう可能性があります。
色(型情報)によってこの付箋が参照している変数は何ページ使っているかわかる様になります。これで安全なアクセスを可能としているのですね。
ポインタのポインタ
ポインタを参照するポインタ
イメージ画像がネタみたいになっていますが、付箋に付箋を貼ることができます。
これをポインタのポインタ、言い換えると ダブルポインタ と言います。
// ダブルポインタにポインタのアドレスを代入
int **pp = &p;
以下の様に書くとわかりやすいかもしれません。
int* *pp;
pp = &p;
int*
型は付箋自身の型で、その型のポインタなので*pp
と一個*
が多くなっています。
青色付箋型に貼り付ける赤色付箋*pp
に青色付箋の付箋番号&p
を代入する感覚です。
変数、ポインタ、ダブルポインタの関係(まとめ)
最後にまとめです。
以下の様に変数を宣言します。
int **x;
int *y;
int z;
ポインタに変数のアドレスを代入
付箋(ポインタ)y
にページ(変数)z
のページ番号(アドレス)&z
を書き込みます。
y = &z;
変数にポインタが指す変数の内容を代入
ページ(変数)z
に付箋(ポインタ)y
が貼り付けられているページの内容*y
を書き込みます。
※ここが混乱しやすい箇所ですが、付箋の内容(ページ番号)ではなくそのページ番号に書いてある内容を代入しています。
z = *y;
ダブルポインタにポインタのアドレスを代入
付箋(ポインタ)y
の付箋番号(アドレス)&y
を別の付箋(ダブルポインタ)x
に書き込む事ができます。
※別の付箋と言ってもちゃんと「int型ページの付箋」の型である必要があります。
x = &y;
ポインタにダブルポインタが指すアドレスを代入
付箋の付箋(ダブルポインタ)x
が貼り付けられている付箋(ポインタ)y
に書き込まれている内容を代入しています。
イメージ画像でいうと、赤色付箋(ダブルポインタ)は青色付箋(ポインタ)に貼っているので、青色付箋に書かれている「ページ番号」を取り出しています。
y = *x;
変数にダブルポインタが指すアドレスが指すアドレス変数の内容を代入
とてつもなく紛らわしいですが、付箋の付箋→付箋→ページを一気に遡る事ができます。
有効な付箋があるというのは、その付箋はページや別の付箋に貼り付いている状態を意味しています。
貼り付いているのであれば一気に遡っても問題ないということですね。
z = **x;
ただし、その逆はできません。
なぜなら&z
でページ(変数)のページ番号(アドレス)を取得できたとしても、そのページ番号が書き込まれている付箋を見つけることはできないためです。
イメージ画像でいうと、青色付箋がないと赤色付箋に辿りつけないという事です。
// NG
x = &&z;
おわり
言葉やサンプルコードで説明されてよくわからなかった人も、図やイメージにしてみると結構簡単だと思いませんか?
まずはここまで理解できれば当面のプログラミングで混乱することは無いかなと思います。
ダブルポインタは何の役に立つのかはこの記事では説明できていないので、(関数の引数で使用すると呼び出し元に参照を渡す事ができるとかは)他の記事等を参照してください・・・
慣れてきたら、付箋も実はメモ帳のページであることや、配列ポインタ、構造体ポインタとステップアップしていくと良いと思います。
(配列や構造体は結局のところページが連続して並んでいるだけなのですんなり理解できます)