こんにちは、ナナです。
「ポインタ変数」はメモリの番地を管理するための変数です。番地を管理するが故に、普通の数値とは異なる演算ルールが適用されます。
特殊である理由も含めて解説していきます。
ポインタの全貌を学びたい方は『C言語 ポインタを使いこなせ【身に付けるための9の極意】』の記事から順に読むことをお勧めします。
本記事では次の疑問点を解消する内容となっています。
では、ポインタへの演算の特殊性を学んでいきましょう。
ポインタ変数に対する四則演算の特殊性
師匠!「ポインタ変数」って番地を覚えてるんですよね。ちょっと変わった変数ですね。変わり者のポインタ変数のことをもっと知って、仲良くなりたいのですっ。
そうだね、ポインタ変数は番地を記憶するという特殊性から、演算に対する結果が特殊なものになるんだよ。そのあたりを学んでみようね。
ポインタ変数は番地を管理するため、四則演算は特殊なルールが適用されることになります。
ポインタ変数に対する加減算の特殊ルール
ポインタ変数が管理する番地に加減算(+・-)をした場合、通常の加減算とは異なる動作をします。
次のように、ポインタ変数に対するインクリメントが、どんな結果となるのかを明らかにします。
short num[2] = {0x0123, 0x4567};
short * pnum = num;
// pnumの番地に1を加算
pnum++;
// pnumの番地はどうなる?
注意してください。
ここで問うているのは、ポインタの参照先のメモリに対する加減算ではなく、ポインタ変数の持つ番地に対する加減算ということです。
こんなのは当然「101番地」に決まっていると考えたあなた・・・、実は違うんです。
答えは「102番地」です。不思議なことに+1したのに番地が2増えるのです。
次のポインタ変数に対する加算は、次の結果になります。皆さん規則性がわかりますか?
各ポインタ変数に対して加算した結果の番地がいずれも異なっています。
よく見ると加算された数は、ポインタの参照先データ型のサイズと一致していますね。
なぜ、このようなルールがあるのかを説明しましょう。
配列に対してポインタの参照が設定されているとします。もしも、short型配列に対してポインタの参照位置が1番地しかずれないと、次のように不正な範囲のメモリ参照となってしまいます。
では、番地が2つずれたとしたらどうなるでしょう。short型の配列num[1]の情報に正しくアクセスができますね。
このようにポインタへの加減算は、不正なメモリアクセスが発生しないよう、参照先のデータ型サイズ分だけ増減するようになっているのです。
ポインタ変数の番地に対する加減算は±1の数値が変化するのではなく、参照先データ型 ±1個分のメモリサイズだけ変化します。
ポインタに対する加減算の意味とは
そもそも、ポインタ変数に対する加減算とは、どのような意味があるのでしょう。
ポインタの番地とは、的となる参照先メモリの場所を示しています。その位置をずらすということは「的への照準を横にずらすこと」と捉えることができます。
ポインタ変数に対する加減算は、ポインタが参照する的への照準を左右にずらす行為なんです。これはポインタで配列を扱う時に非常に大事な演算ルールなんです。
ポインタ変数に対する乗除算の特殊ルール
続いて、ポインタに対して次のように乗除算(×・÷)を行いました。この結果はいったいどうなるでしょうか。
short num[2] = { 0x0123, 0x4567 };
short * pnum = num;
// pnumの番地を2倍にしたらどうなる?
pnum *= 2;
皆さんの環境でも動かしてみると明確にわかるでしょう。実はビルドエラーが発生します。
error C2296: '*=': 無効です。左オペランドには型 'short *' が指定されています。
ポインタ変数に対する乗除算は、C言語では認められていません。
pnumの番地が「100番地」だったとして、×2倍すると「200番地」になりますね。
しかし、得られた200番地にいったいなんの意味があるのでしょう・・・。
番地という数値を2倍にする意味など、存在しないのです。そのため、ポインタ変数に対する乗除算は禁止されています。
このように番地を管理するポインタへの演算は、「番地」を扱うがゆえに特殊な演算結果を生み出します。しかし、理由としては明確なものがあるのです。
ポインタ型の変数のメモリサイズ演算の特殊ルール
師匠!ふと思ったんです。メモリの番地って、どこからどこまであるんですか?ポインタって何番地から何番地まで管理できるんですか?
それはね、すごく大事なことだね。変数とは割り当てられたメモリサイズによって、管理できる数の上限が決まるんだよ。つまり、ポインタ変数のメモリサイズによって管理できる番地の幅が決まるってことだね。
ポインタ変数のメモリサイズについて学びましょう。
ポインタ変数のメモリサイズは何バイト?
まずはおさらいです。次のように変数を定義しました。
char num1;
short num2;
long num3;
変数のデータ型のサイズはchar型は1バイト、short型は2バイト、long型は4バイトでした。このサイズに従い、変数ラベルの長さが変わるのですね。
続いてポインタ変数に目を向けましょう。
ポインタ変数には番地という数値を入れるのでした。つまり、ポインタ変数のメモリサイズの大きさによって、格納できるメモリ番地の範囲が決まることになります。
では、質問です。
char * pnum;
ポインタ変数pnumのメモリサイズは何バイトなのでしょうか?
実は、このポインタ変数のサイズは環境依存です。
とある環境では4バイトかもしれませんし、別の環境では2バイトや8バイトかもしれません。このように、ポインタ変数のメモリサイズは環境により変化します。
では、実際に皆さんの環境でポインタ変数のサイズを見てみましょう。データ型のメモリサイズを求める方法といえば「sizeof演算子」です。
sizeof演算子の詳細は『C言語 sizeof演算子【データサイズの算出と実践的な使い道】』の記事を読むとよいでしょう。
sizeof演算子を使ったポインタのメモリサイズの算出
次のプログラムを記述し、どんな数値が表示されるかを予想してから動かしてみてください。
#include <stdio.h>
int main(void)
{
char * pnum1;
short * pnum2;
long * pnum3;
// ①:ポインタ変数のラベル
printf("pnum1 :%d\n", sizeof(pnum1));
printf("pnum2 :%d\n", sizeof(pnum2));
printf("pnum3 :%d\n", sizeof(pnum3));
printf("------------\n");
// ②:ポインタ変数の型
printf("char * :%d\n", sizeof(char *));
printf("short * :%d\n", sizeof(short *));
printf("long * :%d\n", sizeof(long *));
printf("------------\n");
// ③:ポインタ変数の参照先
printf("*pnum1 :%d\n", sizeof(*pnum1));
printf("*pnum2 :%d\n", sizeof(*pnum2));
printf("*pnum3 :%d\n", sizeof(*pnum3));
return 0;
}
ポインタに対するsizeof演算子を①~③のパターンで使用しました。私の環境で動かした結果は次のものになりました。
pnum1 :4
pnum2 :4
pnum3 :4
------------
char * :4
short * :4
long * :4
------------
*pnum1 :1
*pnum2 :2
*pnum3 :4
①と②は同じ結果であり、すべて4バイトとなっています。
注目してもらいたいのはポインタの参照先の型がchar、short、longであってもポインタ自身のサイズは4バイトであることです。
それに対し③の結果はサイズが変化しています。「*ポインタ変数名」は参照先のデータを示すためサイズが1、2、4バイトと変化したということです。
ポインタに対するsizeof演算子は、ちょっとした書き方の違いによりメモリサイズが変化します。使用する場合は注意が必要です。