ポインタはアドレス扱う変数だと前回までで説明しました。今回はそれを配列で扱うお話です。配列とは変数が複数集まった形態です。ポインタも変数ですから配列で扱うことが出来ます。これで何が出来るのでしょうか。
--
いきなりここに飛んで来ちゃった人は、よろしければ下記からご覧ください。


  • ポインタのポインタ
int 型の配列があったとします。要素数は 10 としましょうか。

int num[] = { 0,1,2,3,4,5,6,7,8,9 };

この時、配列名の num はそのままアドレスでしたね。そのため…

int* ptr = num;

このように、ポインタ変数 ptr に配列のアドレスを直接代入することが出来ました。では、この int 配列が 5 個あり、それをそれぞれポインタ変数に代入したらどうでしょうか?

int bottom[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int under[] = { 10,11,12,13,14,15,16,17,18,19 }; int middle[] = { 20,21,22,23,24,25,26,27,28,29 }; int upper[] = { 30,31,32,33,34,35,36,37,38,39 }; int top[] = { 40,41,42,43,44,45,46,47,48,49 }; int* ptr0 = bottom; int* ptr1 = under; int* ptr2 = middle; int* ptr3 = upper; int* ptr4 = top;

とりあえず個別のポインタ変数に、それぞれの配列の先頭アドレスを代入しています。この ptr0 から ptr4 の 5個のポインタ変数はすべて共通の型である int* ですので、まとめて配列にすることが出来ます。

int* range[] = { bottom, under, middle, upper, top };

ポインタ配列の出来上がりです。配列から値を取り出す際は添字を使います。例えば、upper のアドレスを取り出すのであれば

int* ptr = range[3];

このような記述になります。

さて、ではこの場合のポインタ配列の名前は何を表しているでしょうか。配列名はアドレスです。ポインタ配列の名前は、同様にその配列が格納されているアドレスとなります。このアドレスを格納している状態がポインタのポインタです。だから、ポインタ配列のアドレスを格納するのはポインタのポインタという型を使います。int* のポインタですから ** と並べて記述となり…

int** pptr = range;

このような記述となります。超個人的には変数名の先頭の p はポインタを意味するように命名していたりします。ポインタのポインタなので、p を2つ重ねて pp から始めていたりします。これをさらに配列にすると、ポインタのポインタのポインタにもできます。こうやって際限なくポインタを増やし続けることが出来るのも、ポインタが難しいと言われる由縁なのかもしれません。必要になったら使えばよいだけで、ポインタのポインタを無理して使うもの事でもないです。私自身、過去にポインタのポインタのポインタまで使った事があるかなあ程度です。

さてさて、ポインタのポインタ変数から中身を取り出すとどうなるでしょうか?言い換えると *pptr は何でしょうか?ポインタ変数に * をつけると中身を取り出します。pptr には range という配列名を格納していますので、*pptr とすると range のアドレスが取れます。では、**pptr とするとどうでしょうか?中身の中身です。pptr には range のアドレスが入っていて range には bottom やら under やらの配列アドレスが格納されています。先頭に入っているのは bottom ですから、まず *ptr の結果として、bottom のアドレスが取れます。その bottom のアドレスの中を参照するので、bottom[0] と同じ値 0 が取れるのです。
ポインタのポインタ
この例だと [0] の値を取得してますが、[2] の値を取得するにはどうするのでしようか。これは前回、ポインタのアドレス計算で説明した応用で、アドレスに +2 してから中身を取り出せばよいのです。

int num = *(*pptr + 2);

*ptr は bottom のアドレスです。そのアドレスに +2 した位置の中身を取り出しますので、bottom[2] と同じ結果となります。
ポインタのオフセット計算
では、bottom ではなく upper の要素番号 1 の中身を取り出すにはどうするのでしょうか?

int num = *(*(pptr + 3) + 1);

ポインタオフセットのポインタオフセット
このようにポインタ配列さえあれば、オフセット値の変更だけで参照する場所を変えられるのが、このポインタのポインタを利用するメリットとなります。
※ これはあかん、可愛すぎる❤❤❤

  • 二次元配列とポインタ
ポインタは [n] というオフセットの記述が使えます。先の記述で内側のオフセット計算は

int num = *(pptr[3] + 1);

と置き換えることが出来ます。さらに外側の添字も [n] の書き方に出来ますので

int num = pptr[3][1];

…ちょっと待ってください。この書き方、どこかで我々は既に見ていますよね?そうなんです、二次元配列そのものです。これ、一見、全く同じに見えます(実際書き方は同じです)が、実はこれは全く違う使い方なのです。二次元配列は、ポインタの集まりではなく、数値の集まりであるためです。

少し実験します。二次元配列が本当にポインタのポインタなら

int array[3][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9,10,11,12 }, }; int** pptr = (int**)array;
こんな書き方が許されてしまいますよね?キャストしちゃってる関係もあり、また、アドレスはサイズが同じこともあって、このプログラムはコンパイルは通ってしまいます。では、だからと

printf("nun = %d\n", pptr[2][1]);

こんな使い方をすると
例外で落ちる
例外が発生して落ちてしまいます。二次元配列は単純な数値の集まりなので、メモリ上には、全ての操作が順番に格納されています。この array 配列の場合は、

array: db 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

array から始まるアドレスに数値が順番に格納されているだけです。そこにポインタのポインタ計算を行ってしまったのですから、array から格納されている数値情報をアドレスとみなして、そのデータが存在しないメモリ空間からデータを取得しようとして例外エラーが発生したのです。

この場合でポインタを使用するには array を単純なアドレスに見立てて

int* ptr = (int*)array; printf("nun = %d\n", *(ptr+ 2 * 4 + 1));

このように自分でオフセット計算を行って取得する必要があります。

printf("nun = %d\n", ptr[2 * 4 + 1]);

こちらの書き方のほうがわかりやすいかな?
どうしても、ポインタで二次元配列を使いたいという珍しい(?)要望の場合は、4個の要素を持つポインタであると明示する必要があるため、

int (*pptr)[4] = array;

こういうややこしい書き方になってしまいます…ので、こんなのは覚えなくても良いです😓
※ だったら書くなw

二次元配列とポインタのポインタの違いが分かったところで、一つの疑問が浮かびます。だったら二次元配列で良いのに、どういう時にポインタのポインタを使うのかという疑問です。結論から書くと、要素数が異なる配列を複数まとめて扱うことが出来るのが、ポインタのポインタを使う最たる理由となります。

例えば、要素数が異なる3つの配列をまとめて扱いたいとします。二次元配列だと、

char digits[][8] = { { 3,1,4 }, { 3,1,4,1,5 }, { 3,1,4,1,5,9,2,6 }, };

このような書き方になります。これだと digits[0] の 3,1,4 のデータの後ろに無駄な空間が出来てしまいます。これがポインタのポインタだと、

char digit3[] = { 3,1,4 }; char digit5[] = { 3,1,4,1,5 }; char digit8[] = { 3,1,4,1,5,9,2,6 }; char* digits[] = { digit3, digit5, digit8 };

このような記述とすることで、無駄にメモリを消費することがなくなります。この時、

printf("%d\n", digits[0][5]);

このように値の範囲外を参照してしまった場合は、二次元配列であれば空間が確保されているので問題ありません(0が返却されるという問題はあります)が、ポインタ参照の場合だと存在しないメモリ空間から値を取り出すことになりますので、何が起きても不思議ではなくなってしまいます。
無効な値の読み取り
※ 今の VS2022 はこういう指摘もするんですね。凄いなあ…

このように場合によって使い分けが重要となります。いろいろ試してみて理解を深めてみてください。ではではー

※ デバッグやゲーム配信などの強力なサポートハードです。説明は要らないかな。この商品の選択は安心のアイオーだからですね。ビデオ出力のヤツは持ってますがとても使いやすかったです。