このページではポインタのポインタ(ダブルポインタ)について説明したいと思います。
ポインタについては下記の記事で解説していますので、まだ理解が不十分という方は是非読んでみてください。
【C言語】ポインタを初心者向けに分かりやすく解説
ポインタのポインタ…
ダブルポインタ…
ポインタを理解するのにも苦労したのに理解できるか不安…
うーん、そう感じるかもしれないけど、
ポインタがしっかり理解できていればポインタのポインタは実は簡単だよ
ポインタのポインタと聞いて身構える必要はありません!
ポインタについてしっかり理解していれば、おそらくすんなり理解できると思います。
さらに、”ポインタのポインタ” を理解することでポインタの理解も深まります!
では早速、ポインタのポインタについて解説していきたいと思います。
Contents
ポインタ(おさらい)
前述の通り、”ポインタのポインタ” を理解するためには、まずはポインタについて理解しておいた方が良いです。
ということで、”ポインタのポインタ” について解説する前に、おさらいとして、ポインタとはどのようなものであるかについて簡単に説明しておきたいと思います。
ポインタの理解はバッチリという方は、この章はスキップして次のポインタのポインタまで進んでいただければと思います。
ポインタとは
ポインタは他の変数を指す(他の変数のアドレスを格納する)ことができる変数ですね!
このようにポインタが変数を指すことができるのは、変数が「メモリ上に配置」され、その変数に対して「アドレスが割り振られる」からです。
実際には、ポインタは変数でなくてもメモリ上に配置されているものなら指すことができます(関数など)。
このページでは「どの変数がどの変数を指しているか」が分かりやすくなるように上図のようなメモリ空間の図を使用して説明しています
サイズやアドレス値も正確に書くと図や説明が難解になるため、このページではこれらがてきとうなものになっているので注意してください
このページでは「どの変数がどの変数を指しているか」のみに注目し、まずはポインタのポインタについてのイメージを掴んでいただければと思います
スポンサーリンク
ポインタの使い方
続いてポインタの使い方についておさらいしておきましょう!
ポインタの変数宣言
ポインタを使用するためには、int
や char
などの基本的な型の後ろに *
を付加して変数宣言を行います。
この変数宣言時の *
がポインタであることを示す目印になっているわけですね!
/* ポインタの変数宣言 */
int *p;
ポインタへのアドレス格納
また、変数が配置されているメモリ上のアドレスは、「変数名の前に &
を付加する」ことで取得することができます。
そして、ポインタでは、この変数名に &
を付加して取得したアドレスを格納することができます。
int *p;
int a;
/* アドレスの格納 */
p = &a;
これでこのポインタは変数 a
を指すことになります。
ポインタからのデータアクセス
さらに、「ポインタの変数名の前に *
を付加する」ことで、そのポインタが指しているアドレスのデータにアクセスすることができます。
int *p;
int a;
p = &a;
/* アドレスのデータへのアクセス */
*p = 100;
アクセスしたアドレスにはデータを格納したり、そのアドレスからデータを取得したりすることができます。
ポインタへの加減算
ポインタには加減算を行うことで格納されているアドレスを増減させることが可能です。
で、この時に実際にアドレスが増減する値は型によって異なります。
具体的には、ポインタ変数に対して +1
した際には、アドレスは「そのポインタ変数の型の基になる型のサイズ」分増加することになります(-1
の場合はそのサイズ分減少する)。
要は、ポインタの型から *
を削除した型のサイズ増加します。
例えば char*
型なら char
型のサイズ分(つまり 1
バイト)、short*
型なら short
型のサイズ分(つまり 2
バイト)増加することになります。
詳しくは下記ページで解説していますので詳しく知りたい方はこちらをご覧ください。
【C言語】ポインタの「型」について解説
ここまでは理解しているつもり!
でもポインタのポインタってまだイメージ湧かないなぁ
ポインタのポインタも結局は「ポインタの一種」なんだ
なのでここまで理解していればポインタのポインタについてもバッチリ理解できると思うよ
ポインタのポインタ(ダブルポインタ)
では本題である「ポインタのポインタ」について解説していきます(ポインタのポインタは「ダブルポインタ」とも呼ばれます)。
ポインタのポインタとは
まず、「ポインタのポインタ」はポインタとほぼ変わりません。
前述の通り、ポインタは他の変数を指す(他の変数のアドレスを格納する)ことができる変数です。
これは “ポインタのポインタ” も一緒です。”ポインタのポインタ” は他の変数を指す(他の変数のアドレスを格納する)ことができる変数です。
ただし “ポインタのポインタ “場合、この「他の変数」がポインタになります。
つまり、“ポインタのポインタ” とは「ポインタを指すポインタ」のことをいいます。まさに「ポインタ」のポインタです!
またポインタ(おさらい)でポインタが他の変数を指すことができる理由を下記のように説明しました。
このようにポインタが変数を指すことができるのは、変数が「メモリ上に配置」され、その変数に対して「アドレスが割り振られる」からです。
で、ポインタも結局は変数です。変数なので「メモリ上に配置」され、「アドレスが割り振られる」ことになります。
なので、ポインタもアドレスを持ち、”ポインタのポインタ” はこのポインタを指すことができます。
スポンサーリンク
ポインタのポインタの使い方
続いて “ポインタのポインタ” の使い方について解説していきます!
基本的な考え方はポインタと同じです!
ポインタのポインタの変数宣言
“ポインタのポインタ” を使用するためには、int
や char
などの基本的な型の後ろに **
を付加して変数宣言を行います。
“ポインタのポインタ” においては *
を2つ付けるところがポイントです。
この変数宣言時の **
が “ポインタのポインタ” であることを示す目印になっているわけですね!
/* ポインタのポインタの変数宣言 */
int **pp;
ポインタのポインタへのアドレス格納
また、ポインタが配置されているメモリ上のアドレスは、他の変数同様に「変数名の前に &
を付加する」ことで取得することができます。
そして、“ポインタのポインタ” では、このポインタの変数名に &
を付加して取得したアドレスを格納することができます。
int **pp;
int *p;
/* アドレスの格納 */
pp = &p;
これで、この “ポインタのポインタ” pp
はポインタ p
を指すことになります。
ポインタのポインタからのデータアクセス
さらに、”ポインタのポインタ” においても「”ポインタのポインタ” の変数名の前に *
を付加する」ことで、その “ポインタのポインタ” が指しているアドレスのデータにアクセスすることができます。
int **p;
int *p;
int a;
pp = &p;
/* アドレスのデータへのアクセス */
*pp = &a;
ポインタのポインタへの加減算
これも考え方はポインタと同じです。
“ポインタのポインタ” 変数に対して +1
した際には、アドレスは「その “ポインタのポインタ” 変数の型の基になる型のサイズ」分増加することになります(-1
の場合はそのサイズ分減少する)。
基になる型とは、”ポインタのポインタ” の型から *
を1つ削除した型のサイズです。
つまりポインタ変数の型のサイズ分増加することになります。
ポインタ変数の型のサイズは全て同じです(環境によって異なりますが 4
バイト or 8
バイトだと思います)ので、型によって増減値が変わらないところはポインタと異なりますが、アドレスの増減値に関しても考え方はポインタと一緒です。
なるほど!
“ポインタのポインタ” も結局ポインタと考えるとすんなり理解できるね!
確かにポインタも考えてみれば単なる変数だもんね!
そうそう!まずは分かりやすいイメージをもって苦手意識をなくすことが大事だよ!
ここからは “ポインタのポインタ” とポインタの違いと
ポインタのポインタの注意点について解説していくよ
“ポインタのポインタ” とポインタの違い
ここまでは “ポインタのポインタ” とポインタを同様のものとして考えて解説してきました。
ここからはこの2つの違いを踏まえて “ポインタのポインタ” について解説していきたいと思います。
“ポインタのポインタ” とポインタの決定的な違いは下記になります。
- ポインタのポインタ:指す対象がポインタ(つまりアドレスが格納された変数)
- ポインタ:指す対象がデータの実体(値そのものが格納された変数)
ポインタのポインタでアクセスできるデータはポインタ
つまり、”ポインタのポインタ” が指す先にはアドレスが格納されています。ですので “ポインタのポインタ” の変数の前に *
を付加して取得できるデータはアドレスを格納する変数、つまりポインタとして考えられます。
したがって、”ポインタのポインタ” の変数名の前に *
を付加すると、ポインタ同様にして扱うことができます。
そして、ポインタでも同様に、変数名の前に *
を付加することで、このポインタが指すアドレスのデータにアクセスすることができます。
ポインタのポインタには **
を付けてデータアクセス可能
こういった性質があるため、”ポインタのポインタ” の変数名の前に *
を2つ付加することで、すなわち **
を付加することで、「”ポインタのポインタ” が指すポインタ」が指すアドレスのデータにアクセスすることができます。
ただし、こういった使い方ができるのは「”ポインタのポインタ” がポインタを指し、その指されたポインタがデータそのもの(ポインタでない変数など)を指しているときだけ」です。
ポインタでも、他の変数などのデータそのものを指していない状態(アドレスが格納されていない状態)で *
をポインタの変数名に付加してデータにアクセスしても、どのアドレスにアクセスするか分かりません。
多くの場合、メモリアクセス違反が発生します。
これと一緒で、”ポインタのポインタ” においても、ポインタをしっかり指させた状態で *
を使ってデータにアクセスする必要があります。
さらに “ポインタのポインタ” の場合、その “ポインタのポインタ” が指しているポインタにおいても、他のデータをしっかり指させた状態で *
を使ってデータにアクセスする必要があります。
例えば下記はダメな “ポインタのポインタ” の使い方です。ポインタ p
にアドレスが格納されていない状態でポインタ p
の指すアドレスのデータにアクセスしているのでメモリアクセス違反になります。
#include <stdio.h>
int main(void) {
int **pp;
int *p;
/* ppにpを指させる */
pp = &p;
/* pの指すアドレスの値を表示 */
printf("%d\n", **pp);
return 0;
}
要は、”ポインタのポインタ” においては、その “ポインタのポインタ” だけでなく、そのポインタのポインタの先のポインタがどこを指しているかもしっかり考慮してプログラミングする必要があるということです。
先ほどの例を正しく動作させるためには、下記のようにポインタ p
にアドレスを格納してからポインタ p
の指すアドレスのデータにアクセスしてやれば良いです。
#include <stdio.h>
int main(void) {
int **pp;
int *p;
int a = 100;
/* ppにpを指させる */
pp = &p;
/* pにaを指させる */
p = &a; /* *pp = &a でも良い */
/* pの指すアドレスの値を表示 */
printf("%d\n", **pp);
return 0;
}
考慮する必要があることが多くなるので、この点だけはポインタに比べるとちょっと難しいかなぁと思います。
うーん、
こう聞くとやっぱり難しそう…
結局注意点の本質はポインタと同じだよ
要は、しっかりポインタの指す先を考慮してプログラミングしましょうって話だね
“ポインタのポインタ”の場合は “ポインタのポインタ” とポインタの両方に対してしっかり考慮が必要ってだけだよ
ポインタのポインタを用いたプログラム
“ポインタのポインタ” について理解を深めるために、実際のプログラムを動作させて “ポインタのポインタ” の動きを確認していきましょう!
スポンサーリンク
ソースコード
今回は下記のソースコードのプログラムで “ポインタのポインタ” の動作を解説していきたいと思います。
#include <stdio.h>
int main(void) {
int **dptr;
int *ptr;
int data;
data = 123;
ptr = &data;
dptr = &ptr;
printf("&data = %p\n", &data);
printf("&ptr = %p\n", &ptr);
printf("ptr = %p\n", ptr);
printf("*ptr = %d\n", *ptr);
printf("dptr = %p\n", dptr);
printf("*dptr = %p\n", *dptr);
printf("**dptr = %d\n", **dptr);
return 0;
}
実行すると下記のような結果になりました(環境や実行したタイミングによって値は異なります)。
&data = 0x7ffeefbff464 &ptr = 0x7ffeefbff468 ptr = 0x7ffeefbff464 *ptr = 123 dptr = 0x7ffeefbff468 *dptr = 0x7ffeefbff464 **dptr = 123
0x
で始まるのがアドレスです。
なんだかよく分からないアドレスの値になっていますが、アドレスの値自体は気にしなくて良いです。
注目していただきたいのは、どれとどれが同じ値になっているかです。
まず &data
と ptr
が同じ値になっていますね。これはつまりポインタ ptr
に data
のアドレスが格納されているということになります(ptr
が data
を指している)。
さらに &ptr
と dptr
が同じ値です。これはつまりポインタのポインタ dptr
に ptr
のアドレスが格納されているということになります(dptr
が ptr
を指している)。
したがって data
と ptr
と dptr
の関係を図示すると下のようになります。
プログラムの動き
なぜ上図のような関係になるのかの詳細を、プログラムの動きを踏まえて考えていきましょう!
まず下記の変数宣言により、3つの変数 dptr
、ptr
、data
がメモリ上に配置されます。
int **dptr;
int *ptr;
int data;
図で表すと下のようになります。
次に下記により、data
に値が格納されます。
data = 123;
図で表すと下のようになります。
続いて、下記によって ptr
に data
変数のアドレスが格納されます。つまり、ptr
は data
を指すことになります。
ptr = &data;
図で表すと下のようになります。ここは単純にポインタの話ですね!
ですので、ptr
の値を表示すると、data
のアドレス(つまり &data
)が表示されることになります。
&data = 0x7ffeefbff464 ptr = 0x7ffeefbff464
したがって、*ptr
の値を表示すると、ptr
が指す data
の値である 123
が表示されます。
*ptr = 123
ポインタからのデータへのアクセスを図で考えると下図のようになります。
続いて dptr
を見ていきましょう。ptr
がポインタであるのに対し、dptr
は “ポインタのポインタ” であり、ポインタを指すポインタです。
この dptr
には下記によって ptr
のアドレスが格納されます。つまり dptr
は ptr
を指すことになります。
dptr = &ptr;
これによりポインタ変数のアドレスを他のポインタが指すことになります。このポインタ変数を指すポインタが、”ポインタのポインタ” です。
図で表すと下のようになります。
dptr
は ptr
を指していますので、dptr
と &ptr
は同じアドレス値になります。
&ptr = 0x7ffeefbff468 dptr = 0x7ffeefbff468
また dptr
は ptr
を指していますので、*dptr
の値はポインタ ptr
に格納されているアドレスの値と同じになります。さらに前述の通り、ptr
は data
を指していますので、*dptr
と ptr
と &data
を表示すると同じアドレスの値が表示されることになります。
&data = 0x7ffeefbff464 ptr = 0x7ffeefbff464 *dptr = 0x7ffeefbff464
つまり、*dptr
で取得できるデータは変数 data
のアドレス、つまり変数 data
を指しているポインタです。
なので、このポインタに対して *
を付加すれば、変数 data
に格納されている値を取得できることになります。
したがって、*dptr
の前に *
を付加した **dptr
では、変数 data
の値を取得できることになります。
**dptr = 123
この辺りの動きを図示すると下図のようになります。
なるほど!
“ポインタのポインタ” が分かった気がするよ
難しそうだったけど、図があると分かりやすいね!
そう!
ポインタは絶対図を書いた方が動きを理解しやすいと思うよ!
特にプログラムがうまく動作しないような時は、図を書いてポインタの動きを整理してみることをオススメするよ
ポインタのポインタの使いどころ
では “ポインタのポインタ” はどういった場面で使われるのでしょうか?
この点について解説していきたいと思います。
スポンサーリンク
関数の引数
“ポインタのポインタ” が一番利用されるのは「関数の引数」です。
特に「関数内でポインタに格納されているアドレスを変更したい場合」に使用されます。
下記ページでも解説していますが、関数内で引数の変数の値を変更しても、関数呼び出し元で使用される変数にはその変更は反映されません。
【C言語】ポインタを初心者向けに分かりやすく解説これは、関数呼び出し時に指定する変数と、関数の引数で渡される変数は全く異なるデータだからです(値がコピーされているだけ)。
関数内で変数等の値を変更するためには、引数としてはその変数を指すポインタを渡す必要があります。
ポインタ変数は関数呼び出し元と関数内とで異なりますが、それらには同じアドレスが格納されています。つまり、関数内のポインタから関数呼び出し元の変数を指すことができます。
ですので、そのアドレスにあるデータを変更すれば、関数呼び出し元の変数を変更することになります。
つまり、引数にポインタ(アドレス)を渡すことで、関数内から関数呼び出し元の変数やデータを変更するようなことができます(この辺りは上記のページで解説しています)。
これと一緒で、ポインタに格納されているアドレス(つまりポインタの指す先)を関数内で変更したい場合は、そのポインタを指すポインタ、つまりは “ポインタのポインタ” を関数の引数に指定する必要があります。
例えば下記のソースコードの getPtr
関数では、引数 ptr
に data
のアドレスを格納していますが、関数の引数の ptr
と、関数呼び出し元の ptr
は全く異なるデータなので、関数呼び出し元の ptr
には data
は格納されていません。
なので、最後の printf
で表示される値は 1000
にならないはずです(たまたまなる場合もある)。
#include <stdio.h>
static int data = 1000;
void getPtr(int *ptr) {
ptr = &data;
}
int main(void){
int *ptr;
getPtr(ptr);
printf("%d\n", *ptr);
return 0;
}
一方で、下記のソースコードの getPtr
関数では、引数 dptr
は “ポインタのポインタ” なので、*dptr
に data
のアドレスを格納してやれば、関数呼び出し元の ptr
に data
のアドレス格納されることになります。
#include <stdio.h>
static int data = 1000;
void getPtr(int **dptr) {
*dptr = &data;
}
int main(void){
int *ptr;
getPtr(&ptr);
printf("%d\n", *ptr);
return 0;
}
こんな感じで「関数内でポインタに格納されているアドレスを変更したい場合」には “ポインタのポインタ” を利用することがあります。
2次元データ
後は2次元のデータを利用するような際にも “ポインタのポインタ” を使用することがあります。
“ポインタのポインタ” で2次元のデータを扱う例は下記ページで紹介していますので、詳しく知りたい方は是非読んでみてください。
C言語で2次元データをいろいろな方法で扱ってみる(二次元配列・ポインタのポインタなど)例えば画像などは縦方向と横方向に画素が広がる2次元のデータとして考えて扱われることがあります。
ポインタの学習
次は実践的な話ではないですが、”ポインタのポインタ” の利用によりポインタへの理解をより深められると思います。
ポインタは「どこを指しているか」をしっかり考えて利用することが重要です。
さらに “ポインタのポインタ” は、この “ポインタのポインタ” 自身だけでなく、これが指しているポインタも含めて「どこを指しているか」をしっかり考える必要があります。
逆にいうと、”ポインタのポインタ” は「どこを指しているか」をしっかり考えてプログラミングしないと上手く動作してくれないので、その修正やデバッグなどを繰り返すことで自然と「どこを指しているか」を考える力がつくと言えると思います。
確かに!
“ポインタのポインタ” を学ぶことで、よりポインタがデータを指すイメージが身についたよ!
スポンサーリンク
ポインタのポインタのポインタ(トリプルポインタ)
次は、ここまでの話を踏まえてさらに応用的なポインタの使い方について紹介したいと思います。
“ポインタのポインタ” がポインタを指すことができるのは、ポインタがメモリ上に配置されてるからだよね?
“ポインタのポインタ” もメモリ上に配置されてるんだから、これも他のポインタから指すことができるの?
いい気付きだね!できるよ!
“ポインタのポインタ” を指すポインタだから “ポインタのポインタのポインタ” って言っていいかもね
下の図はポインタのポインタを用いたプログラムでお見せした “ポインタのポインタ” である dptr
、ポインタである ptr
、int
型の変数である data
の3つの関係性を示した図になります。
ポインタの ptr
が “ポインタのポインタ” の dptr
で指すことができるように、”ポインタのポインタ” の dptr
もメモリ上に存在してアドレスが割り振られているため、他のポインタにより指すことができます。
このような、”ポインタのポインタ” を指すポインタをトリプルポインタと呼びます。
詳細な説明は省略しますが、”ポインタのポインタ” と同様の考え方で使用することができます。
下記はトリプルポインタを用いたプログラムのソースコード例になります。
#include <stdio.h>
int main(void){
int ***tptr;
int **dptr;
int *ptr;
int data;
data = 123;
ptr = &data;
dptr = &ptr;
tptr = &dptr;
printf("&data = %p\n", &data);
printf("data = %d\n", data);
printf("&ptr = %p\n", &ptr);
printf("ptr = %p\n", ptr);
printf("*ptr = %d\n", *ptr);
printf("&dptr = %p\n", &dptr);
printf("dptr = %p\n", dptr);
printf("*dptr = %p\n", *dptr);
printf("**dptr = %d\n", **dptr);
printf("tptr = %p\n", tptr);
printf("*tptr = %p\n", *tptr);
printf("**tptr = %p\n", **tptr);
printf("***tptr = %d\n", ***tptr);
return 0;
}
実行結果は下記の通りになります。どのポインタがどのデータを指しているかを図を書いて考えてみると、より理解が深まると思います。
&data = 0x7ffeefbff45c data = 123 &ptr = 0x7ffeefbff460 ptr = 0x7ffeefbff45c *ptr = 123 &dptr = 0x7ffeefbff468 dptr = 0x7ffeefbff460 *dptr = 0x7ffeefbff45c **dptr = 123 tptr = 0x7ffeefbff468 *tptr = 0x7ffeefbff460 **tptr = 0x7ffeefbff45c ***tptr = 123
“ポインタのポインタのポインタ” もメモリ上に配置されるんだよね?
もしかして “ポインタのポインタのポインタのポインタ” もあるの?
一応あるよ!
でも実際に使われてる例は見たことないけどね…
ここまで解説してきた内容を踏まえれば、実は下記のようにもっと深い階層を指すポインタも利用することはできます。
が、こんな使い方をしているプログラムは見たことないですね…。
一応こういった使い方ができることや、なぜこのような使い方ができるのかは覚えておくと良いと思います!
#include <stdio.h>
int main(void){
int *******ptr7;
int ******ptr6;
int *****ptr5;
int ****ptr4;
int ***ptr3;
int **ptr2;
int *ptr1;
int a = 100;
ptr1 = &a;
ptr2 = &ptr1;
ptr3 = &ptr2;
ptr4 = &ptr3;
ptr5 = &ptr4;
ptr6 = &ptr5;
ptr7 = &ptr6;
printf("%d\n", *******ptr7);
return 0;
}
まとめ
このページでは “ポインタのポインタ” について解説しました。
“ポインタのポインタ” も結局はポインタの一種です。
ポインタが使いこなせれば “ポインタのポインタ” も使いこなすことができると思います。
ただし、”ポインタのポインタ” が指すデータはポインタになります。ここだけ注意です。
指すデータがポインタなので、”ポインタのポインタ” の変数名に **
を付加することで、「”ポインタのポインタ” が指すポインタ」が指すアドレスのデータにアクセスするような使い方もできます。
ポインタ同様にどのデータを指しているかをしっかり考えて利用するようにしましょう!
オススメの参考書(PR)
C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!
この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。
- C言語でのメモリの使い方
- 配列とポインタの関係性
- ポインタのよくある使い方
- ポインタの効果的な使い方
一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。
また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。
ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。
ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。
入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/
分かりにくいのは表現に不備があるから
ポインタのポインタではなく
正確には、ポインタが格納された変数のポインタ