2001/11/15 加筆修正
修正部分はこの部分と同色で示す
説明に誤りがあることが発覚
ポインタの内容であるアドレスを知るために「&p」と書く。
と読みとれるような記述をしていたため、大部分を修正。
楓花さんの指摘により、ソースの記述ミスが発覚
実際の使用例のソース内部において、「y*」と誤って記述していたものを
「*y」と修正する
御指摘ありがとうございました。
●
ポインタについて
今回はポインタについて説明します。
今までポインタについて聞いたことがあるかもしれません。それも「ポインタは難しい」「分からない」などという言葉まで一緒に聞いたかと思います。
しかし、ポインタが分からない・難しいと感じる理由は二つ考えられます。それは
@ ポインタが何か分からない。
A ポインタが何の役に立つのか分からない。
多くの場合、この二つの理由から「ポインタは分からないor難しい」という話になっていると思われます。
まず、@の「ポインタが何か分からない」ということですが、例えば「アダプティブサブディビジョン」という手法があります。この言葉からしてなにやら難しそうです。しかし、それは言葉を知らないからであって、実際に「適応分割」「再帰分割」という言葉に置き換えたり、実際の説明である
「直線で近似できる部分は直線で近似し、あまりにもズレが大きければ細かいステップで直線近似する手法である。」
というようなことを聞けば、「難しそう」というのが誤解だと分かるでしょう。
(※ここではアダプティブサブディビジョンの詳細な説明は省略します。
そのため、まだ違和感が残ったり難しそうという気持ちがあるかもしれません。
しかし、それは詳細な説明を聞けば解消されるでしょう。)
まぁ、つまるところ“自分がよく分からないものは、なんだか難しいと感じる”ということです。
次にAですが、これは@に通じるところがあります。例えば難しい話をダラダラと言われても、それが実際に何の役に立つのか分からなければ、勉強する気にもならない。そして分からなくなる。という感じでしょうか。学校の授業にありがちですね…。
以上のような理由からポインタは難しいと“錯覚”されています。あくまで錯覚であり上に挙げた2点をきちんとクリアすれば何ら問題はありません。
では、まず勉強する気になりましょう。なぜポインタが必要か?ということです。
● なぜポインタが必要か?
◎予備知識 その1 関数・引数・戻り値
全く知識がない状態で、このことを説明するのは、困難です。そのために予備知識の確認をしておきましょう。それは「関数」「引数」「戻り値」です。
@ 関数
まず、関数は次のように記述されます。
int func(int a, int b)
{
int r; //
戻り値用 変数
// 関数の動作内容
r = a+b;
return r; //
戻り値を返す
}
int
ans;
ans
= func(1, 3);
ここで、一行目の int func( int a, int b) において func は関数名であり、変数名と同様に自分で自由に決定できます。
そして、func の前の int は戻り値の型を指定しています。最後にfunc( の括弧の中にある int a, int b は引数です。
A 引数
関数が何かの値を元に動作する場合、その値を「引数」として関数の中に持ち込みます。ここでは、int a, int b ということで、二つの整数を取り込み、関数ないでの処理(足し算)に使います。
B 戻り値
関数は、何らかの処理をした後、結果を返す必要があります。その結果を返すのが「戻り値」です。その構文は
return 変数名;
となります。
ここで注意すべきは、戻り値の型は、(int funcのように)“関数名の前”で指定すること。そして“戻り値は一つの値しか返せない”ということです。
戻り値は、 return a, b, r; というように複数の値を返すことができないのです。
これは、ポインタを利用する大きな動機に繋がりますので、少し覚えておいてください。
そして、最後の func(1,3); のようにしてa=1, b=3 として関数内の処理 r=a+b
が行われ、return r; によって、4という数値をfunc が返すことになります。
もうそろそろ頭が混乱してきたかもしれませんが、上の3つは“整数を使うならint と書く”というのと全く同じキマリです。これは馴れるしかないと思います。ただ最後の“戻り値では一つの値しか返せない”というのは覚えておいてください。
◎
予備知識 その2 通用範囲(スコープ)
さて、戻り値では値が一つしか返せない。ということが分かりました。ではこれならどうでしょうか?
int
func(int a, int b)
{ // 通用範囲の始め
int r,s; // この中で宣言された
r=a+b;
s=a*b;
} // 通用範囲の終わり
int
main(void)
{ // 通用範囲の始め
func(1, 3);
printf(“%d”, r); // 実は通用範囲外
printf(“%d”, s); // 実は通用範囲外
return 0;
} // 通用範囲の終わり
なにも戻り値を使わなくても、そのまま「r」や「s」を使ってやればいいじゃん。という考え方です。きちんとfuncの中でrもsも定義してあるから、問題なさそうです。
しかし、これではコンパイル時にエラーが出てしまいます。「'r' : 定義されていない識別子です。」というように怒られます。なぜか?それは“変数には活動範囲があるから”です。この活動範囲のことを“通用範囲(スコープ)”と呼びます。
このスコープを外れると、その変数は存在しないものとして扱われるのです。
スコープは“{”と“}”によって設定されます。“{”以降に定義された変数は次の“}”以降は通用しません。ここでは、funcの始めの { の中でrとsが宣言されたため、funcの最後の } より後ろでは通用しません。つまりmainの中では通用しないのです。
これを“通用範囲という壁”が、変数の周りを取り囲んでしまうので、どこにその変数がいるのか分からない。だから操作できない。というようなイメージを持てば、「ポインタの必要性」を認識できます。
これが、先ほどの“戻り値は一つしか値を返せない”という決まり事と重なって非常に大きな問題となります。
スコープが邪魔をして値を外に出せないとすれば、値を返すには戻り値を使うしかありません。しかし、戻り値では一つの値しか返せないのです。次のようなときが最も困ります。
void swap(int x, int y) // 戻り値型voidは何も返さないという意味
{
int
tmp;
tmp
= x; // 一旦確保
x
= y; // yをxにして、
y = tmp; // とっておいたxをyとする
}
一回見ただけでは、何をしてるか分からないかもしれませんが、これはその関数名のとおり、数値を入れ替える関数です。確かに関数の内部でxとyが入れ替わっています。
しかし、さっきの通用範囲の問題があるため、これを実際に使って
int main(void)
{
int a=5, b=8;
swap(a, b);
printf(“a=%d, b=%d”, a, b);
}
としても、表示されるのは入れ替わる前の a=5, b=8 という結果です。またreturn をつかっても片方の値しか返せないのであれば、二つの値を入れ替えた結果を返すことができないのは言うまでもありません。
そこで登場するのがポインタなのです。
●
悪名高きポインタ見参
というわけで、「二つの値を返したい」という欲求を満足させるためポインタに登場いただきましょう。ポインタは世間では“悪名高い”と言われていますが、それはきちんとした説明がなされていないからであり、それ以上でも以下でもありません。ポインタは悪人ではないのです。
ポインタを説明するにはメモリと絡めて説明すると分かりやすくなります。
●
ポインタとメモリ
前回は配列とメモリの関係に少し触れました。あれは、今回の「ポインタとメモリの関係」に対する布石だったのです。
まず、図を見てください。
|
メモリ番地 502 503 504 505 |
変数名 |
値 |
ポインタ ← |
|
value1 |
201 |
||
|
value3 |
203 |
||
|
value4 |
204 |
||
|
value2 |
202 |
これが大まかなポインタとメモリーの概念になります。ポインタは図ようにメモリー上の位置を指し示す矢印のようなものです。
つまりその中には、位置に関する情報が入っています。
●
ポインタによる問題解決
さて、最初の問題を思い出してください。それは
「二つの値を入れ替える関数を作りたい。しかし、戻り値は一つしか返せないし、通用範囲に邪魔されるので入れ替えることはできない。」
ということでした。
これをポインタによって解決しましょう。
戻り値は一つしか返せません。これはキマリです。そこで通用範囲の網目をくぐり抜けます。通用範囲を超えることができないのは、それがどこにあるか分からないからです。しかし、ポインタはメモリ上の位置を示すので通用範囲なんて関係ないのです。
まず、前回説明したように変数はどんなものでもメモリ上に確保されます。そして、そこにはメモリ番地(アドレス)があります。これをポインタは知っています。
そこで“ポインタを使って直接 値を書き換えてやる。”のです。
上の図で「矢印の位置の値を1000に変えろ」というのであれば通用範囲も何も関係ないですよね。
●
ポインタのキマリ
さて、これで問題解決の方法は見つかりました。
次はそれをプログラムではどう書くか です。プログラムには“整数をintとして宣言する”だとか、“printf文の書式指定で整数は%dだ。”とか色々キマリがあります。ここで説明するポインタのキマリもなんら変わりありません。
まず、ポインタは“変数の位置を指す”のでした。ということは、自然と出来ることが2つ出てきます。
@ 変数のアドレスを知る
A 変数の値を操作する
@
変数のアドレスを知る
繰り返しますが、ポインタというのは“変数の位置を指す”モノです。それは“アドレスを知っている”ということにほかなりません。つまり
「ポインタ」=「アドレス」
となるように、ポインタとはアドレスの入れ物(=位置を指すモノ)であり、中身はアドレスそのものなのです。
ですから@のポインタからアドレスを知ることが出来るのは当たり前です。ポインタpの中身であるアドレスを知る為の操作をC言語では
address = p;
と書きます。
そのままですね。整数同士の代入と変わりません。
最初に変数のアドレスをセットするときは
p
= & value;
と書きます。
ここで、「&」(アンド・アンパーサンド)はアドレスを知る為の演算子であることから「アドレス演算子」と呼ばれます。
A
変数の値を操作する
アドレスが分かっていれば、それを使ってその値を操作できるのは言うまでもありません。
ポインタを使って、その指定先を書き換える操作を
*p
= 1000;
と書きます。
ここで、*pと書くことで「アドレス」から「実体」に変化するわけです。
もちろん*にも名前が付いています。アドレス演算子に対応して「実体演算子」なんていう分かりやすい名前なら良いんですが、誰が考えたんだか「間接参照演算子」なーんてたいそうな名前がついています。(いや、実際は理にかなっているんでしょうけど。)まぁ、私たちのレベルでは「実体演算子」で十分なんですが、これまたキマリなので「間接参照演算子」と呼びましょう。
さて、少し話はズレるのですが理解を深めるために少し寄り道を。今まで
「printfとscanfってどっちが & いるんだっけなぁ?」
って悩んだことありませんか?その問題が上の&の説明から分かるはずです。さて、どっちに&がいるんでしょうか?そうです。「 」ですね。
上で正しい答えを言えた人には説明の必要はないかもしれませんが、一応説明します。まず、ポインタを使う一つの理由に「コピーでは困る」「直接値を書き換える必要がある」というものがありました。ここでコピーでは困るのはどちらでしょうか?そうです。「scanf」はコピーでは意味がありませんよね。実際の値を書き換える必要があります。逆に「printf」はコピーだろうが何だろうが、表示するだけですから問題はありませんよね。
よって、
「scanfは & が必要」
という事になるのでした。
寄り道でしたが、良い勉強になったと思います。
●
ポインタの演算
ポインタも変数です。他の変数となんら変わりません。ただ、“中身がアドレスである”というだけなのです。
まず、C言語における定義の書き方から見てみましょう。
int*
p; // 整数へのポインタpを宣言
となります。こうすることで、int型(整数型)へのポインタであることが決定されます。また、この*の位置は
int
*p; // 整数へのポインタpを宣言
というように、「p」に付けて書かれることが多いようです。私は個人的には「int型へのポインタ」=「int*」p という感じがするので、上の方を好んで使っています。
なぜ下の方が主流なのか、はっきりした理由は分かりませんが、ポインタを2つ以上続けて宣言する場合、上の表記では不具合が生じることは事実です。
上の書き方を使えば、
int*
p, q, r; // 整数へのポインタp,q,rを宣言
という書き方が成立しそうな気がします。しかし、これは間違いです。実際には
整数型へのポインタ p
整数型 q,
r
というように処理されてしまいます。
ですから、全てをポインタとしたいなら下の表記のように
int *p, *q, *r // 整数型へのポインタp,q,rを宣言
とするしかありません。
この2つは好みの問題ですが、あとあとの事を考えると、下の表記の方が間違いが少ないかもしれません。
私は癖みたいなもので、上の表記を使いますが、みなさんは必要なら随時読み換えていってください。
さて、前置きが長くなりましたが、実際の演算方法を見てみましょう。
「ポインタも変数だ」ということなら“ポインタを使った演算”もあるだろうと考えられます。ただ、中身がアドレスですから、“アドレス1とアドレス2の和”なんてのは考えても意味がないように思えます。
では、具体的に見てみましょう。
@ ポインタへの代入
A ポインタからの取り出し
B ポインタ同士の代入
C ポインタへの加算
D ポインタへの減算
大きく分けるとこのようになると思います。
それぞれを具体的に見てみましょう。
@
ポインタへの代入
まずは、ポインタへの代入です。これは具体的には“ポインタ変数へアドレスを代入する”ということを指します。
「ポインタ変数」=「変数のアドレス」
となるであろうことは想像できますね。そして変数のアドレスはアドレス演算子&で求めるのでした。以上からポインタへの代入は
int
value; // 整数型valueの宣言
int*
p; // 整数型へのポインタpを宣言
p
= &value; // pにvalue のアドレスを代入
となります。
こうしてポインタを初期化するのです。
A ポインタからの取り出し
“ポインタからの取り出し”とは“ポインタから実体に変換する”と言えるかもしれません。つまり「間接参照演算子」を使う。ってことですね。早速見てみます。
int value1, value2; // 整数型の変数
int* p; // 整数型へのポインタ
value1 = 100; // それぞれを初期化
value2 = 0;
p = &value1; // ポインタにvalue1のアドレスをセット
value2 = *p; // 間接参照演算子を使って実体を取り出して
// その値をvalue2に代入
としてやればポインタから値を取り出して、他の変数に代入できます。
この場合であれば、最後の段階で「value2 = *p」=「value2 = value1」となるから「value2 = 100」となり、value2が100になるのです。イメージがわきにくいでしょうか…。
これはある程度キマリであり 馴れてくれば何ともないのですが、最初はなにか気持ち悪いかもしれませんね。この気持ち悪さが「難しさ」と誤解されることも良くあるのですが…。
B ポインタ同士の代入
ポインタといっても単なる変数ですから、ポインタ同士の代入は至って簡単です。
int *p, *q; // ポインタを宣言
int value1; // 整数を宣言
q = &value; // アドレス確保
p = q; // ポインタからポインタへ代入
「p = q」という部分だけを見れば、それがポインタかどうか分かりませんよね。普通の整数同士の代入と全く同じです。全く同じ書き方で代入が行えます。
C
ポインタへの加算
最初にポインタ同士の加算つまり「アドレス」+「アドレス」には意味がない。といいました。これは間違いない事実です。ところが、「ポインタ」+「整数」であれば少し話が違ってきます。
もう一度図を見てみましょう。
|
メモリ番地 502 503 504 505 |
変数名 |
値 |
ポインタ ← |
|
value1 |
201 |
||
|
value3 |
203 |
||
|
value4 |
204 |
||
|
value2 |
202 |
ポインタpのアドレスは「&p」としてやると「502」と求まります。
ここで“p+1”というものを考えてみます。
「p+1」とすると、実は、こうなるのです。
|
メモリ番地 502 503 504 505 |
変数名 |
値 |
ポインタ ← |
|
value1 |
201 |
||
|
value3 |
203 |
||