このページではポインタの型について解説していきたいと思います。
下のページでポインタは他の変数を指す矢印であることを説明しました(アドレスを格納する変数)。ただしこのページではポインタの型については触れていません。
【C言語】ポインタを初心者向けに分かりやすく解説通常の変数に int
や char
や short
といった型が存在するように、ポインタ変数にも int*
や char*
や short*
といった型が存在します。
ではこのポインタ変数の型によって何が異なるのでしょうか?
うーん、int
や char
はサイズが違うよね
さらに表現できる値の種類も違う
それと一緒で、型によってサイズや表現できる値の種類が異なるんじゃないの?
そう考えるのが自然だよね…
だけど、ポインタの型は全てサイズも同じ、さらに表現できる値の種類も一緒なんだ
は?
じゃあポインタの型の意味って…
実は、通常の変数の型の int
や char
などとは異なり、ポインタ変数の型では型によってサイズや表現できる値に差はありません。
char*
であろうが int*
であろうが long*
であろうがポインタ型は全てサイズは同じです。
では、この型の違いにどのような意味があり、型の違いによってどのような違いがあるのでしょうか?
この辺りをこのページで解説したいと思います。
Contents
ポインタの「型」とは
ではポインタの「型」がどのような意味を持つのかについて解説していきたいと思います。
通常変数の型
と、その前にまずは通常変数の型についておさらいしておきたいと思います。
通常変数では、型によって下記の違いがあります。
- サイズ
- 扱われ方
型によってサイズが違う
まず型によってサイズが異なります。
例えば int
型のサイズは 32
ビット(4
バイト)、char
型のサイズは 8
ビット(1
バイト)といったように、型によってサイズが異なります。
そして、これによって変数の型によって、その変数に格納できる数値(表現できる数値)の種類(範囲)に違いがあります。
各型の変数に格納できる数値の違いの一例は下記のようになります。
型 | 数値の範囲 |
char |
-128 〜 127 |
short |
-32768 〜 32767 |
int |
-2147483648 〜 2147483647 |
型によって扱われ方が違う
また型によって変数に格納されたデータの扱いが異なります。
例えば int
型に格納されたデータはは符号 “あり” の数値として扱われますが、unsigned int
型に格納されたデータは符号 “なし” の数値として扱われます。
また int
型や short
型は格納されたデータは “整数” として扱われますが、float
型や double
型の変数に格納されたデータは “浮動小数点数” として扱われます。
こんな感じで、通常の変数においては型によって扱われ方が異なります。
スポンサーリンク
ポインタの型
では、本題である「ポインタの型」についてはどうでしょう?
全ての型でサイズは同じ
まずポインタ型の変数では、型による「サイズの違いは無い」です。全ての型でサイズは同じです。
ポインタ型の変数に格納される値は「アドレス」として扱われます。
このアドレスのサイズは CPU によって異なりますが、おそらく現在だと 64
ビット(8
バイト)になると思います。
型によって扱われ方が違う
ただし、通常の変数同様に型によって扱われ方が異なります。
具体的には、型によって下記の2点の違いがあります。要は、ポインタの型によって下記の2つが決まります。
- アクセス先のデータの型
- 加減算時のアドレス増減値
それぞれについて、詳細を解説していきたいと思います。
ポインタの型でアクセス先のデータの型が決まる
まず、ポインタの型によって「アクセス先のデータの型」が決まります。この詳細を説明していきます。
ポインタ変数でのデータのアクセス
ポインタ変数は、変数使用時に変数名の前に *
を付加することで、そのポインタに格納されるアドレスのデータ(そのポインタが指すデータ)にアクセス(取得や格納)することができます。
下記はポインタ変数に格納されるアドレスのデータに値を格納する例になります。
int *p;
int a = 0;
p = &a; /* pにaを指させる */
*p = 1000; /* pが指す先に100を格納 */
printf("%d\n", a); /* 1000が表示される */
上記では、*p
によりアクセスされるデータは何型として扱われるでしょうか?
int
型の変数 a
を指してるんだから int
型じゃない?では次のポインタでのデータのアクセスについてみてみよう
スポンサーリンク
ポインタ変数でアクセスしたデータの扱い
ポインタ変数では、そのポインタの型の基になる型の変数を指す(アドレスを格納する)のが一般的です。
「ポインタの型の元になる型」とは、例えば int*
型であれば int
型、char*
型であれば char
型といったように、ポインタの型から *
を除去した型のことを言っています
ただし、異なる型の変数を指すことも可能です。
下記は char*
型のポインタ変数に int
型の変数を指させる例になります。
char *p;
int a = 0;
p = &a;
*p = 1000;
printf("%d\n", a);
上記では、*p
によりアクセスされるデータは何型として扱われるでしょうか?
int
型の変数 a
を指してるんだから int
型?
実際は char
型として扱われているんだ
上記のソースコードでは、ポインタ変数 p
に int
型の変数 a
を指させているので、*p
でアクセスしたデータは int
型として扱われると思われる方もいるかもしれません。
しかし、実際にはポインタ変数 p
の型の「基になる型」である char
型のデータとして扱われます。
なので、上記プログラムを実行すると printf
での表示は 1000
にはなりません。
これは *p
が char
型のサイズである 8
ビットのデータとして扱われるためです。1000
は 8
ビットでは表現できないため桁あふれが発生し、実際にはその桁あふれが発生した結果である 232
が p
の指す変数 a
に格納されることになります。
この辺りの動きを図示しながら説明すると、下記のようになります。
まず変数宣言により変数 a
と変数 p
がメモリ上に配置されます。
一つの四角が 1
バイトを表しており、変数 a
は int
型なので 4
バイト、変数 p
はポインタ型なので 8
バイトのデータとして図示しています。
次に p = &a
により、p
に a
のアドレスが格納され、p
が a
を指すことになります。
次に行うのが *p = 1000
です。
p
は a
を指しているので、当然 a
に対して 1000
を格納しようとします。
ただし、*p
でアクセスするデータは、p
の型の「基になる型」として扱われるため、char
型のサイズである 1
バイトのデータに 1000
を格納することになります。
つまり、a
の先頭の 1
バイトに 1000
を格納しようとします。ただ、桁あふれが発生するので、桁あふれ発生後の値が格納されることになります。
で、最後に a
を表示しているので、先頭の 1
バイトにのみ値が格納された int
型変数の中身、つまり 232
が表示されることになります。
こんな感じで、ポインタ変数がどんな型の変数を指していたとしても(どんな型の変数のアドレスを格納していたとしても)、ポインタ変数からアクセスするデータは、そのポインタ変数の型の「基になる型」として扱われます。
つまり、ポインタの型によって「アクセス先のデータの型」が決まるということになります。
異なる型のデータを指す応用例
これを利用することで、結構いろんなことが行えます。
例えば、下記では int
型の変数を char
型のデータとして扱うことで、4
バイトのデータを 1
バイトずつ分割して表示しています。
#include <stdio.h>
int main(void) {
char *p;
int a = 0x01234567;
p = &a;
printf("%02x\n", *p);
printf("%02x\n", *(p+1));
printf("%02x\n", *(p+2));
printf("%02x\n", *(p+3));
return 0;
}
実行すると下記のように int
型の変数のデータが 1
バイトずつ表示されます。
67 45 23 01
また、下記では浮動小数点数を int
型のデータとして扱うことで、内部データを整数として扱っています。
#include <stdio.h>
int main(void) {
int *a;
float b = 0.625;
a = &b;
/* 内部データを2進数表示 */
for (int i = 0; i < 32; i++) {
unsigned int shift;
shift = (unsigned int)1 << (31 - i);
printf("%u", (*a & shift) >> (31 - i));
}
printf("\n");
return 0;
}
実行すると、浮動小数点数の内部データを2進数化したものが表示されます。
00111111001000000000000000000000
浮動小数点数や内部データって何?という方は、下記ページで解説していますのでこちらを参照していただければと思います。
【C言語】浮動小数点数における「数値⇔内部データ(符号部・指数部・仮数部)」の変換
ポインタ変数に「基になる型」意外の変数のアドレスを格納しようとすると警告が出るんだ
意図しない結果になる可能性がありますよ?!っていう警告だね
実際上記の例でも結構イレギュラーな使い方してるからね…
このページでは、あえてポインタに「基になる型」とは異なる型の変数を指させています。
実際便利な場面もあるのですが、バグの原因になる可能性も高いです。
例えば、下記のように int*
型のポインタで、int
型よりもサイズの小さい char
型の変数を指すと、指している変数のサイズを超えて他の変数の値を意図せず変更してしまうような場合があります。
#include <stdio.h>
char a = 0;
char b = 0;
char c = 0;
char d = 0;
int main(void) {
int *p;
p = &a;
*p = 0x01234567;
printf("%02x\n", a);
printf("%02x\n", b);
printf("%02x\n", c);
printf("%02x\n", d);
return 0;
}
使い方には十分に気をつけましょう!
加減算時のアドレス増減値
次はポインタの型による「加減算時のアドレス増減値」の違いについて解説していきたいと思います。
スポンサーリンク
ポインタ変数に対する加減算
C言語では、ポインタ変数に対して加減算を行うことで、ポインタ変数に格納されているアドレスを増やしたり減らしたりすることができます。
例えば下記のように、配列の先頭アドレスを指しているポインタ変数に加算を行うことで、次の要素のデータにアクセスすることができますね!
#include <stdio.h>
int main(void) {
int *p;
int a[100];
int i;
p = a;
for (i = 0 ; i < 100; i++) {
*p = i;
p++;
}
for (i = 0; i < 100; i++) {
printf("%d\n", a[i]);
}
return 0;
}
加減算時のアドレスの増減値
では、ポインタ変数に +1
すると、アドレスはいくつ増減するでしょう?
例えば下記のように float*
型のポインタ変数に +1
する前後のアドレスを表示するといくつの差が生じるでしょうか?
float *p;
float a[2];
p = a;
printf("before = %p\n", p);
printf("after = %p\n", p+1);
1
足してるんだから 1
でしょ!4
なんだ…
なんで…
ポインタ難しい…
ポインタ変数に +1
した時に、格納されているアドレスがいくつ増加するかは「ポインタの型」によって異なります。
より具体的に言うとポインタの型の「基になる型のサイズ」分増加することになります。
+n
した時は n × 基になる型のサイズ
アドレスが “増加”し、-n
した時は n × 基になる型のサイズ
アドレスが “減少” することになります。
前述のプログラムでは、float*
型のポインタに対して +1
しているので、アドレスとしてはその基になる float
のサイズ分、すなわち 4
バイト増加することになります。
なので、前述のプログラムを実行して表示されるアドレスは下記のようになり、+1
することでアドレスが 4
増加していることが分かると思います。
before = 0x7ffeefbff460 after = 0x7ffeefbff464
要は、前述の通りポインタからアクセスするデータは、「そのポインタの型の基になる型として扱われる」ので、次にアクセスするデータ(ポインタに +1
してアクセスするデータ)は、その基になる型の「サイズ分ずらしたアドレスのデータ」になるということです。
下記は各ポインタ型に +1
した時のアドレスの変化を表示するプログラムの例になります。
#include <stdio.h>
int main(void) {
char *charp = 0x1000;
short *shortp = 0x1000;
int *intp = 0x1000;
double *doublep = 0x1000;
printf("%p(%d)\n", charp+1, sizeof(char));
printf("%p(%d)\n", shortp+1, sizeof(short));
printf("%p(%d)\n", intp+1, sizeof(int));
printf("%p(%d)\n", doublep+1, sizeof(double));
return 0;
}
ちなみに配列の要素指定においても、同様のことが言えます。 例えば int
型の配列 array
について考えると、array[n]
のアドレスと array[n+1]
のアドレスの差は int
型のサイズである 4
になります。
int array[100];
printf("array[10] = %p\n", &array[10]);
printf("array[11] = %p\n", &array[11]);
ポインタの型の選び方
最後にポインタの型の選び方について解説していきたいと思います。
ここまでポインタの型によって下記の2つが異なることを説明してきました。
- アクセス先のデータの型
- 加減算時のアドレス増減値
この2つのうち、ポインタの型の本質的な違いは前者の「アクセス先のデータの型」だと思います。
後者に関しては、前者の「アクセス先のデータの型」の違いを実現するための帳尻合わせのようなものです。前者を行う上で後者を行う方が便利なのでそうなっているだけだと思います。
したがって、ポインタの型は「アクセス先のデータをどの型として扱うか」に応じて設定すれば良いと思います。
例えば、アクセス先のデータを「符号なしの 4
バイトの整数」として扱いたいのであれば unsigned int*
型を選べばいいですし、「8
バイトの浮動小数点数」として扱いたいのであれば float*
型を選べばいいです。
うーん…
4
バイトとか浮動小数点数とか難しい….
ちょっと難しく感じる方もおられるかもしれませんが、変数をポインタに指させるのであれば、結局「ポインタが指す変数の型」に対するポインタ型を選べば良いと思います。
ポインタに変数を指させる場合、結局その変数に対してポインタを介してアクセスしたいだけだと思います。したがって、その変数の型としてデータを扱えるように、その変数の型に合わせたポインタの型を選べば良いです。
一方で、ポインタに変数以外を指させる場合(例えば malloc
で確保したメモリのデータを指させる場合)は、前述の通り、「アクセス先のデータをどの型として扱うか」をしっかり考えてポインタの型を選ぶようにしましょう!
スポンサーリンク
まとめ
このページでは「ポインタの型」について解説しました。
ポインタ変数においては、通常の変数と異なり型によってサイズは変わりません。
しかし、変数名に * を付加してポインタからデータにアクセスする際、そのデータはそのポインタの型の「基になる型」として扱われることになります。
また、ポインタに加減算を行った時のアドレスの変化もポインタの型に応じて異なります。
具体的にはポインタに +1
した際には、そのポインタの型の「基になる型」のサイズ分アドレスが増加することになります。
これらを利用して色々応用することも可能ですが、特にポインタに慣れていない方は、まずはポインタに指させたい変数の方に応じてポインタの型を選ぶようにしましょう!
ちなみにポインタの型には void*
もあります。
この void*
については下記ページで解説していますので、是非こちらも合わせてお読みください!
オススメの参考書(PR)
C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!
この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。
- C言語でのメモリの使い方
- 配列とポインタの関係性
- ポインタのよくある使い方
- ポインタの効果的な使い方
一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。
また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。
ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。
ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。
入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/