##はじめに
この記事は、Cに余り馴染みのない方向けの、ポインタに関する解説を目的としています。
今までC言語でのデザインパターン・オブジェクト指向について書いてきましたが、せっかくの設計思想に関する記事なのに、私がC言語のメリットとしている考えが共通認識のように記事を書いていた気がしてきました。
これではもったいない気がするので、改めてC言語のポインタについての解説を通して、ハードルを下げつつC言語の強みについて整理出来ればなと思いました。
ついでにポインタについてピンとこないC言語初心者のお役に立てないかなって思ったり。
この記事でC言語や今までの記事へのハードルが下がれば幸いです。
・C言語に詳しい方々へ
もし記事に目を通していただき、「こういった情報があるとイメージしやすい」みたいなアドバイスを下さる酔狂な方がいらっしゃれば、コメントいただけると幸いです。
##ポインタの前に メモリ領域とアドレス
###プログラムのデータはすべてメモリ領域上に
プログラムとプラグラムが扱うデータは基本メモリ空間内の領域が利用されます。
私の中では、メモリ空間は地主のCPU様が管理している領地みたいなイメージです。(イメージなので多めに見てください。)
ユーザーがプログラムを実行すると、CPUがプログラム実行に必要なメモリを割り当てます。
この時、プログラムが使う色々な型のデータも、関数も、すべてメモリ上に展開されます。
###メモリ領域にはアドレスがある
CPUさんはとても気遣い上手なので、関数には関数用の領域を与えてあげる等、プログラムがメモリ領域を意識しなくても、意図しないデータ競合が発生しないように制御してくれます。
このプログラムが意識していないメモリ領域ですが、一体どのように管理しているのでしょうか?
ここで出てくるのがアドレスです。
メモリ領域にはアドレスと呼ばれる住所情報があるため、どのメモリ領域がどこにあるのかを把握できます。
つまり、アドレス情報さえ知ることが出来れば、メモリ領域自体を教えられてなくても間接的にメモリ領域の中のデータを参照・編集することが出来るのではないでしょうか?
これを間接参照/編集といった用語で呼んでいます。
(与えられた変数に直接アクセスするのは直接参照/編集といった呼ばれ方をします。)
このアドレスを知ることが出来れば、自由にメモリ領域を扱うことが出来そう!
##ポインタ メモリ領域に何とかしてアクセスしたい!
###アドレスを知ってメモリ領域を参照/編集したい! ポインタの出番
普通に動作しているだけだとプログラムは直接メモリ領域を意識して参照/編集できませんが、アドレスを知ってメモリ領域を参照/編集できるようになれば、
関数間でのデータのやり取りが出来るようになり、複雑なプログラムも作成することが出来ます。
というわけで、何とかアドレスを知る方法はないんでしょうか?
ここで登場するのがポインタ/ポインタ型です。
ポインタはメモリ領域のアドレス、ポインタ型はアドレスを格納するための型となります。
つまりポインタを使えばメモリ領域上にある色々なプログラムのデータを参照・編集可能になるわけです。
当然ポインタ型もメモリ領域上にあるデータ。ポインタ型のアドレスも存在し、そのアドレスはポインタによる参照が可能です。
この辺りが初心者のつまずきやすいところなのかなとか思ったりします。
###自由なC言語ポインタ
ここまでで、ポインタを使ってメモリ領域上のデータを扱えることがわかりました。
では、今度はポインタの種類について考えています。
ポインタを定義する際は、int *
, char *
等、使用する方のポインタ型として変数を宣言します。この型の違いは何でしょう(使い方はありふれてるので省略)
まず、インクリメントした(ポインタをずらすといった呼び方をします)時のずれ幅が違います。それぞれ元の型サイズ分ずれます。int *
なら4 or 8(system依存), char *
なら1です。
こういった先頭からのずらし方をした場合は、アドレスの位置はそれぞれint *
, char *
の想定した位置と異なってくるので注意が必要です。
(意図的にポインタを先頭からずらした場合は、その位置は型に依存するため注意が必要)
後は、他の型と同じように、別の型を代入する時は、キャストをしないと怒られます。
それくらいしかぱっと思いつかない。
(char *
型のアドレスから+1等して)位置をずらしたアドレス以外はキャストして別のポインタ型を代入しても直ちに影響はないですし。
基本ポインタサイズはその環境内で共通なので。
(古いシステムでは異なることもあったそうですが、今日調べて初めて見かけたレベルです。)
- ポインタを使えばメモリ領域上のデータを参照・編集可能
- 変なずらし方をしなければ、ポインタのサイズは基本同じ
と考えると、なんでもできる感が強まってきましたね。
###最初にみんなハマること 空のメモリ領域アクセスはプログラムが落ちる
なんでもできそうなポインタですが、慣れてない方に注意を1点。
メモリ領域が用意されていないアドレスにアクセスすると、プログラムが落ちます。
特に以下以外の領域は関数実行後に解放されてしまうので、そういったアドレスを参照していたらメモリ領域のデータがなくなって落ちるなんてことは一度はあるかもしれませんね。
-自分でmalloc
等で確保している
-グローバルやstatic
な変数である
###用語の説明:ポインタの考え方整理
ここまでポインタの考え方について解説を行ってきました。考え方解説のまとめとして、よく使われる用語を通してポインタについて整理しましょう。
用語 | 英語 | 説明 |
---|---|---|
メモリ領域(名詞) | Memory area | プログラム内のデータが格納される場所 |
アドレス(名詞) | Address | メモリ領域の住所情報を表すデータ |
ポインタ(名詞) | pointer | 参照元 |
ポインティ(名詞) | pointee | 参照先、メモリ領域 |
ポイント(動詞) | point | 参照する |
ポインタ変数 | - | 参照先メモリ領域のアドレス値を保持する変数 |
直接参照 | Direct reference | 定義した変数を利用してメモリ領域を参照/編集すること |
関節参照 | Indirect reference | ポインタを利用してメモリ領域を参照/編集すること |
※英訳間違ってたらすいません。 |
##ポインタの色々な使い方
###関数内での値変更
初心者向けも踏まえているので基本的な使い方も紹介
#include <stdio.h>
void update_integer_in_func(int value) {
value++;
}
void update_integer(int * value) {
(*value)++;
}
int main() {
int localvalue=0;
update_integer_in_func(localvalue);
printf("after call update_integer_in_func, localvalue=%d\n", localvalue);
update_integer(&localvalue);
printf("after call update_integer, localvalue=%d\n", localvalue);
return 0;
}
main(){}のように()と{}まとめられたものが関数。mainが一番最初に呼ばれる関数です。
関数()内の値は引数といって、関数内で利用可能な値となります。update_integer_in_func(localvalue);のように関数名(引数)と書くと、その引数の値が関数で利用されます。
上のupdate_integer_in_funcだとmainで実行した際のlocalvalueが0なので、update_integer_in_func側の引数valueに0が代入された状態で、処理が走るという感じです。
この結果表示はこうなります。
after call update_integer_in_func, localvalue=0
after call update_integer, localvalue=1
最初のupdate_integer_in_func
関数の引数valueは、CPUさんの気遣いによりmain
関数のlocalvalue
と違うメモリ領域にデータが確保されるので、いくら関数内でvalue
を変えてもlocalvalue
は変わりません。
2度目の最初のupdate_integer
関数は、int * value
のメモリ領域は新しく確保されたポインタとなりますが、value
内の値は引数localvalue
のアドレス(&localvalue
で表現します)になるので、update_integerからlocalvalueのメモリ領域を参照・編集することが出来ます。
その為関数内でlocalvalueの中身を編集することが出来るというわけです。
###void *
普通の型のポインタを定義すると、他のポインタを代入する際はキャストがいります。
unsigned int uintdata;
int * data = (int *)&uintdata;
//こっちはキャストがいらない。
unsigned int * data = &uintdata;
みたいに。サイズが同じだとしても違和感ありますよね。
そこでvoid *
。
これは利用する際はキャストしないと使えないけど、代入する際はどんなポインタでも利用可能です。どんなものでも扱えるのでAPIでの定義に便利です。
例えばメモリ領域を確保するための関数malloc
の戻り値はvoid *
だったり、関数のreturn
値としてよく使われるのを見ます。
普通ならどんな型なのかわからないと使えないものが、とりあえず"アドレス情報"としてvoid *
と書けばOK!
後は使う人の間で(または1箇所でラップして)認識があっていれば、もうそれだけでAPIとして成立しちゃいます!
だから以前のvoid *についての記事のようにオブジェクト指向でのAPIも出来るわけですね。
###関数ポインタ
これもC言語の強みの一つだと思います。
メモリ領域説明の際の絵に、しれっと「関数」(色々出来そうなロボットの絵)を入れていました。
そう、関数もメモリ領域にあるんです。ということは、関数もポインタで参照できるということになります。
関数をポインタを利用した変数のように扱うことが出来るので、まるでクラスメソッドのように扱うことも出来たり、利用者に意識させずに振る舞いを差し替えることだってできます。
Cのインターフェイスクラス表現について書いたような、
関数ポインタを利用したインターフェイスクラスの表現が出来るのもこのためです。
興味がある方は上の記事を参照ください。
###コールバック
これが一番よく見る関数ポインタの利用法なので紹介します。
例えばAPIの処理に時間がかかる場合、すぐに処理が終わらないので要求だけ出して後で結果を見たいというケース、よくあります。
こんな時に使うのがコールバックという仕組みです。
APIの引数に関数ポインタを設定して実行。APIはすぐに終了。
裏で処理を進め処理が終わるともらった関数ポインタの関数を実行します。
こうすれば時間のかかる処理に止められることなく、かつ終わったタイミングをすぐに知ることが出来るというわけです。
便利なので非同期のAPIではよくコールバックが利用されます。
##最後に
今回は、C言語でのオブジェクト指向の考え方を行うために、C言語の特徴であるポインタについて解説してみました。
自由度が高い分、やっぱり便利ですね。
あといらすとやの素材を使ったお絵かき楽しい
記事の更新に辺り、コメントしてくれた@shiracamusさん、@tenmyoさん、この場を借りてお礼申し上げます。
##参考:
イラスト素材: いらすとや