C言語では、変数の値はコンピュータのメモリに格納されます。
その格納されているアドレスが格納されている変数のことをC言語の文法的に「ポインタ」と呼んでいます。
「ポインタ」のメリット
メモリを節約して、高速動作する関数を書くことができる点になります。
我々が使うPCは高性能化しているのであまり必要ないかもしれませんが、家電等のPC以外の機器に搭載されているCPUやメモリは、PCに比べると貧弱なものです。場合によっては、数KB程度のメモリ領域でプログラムを組まないといけなくなったりもします。
例
ポインタを使わない場合
引数に「100バイトの項目を持つ構造体」を渡すような関数があったとしましょう。
構造体に値を格納した時点で、100バイトのメモリ領域を消費しています。
そこで、関数の引数にその100バイトの値を渡すと、値がコピーされるので、元の領域と合わせて200バイト消費してしまうことになります。
これを関数に対する「値渡し」と呼びます。
ポインタを使った場合
逆に、その構造体のアドレスだけを関数に渡すようにした場合は、100バイトではなく、数バイト程度のアドレスだけ渡すだけで済み、メモリの消費量を大幅に抑制することができるのです。
これを関数に対する「ポインタ渡し」と呼びます。
ポインタを学ばなければならない理由
上記メリットを差し置いても、下記理由があるためC言語ではポインタを押さえておく必要があります。
標準関数にポインタを使った構文が多いため
printfやrand等の標準関数は、ポインタを使うことが前提なので知っていないと十分に使えません。
配列を関数の引数のする場合は、ポインタとして渡すことが必須になるため
C言語では、配列を関数の引数として渡した場合は、ポインタを渡したことと同義になってしまいます。
例えば下記のような構文があった場合は、
1 2 3 |
void func(int a[2]){ … } |
実際は、下記のようにポインタ型の変数が渡ったとして読み替えられてしまいます。
1 2 3 |
void func(int* a){ … } |
先頭アドレスの取得
通常の変数の場合
1 |
&変数名 |
配列の変数の場合
下記のように記述した場合は
1 |
func(配列名); |
実際は、下記のように配列の先頭アドレスの値に読み替えられます。
1 |
func(&配列名[0]); |
アドレスを格納する変数の宣言
void*型
どんなデータ型でもお構いなしに指すことができるポインタ型になります。
ここで使っているvoidは関数の引数、戻り値に扱う文字列とは何の関係もないので注意です。
1 |
void* 変数名; |
この意味のあるデータではなく、メモリ上のアドレスを格納するためだけの型のことをポインタ型と呼びます。
ただ、実際はこの型はあまり実務で使われることはありません。
なぜなら、このアドレスから値を取り出す際は、先頭から何バイト取り出すということを明示的に指定しなければならないからです。
実際は、int*等の既存の型に「*」を付けた型が使われます。
int*型
1 |
int* 変数名; |
void*型とほぼ同じようなものですが、void*型と違って、int型(4バイト領域)の変数なので、先頭アドレスを指定するだけで、自動的に4バイト分のアドレスを指定して、値を取り出してくれるのです。
ポインタに格納されている値の取り出し
通常の変数の場合
1 |
*ポインタ変数名 |
配列の参照の場合
「str[0]」と「*str」及び、「str[整数]」と「*(str+整数)」は同じ意味になります。
構造体ポインタを参照する方法
通常の参照とは違い、構造体をポインタとして参照する場合は「かっこ()」で括る必要があります。
1 |
(*構造体の変数名).メンバ名; |
また、下記のように省略表記を使うことも可能です。
1 |
構造体変数名 -> メンバ名; |
これを「アロー演算子」と呼びます。
ポインタの整理
C言語のポインタは一口に「ポインタ」といっても下記の四つに分かれます。
混同しやすいので注意をする必要があります。
- ポインタ型
- ポインタ型の変数
- ポインタ型の値
- ポインタ型の値の指す先の値
百聞は一見にしかずです。下記のサンプルプログラムを例に説明いたします。
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() { //通常のint型の変数 int no_pointa = 5; //intへの「ポインタ型の変数」(int型変数のアドレスを代入) int *pointa = &no_pointa; //「ポインタ型の値」を表示(アドレス) printf("%p\n", (void*)&pointa); } |
ポインタ型
他の型から派生する形で生成されます。「intへのポインタ型」や、「doubleへのポインタ型」等。
上記サンプルでいえば、「int *pointa」の変数宣言の「int *」の部分のことを指しています。
ポインタ型の変数
ポインタ型で実際に定義した変数のことです。
上記サンプルでいえば、「int *pointa」の変数宣言の「pointa」(「*」はあくまで型に含みます。)の部分のことを指しています。
ポインタ型の値
実際にはメモリのアドレスのこと。変数の先頭にアドレス演算子(&)を付けることで取り出すことができる。
上記サンプルでいえば下記のソースコードで出力される結果になります。
1 2 |
//「ポインタ型の値」を表示(アドレス) printf("%p\n", (void*)&pointa); |
ポインタ型の値が指す先の値
「*ポインタ型の変数」と間接演算子(*)をポインタ型の変数の先頭に付ければ、アドレスの先の値まで見ることができます。
表記法の違い
C言語では、ポインタの表記方法としては下記の二つがあります。
1 2 |
int *pointa; int* pointa; |
結論としては、上記サンプルであればどちらも同じように動きます。
ただ、下記のようにした場合はどうなるでしょうか。
1 2 |
int* pointa1,pointa2; int *pointa1,pointa2; |
この場合は前者であれば、「pointa1」も「pointa2」もどちらもint型のポインタを指す変数として正常に定義することができます。
しかし、後者の場合は、pointa1はint型のポインタを指しますが、pointa2は通常のint型になってしまいます。
変数名に間接演算子(*)を寄せるか、型の方に寄せるかは諸説あるようですが、個人的には複数変数を定義した際に誤った挙動をしてしまう可能性があるため、型に間接演算子(*)を寄せる方法にした方がよいと思います。
ポインタ演算とは?
ポインタに整数を足したり引いたりして、ポインタ同士で計算をする機能です。
1 2 3 4 5 6 7 8 9 10 11 |
int main() { int a; int *b; b = &a; printf("%p\n", (void*)b); //ポインタにアドレスを加算する。 b++; printf("%p\n", (void*)b); } |
上記プログラムの実行結果としては私の環境では下記のように表示されました。
1 2 |
0055F80C 0055F810 |
4だけメモリ領域が増えていることがわかります。
これは指定された型のサイズ分だけメモリが進んでいるということなのです。(上記例の場合でいえば、intへのポインタなので4バイトメモリが進んでいます。)
ヌルポインタとは?
何も指していないことが保証されるポインタのことです。
ヌルポインタを指すマクロ定数としては「NULL」を使用します。
用途
ポインタ変数の初期値
ポインタ変数の初期値にNULLを入れておくことで、ヌルポインタを経由して値を参照するようなことがあれば、OSがプログラムを即停止してくれるので、バグにすぐに気づくことができます。
C言語だけでなく、Javaとかも参照型にはNULLで初期化したりしますが、その目的としてはこれと似たようなことになります。
関数の異常時の戻り値
有効などんなポインタと比較しても等しくならないことが保証されているので、ポインタを戻り値として返す関数の異常時の戻り値として活用できます。
リスト構造の末尾のデータ
リスト構造のようなデータの末尾のデータとしても活用できます。
この記事へのコメントはありません。