
この後、ここではC言語で特に難しい挫折ポイントである「ポインタ」についてお話していきたいと思います。 ポインタを理解する前提となる知識としてC言語からどのようにメモリを使っているかの基礎知識をお話できればと思います。
C言語で変数などを使うとき、変数に入れる数値はメモリの中に作られるとお話したかと思いますが、もう少しこのあたりも具体的に見ていきたいと思います。
「ポインタとはメモリのアドレスが入っている変数」とよく言われます。 また、アドレスとはメモリに付けられた番地、とよく言われています。メモリは保存できる領域があり、保存できる場所ごとに順番に通し番号が振られています。 ポインタを使うことでメモリのアドレス(番地)を指定して数値を保存(代入)したり、一つ隣のアドレスを参照するといった操作を行うことのできる、C言語の機能です。 もう少し、ここでいうところのメモリとはどんなものを想定しているでしょうか。
プログラムが動くとき、OS(Operating System)がプロセスを作成し、このプロセス単位でメモリの領域が割り当てられます。 基本的に、プログラムからは、OSが割り当てたメモリ領域しか見えません。 プログラムはこのOSが割り当てたメモリ上の番地(アドレス)に対して読み書きをすることになります。
なぜこのような手間をかけているでしょうか。 物理的なメモリを直接プログラムで使うことを考えてみましょう。 例えばアドレス 0 ~ 1000 番地までの領域が書き込み可能なメモリを搭載したマシンを使うとき、計算結果を保存するために500番地に書き込み、などアドレスを指定したとします。 このような、実際の主記憶装置の中の記憶する場所と紐づくようなアドレスは物理アドレスと呼ばれます。 この方法だと、例えば2つのプログラムが動いたとき、同じ物理アドレスをデータ保存用に使ってしまい、上書きしてしまうことがあるなどの欠点があります。
そこで仮想アドレスという概念の登場です。 プログラムが動くとき、OSのほうで、あらかじめそれぞれのプログラムが使えるメモリ領域(この領域を仮想アドレス空間と呼びます)を割り当ててあげればよいのです。 具体的にプログラマからしたらどのように楽になるでしょうか。
例えばプログラムAが起動したとき、OSが割り当ててくれた仮想アドレス空間のアドレス100番にプレイヤの体力である数値「200」を書き込むという命令をしたとします。このとき、プログラマは意識する必要はありませんが、裏でOSはこの命令を受け取ったとき、プログラムA用が動いているプロセスのアドレス100番→物理アドレス上の10000番、など、他のプロセスに割り当てている物理アドレスとぶつからないように調整して、仮想アドレスと物理アドレスの変換をしてくれます。
つまり、どのようなマシンでプログラムを動かしたときでも、OSが割り当ててくれたメモリ領域の仮想アドレスだけを意識すればよく、物理アドレスを気にする必要がなくなります。
C言語で変数を宣言して使ってきたとき、変数はメモリに作成されるとお話したかと思います。
変数 i に整数 100 を代入するイメージ
この変数名 i という数値を代入できる箱は、OSがプログラムを動かすときに作成したプロセスに対して、割り当てられた仮想メモリ上に作成されます。(実際にはOSが裏で物理アドレスに変換して、そこに数値が記憶されています。)
i という名前は人間が読みやすくしているラベル(貼り紙)のようなもので、変数 i というラベルはメモリ上に確保された数値 1000 が書かれている場所(仮想アドレス)を示しています。
C言語上で変数を宣言して使うとき、変数に対してそれぞれ仮想アドレスが指定され、数値が格納されています。実際に下のようなプログラムを書いてみると、変数のアドレスが数値として表示できます。
#include <stdio.h>
int main(void) {
char a = 100;
long b = 200;
int c = 300;
printf("変数 a のアドレスは16進法で %p ,10進法で%ld\n", &a, (long)&a);
printf("変数 b のアドレスは16進法で %p ,10進法で%ld\n", &b, (long)&b);
printf("変数 c のアドレスは16進法で %p ,10進法で%ld\n", &c, (long)&c);
}
実行結果は下のようになりました。
変数 a のアドレスは16進法で 00FAFE43 ,10進法で16449091
変数 b のアドレスは16進法で 00FAFE34 ,10進法で16449076
変数 c のアドレスは16進法で 00FAFE28 ,10進法で16449064
変数名の頭に「&」記号を付けることで、その変数のアドレスの番地を見ることができます。 もし「&」記号がないと、変数のアドレスに書いてある内容を見ることができます。この「&」記号をアドレス演算子と呼びます。
また、printf の中で “%p” という書式指定子がありますが、これでポインタのアドレスを 16 進法で表示することができます。 今回は無理やり 10 進法でもアドレスの位置を表示しています。
例えば今回の例では、char 型変数 a は仮想アドレス空間の中のアドレス 16449091 番目からデータが入っていることを示しています。 また、次の long 型変数 b はアドレス 16449076 番に割り当てられていることがわかります。
見てわかる通り、非常に割り当てられたメモリのアドレスは近く、a → b → c と進にあたり、アドレスは小さくなっていることがわかるかと思います。 このメモリのアドレスは実行する環境によって変わって来ますし、同じPC内でも実行するたびに結果は変わる可能性があります。 しかし、a → b → c でアドレスが小さくなっていくことについては同様かと思います。
このようなアドレスの割り当て方についてもC言語でルールが決まっています。
今までのお話ししてきたプログラム例でも、変数を宣言、代入して演算に使用してきましたが、すべて main 関数やオリジナル関数の中でのみ使ってきたと思います。
このように、ある関数の中で宣言して使用する変数はローカル変数と呼ばれ、関数の中でのみ、その変数が使えるという風に、コードの中で使える範囲が定められていました。
他にもグローバル変数と、スタティック変数という変数があります。 グローバル変数はプログラム全体のどの関数の中でも使うことができます。 スタティック変数はソースファイルの中や定義された関数の中だけで利用することができます。
実際に変数の例と使える範囲(スコープ)を見てみましょう。
{imgcation}グローバル変数、スタティック変数、ローカル変数のスコープ
具体的な違いとして、グローバル変数は関数の外側で宣言し、ローカル変数は main 関数などの関数の内側で宣言します。 また、スタティック変数は、「static」というキーワードを頭に付けて宣言することで、スタティック変数として扱うことができ、関数の外でも内でも宣言することができます。 関数の外で宣言した場合、そのソースコードファイルの中で使うことができるようになります。 また、関数の内で宣言した場合、その関数の中だけで使うことができるようになります。 スタティック変数を関数の中で使う時、関数から returnで抜け出したあとも変数の中身は残り続け、次にその関数が呼び出されたときは前回のスタティック変数の中身が残っています。
ローカル変数、スタティック変数、グローバル変数といった、変数の種類によって、 メモリ上のどの位置に変数の中身を記録するかが変わってきます。 詳細は後ほどお話していきます!
C言語でポインタを使うときの機能の一つとして、ポインタ変数があります。 通常の変数と同じように宣言し、初期化することができます。 このポインタ変数は下のように宣言することができます。
int *pointer;
記号「*」はポインタ演算子と呼ばれ、これがポインタ変数で宣言されているということを示しています。
これで、pointer という名前の変数ができました。また頭でint 型を指定していますが、 これで変数 pointer にint型の整数が入るというわけではありません。
int 型が格納されている(であろう)アドレスを代入することができる変数、という意味になります。 当然ながら、int型以外のポインタ変数ついても宣言することができます。
たとえば、char型が格納されているメモリアドレスを代入することができる変数なら、
char *charpointer;
のように書くことが出来ます。
いずれにせよメモリのアドレスを代入することに違いはありません。
int *pointer;
と定義したとき、変数pointer へアクセスすると、
変数 pointer に代入されているメモリアドレスの位置から、int 型のサイズ分データを読み取る、という解釈ができます。
以前の記事でお話しましたが、char型や int 型などはそれぞれ、型によって使うメモリのサイズが異なります。
たとえば、char型なら 1 バイト(=8ビット)、int型なら 4 バイト(=32ビット)などです。
このようにポインタ変数を宣言するときは、
メモリが指す先の型 *変数名;
という書き方をします。これで、変数名にはメモリアドレス(仮想メモリのアドレス)が代入できるようになります。
補足:ポインタ変数の型を使った計算
1例ですが、ポインタを使って配列の一つ隣の要素(アドレス)を参照したいときなど、ポインタを使った操作をすることがあります。
この ”一つ隣” というのは、int型であれば 4 バイト隣ですし、char型であれば 1 バイト隣かもしれません。
このようにアクセスする先の型のメモリサイズで、読込メモリアドレスを変化させるときに型情報が使われたりします。
ポインタの概念を理解しておくと、他の言語でも大量のメモリを扱うプログラムを効率的にかけたり、設計したりがしやすくなります。
ただ、ポインタはツールなどを使ったコード解析(静的コード解析)がしづらい特徴があり、バグが気づかずに潜んでしまったり、 ちょっとした記述ミスでアクセスする先のアドレスを誤って指定してしまい、想定がの動作を引き起こすなど、バグの温床にも繋がります。
最近のスマートフォンやPC、サーバなどは高速に動作しますし、コンパイラが頑張って高速に動くプログラムに仕上げてくれますので、 「ポインタを多用して少し高速に動く読みづらいソースコード」よりも、「プログラムの設計やアルゴリズムに注意を払って、人間が読みやすいソースコード」を書く方が良いと思います。
更新日 | 更新内容 |
---|---|
更新なし |
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: