ポインタはアドレスを管理します。配列名のようにアドレスが固定なら別にポインタを使う必要はありません。これはポインタ変数です。変わる数値だから変数です。変数だから計算が出来ます。今回はポインタを使用したアドレスの変化や計算について説明します。
--
いきなりここに飛んで来ちゃった人は、よろしければ下記からご覧ください。


  • ポインタが扱うアドレスの単位
アドレスはコンピュータ内のメモリの位置を示しています。コンピュータで扱う数値の最小単位はバイトです。そのため、アドレスも通常はバイト単位で存在しています。アドレスに +1 した場合は単純に +1 となります。ですが、ポインタを +1 した場合はアドレスは単純に +1 とはなりません。少し試してみましょう。

int num; int* ptr = # printf("ptr = %X\n", (unsigned)ptr); ptr++; printf("ptr = %X\n", (unsigned)ptr);
%X は16進数で 32bit を表示する書式です。ポインタは私の環境では 64bit ですので、下位 32bit だけをとりだすために 32bit の unsigned でキャストしています。この実行結果は…
※ unsigned だけの記述は暗黙的に unsigned int と扱われます。
intポインタアドレスを+1した結果
アドレスが +4 進んでいるのが分かりますでしょうか。次は short 型で試してみます。

short num; short* ptr = # printf("ptr = %X\n", (unsigned)ptr); ptr++; printf("ptr = %X\n", (unsigned)ptr);
この short のポインタを +1 した結果…
shortポインタアドレスを+1し
今度はアドレスが +2 進んでいます。この違いは何かというと、型のサイズの違いです。先の int の場合、サイズは 4 です。これは sizeof(int) とすれば数値(サイズ)がわかります。ポインタが扱うアドレスの単位は、変数のサイズになるのです。

この理由は、配列で考えるとわかります。配列はメモリに指定した数だけ連続して配置されます。例えば int num[5]; と宣言した場合の各要素のアドレスを調べてみます。


int num[5]; for (int i = 0; i < _countof(num); ++i) { printf("ptr%d = %X\n", i, (unsigned)&num[i]); }
この実行結果は
配列各要素のポインタアドレ
このように int のサイズ毎に綺麗に並んで配置されています。int のサイズが 4 バイトだから当然です。さて、もしポインタが純粋にアドレスを +1 するだけならどうなるでしょうか。例えば、上記の状態で、F5EFF779 の位置はどうなるでしょう?

~ シンキングタイム ~

F5EFF779 は、配列要素 0 と、配列要素 1 の境界を跨いだ意味のないアドレスです。そのアドレスに無理やり数値を書き込むと、この場合は配列要素2つが同時に意図しない数値に置き換わります(環境によってはアプリが落ちます)。このような理由から、ポインタは計算の単位が型のサイズとなっているわけです。


  • 配列とポインタアドレス
配列の基礎が理解できている前提で話をしますので、もし理解が怪しい場合はこちらで復習しておいてください。



配列の添字を変更すると、その添字に応じた配列に対して読み書きが出来ますが、同様な事をポインタを使っても出来ます。例えば、

int num[] = { 1, 2, 4, 8, 16 }; for (int i = 0; i < _countof(num); ++i) { printf("num[%d] = %d\n", i, num[i]); }
この配列の添字を使った実行結果と、

int num[] = { 1, 2, 4, 8, 16 }; int* ptr = num; for (int i = 0; i < _countof(num); ++i, ++ptr) { printf("num[%d] = %d\n", ptr - num, *ptr); }
このポインタを使った実行結果は全く同じになります。ポインタの記述で少し変わった部分があるのに気が付きましたでしょうか。ptr - num という計算です。ptr ポインタ変数はループ毎に +1 されています。num は配列の先頭アドレスなので、現在のポインタから配列の先頭アドレスを引くと、添字番号が手に入ります。これも、ポインタアドレスの最小単位が型のサイズになっているからこそ出来るワザですね。

※ 安い!実は探すと1万円を切ってる販売店もあるけど個人的にはAmazon販売をオススメします、1,000円ちょっとの安心感?…かなあ。
*ptr でポインタが示すアドレスに格納されている値を取り出します。ここで少し違った書き方を紹介します。

int num[] = { 1, 2, 4, 8, 16 }; int* ptr = num; for (int i = 0; i < _countof(num); ++i) { printf("num[%d] = %d\n", i, ptr[i]); }
num[i] の配列名をポインタ変数に変えただけです。これでも同じ結果となります。配列の名前はアドレスです。これは固定アドレスであり定数です。ポインタもアドレスを扱っています。こちらは変数に格納されているアドレスですから変数です。定数と変数の違いではありますが、どちらもアドレスなので同じ記述ができるのです。逆も然りでこんな書き方もできます。
※普通はこんな風には書きません…

int num[] = { 1, 2, 4, 8, 16 }; for (int i = 0; i < _countof(num); ++i) { printf("num[%d] = %d\n", i, *(num + i)); }
このように配列名をポインタに見立てて、ポインタのように記述することだって出来ます。この自由さが C言語の素晴らしさでもあり難しさでもあったりします。

なお、ポインタの場合は配列の最大数は分からないので、_countof が使えることは稀です。というか、_countof が使えるのならポインタ要らないかもです。そのため、ポインタだけでデータを取り出す場合に、その終端を示す方法としてよく使われるのが、データの最後に通常は使用されない値を入れる番兵法と呼ばれる手法が採用されます。この場合だと…

int num[] = { 1, 2, 4, 8, 16, -1 }; for (int* ptr = num; *ptr >= 0; ++ptr) { printf("num[%d] = %d\n", (unsigned)(ptr - num), *ptr); }
このような記述となります。データにマイナスの値が存在しない前提です。文字列の最後に \0 が入っているのも番兵と言えます。


  • 処理の順番
さて、先程からアドレスの +1 に ++ を多用していますが、このインクリメント演算子やデクリメント演算子を使用する場合に、特に処理の順番を意識する必要があったりします。例えば…

int num[] = { 1, 2, 4, 8, 16, -1 }; int* ptr = num; while (*ptr >= 0) { printf("num[%d] = %d\n", (unsigned)(ptr - num), *ptr++); }
これ、一見正しいと思える記述ですが、実は問題があります。これの問題がすぐ分かる人は以降読む必要はありません。これはポインタの問題というより printf の問題です。printf には複数の引数が指定できます。これを可変引数と言います。この可変引数は、私の知る限り右から順番に処理されていきます。今回の場合は *ptr++ が最初に処理されて、続いて (unsigned)(ptr - num) となります。結果、
添字が0からではなく1からと
このように、添字番号が 0 からではなく 1 からとして表示されています。期待した結果と異なりますよね?
※ ヒトはこれをバグと呼びます😁

では、右から処理されるのであればと次のように記述を変えたとします。

int num[] = { 1, 2, 4, 8, 16, -1 }; int* ptr = num; while (*ptr >= 0) { printf("num[%d] = %d\n", (unsigned)(ptr++ - num), *ptr); }
最初に *ptr が処理されて、次にアドレス計算が処理されるから、これで大丈夫かと実行すると…
番兵が見えちゃった…
今度は *ptr の結果が狂ってしまいました。
※ ヒトはこれをエンバグと呼ぶかもです😱

これは評価と処理が分かれているために起きているのだと私は考えます。もしかするとこれは環境依存で、VS2022 以外では異なる結果となる可能性もあります。このような記述は結果を保証できないと言えるわけで、例え正しい結果が得られる書き方が出来たとしても、あまりオススメ出来る記述ではありません。この場合は素直に…

int num[] = { 1, 2, 4, 8, 16, -1 }; int* ptr = num; while (*ptr >= 0) { printf("num[%d] = %d\n", (unsigned)(ptr - num), *ptr); ++ptr; }
このように表示と処理を2行に分けて記述するのがよろしいと思います。なお、printf に引数が一つだけなら printf("%d", *ptr++); のような書き方も OK ではあります。

今回は少しややこしい部分が多々あったかと思います。まとめます。
  1. ポインタは型の単位でアドレスが変化する
  2. ポインタから配列名を引くと添字番号となる
  3. ポインタのインクリメント/デクリメントは処理順に特に注意する
まだまだポインタの解説は続きますー

※ ノート用32GBならこちら。実はAmazon販売でなければ4,000円ほども安い販売店はあります。その販売店を信用できればそちらのほうがお得かも。私のおすすめはあくまでもAmazon販売品としてます。
    2023/03/12 19:00 更新
    配列の初期化子に = が抜けているので追加しました。あと、%X によるキャストしてのアドレス表示は %P とすべきとの指摘がありましたが、それはあえて %X としたままにしています。あしからずご了承ください。