C言語で難しいポイントの一つ、ポインタのキャスト。
キャスト自体はシンプルなのだが、皆大嫌いポインタと絡むとまたちょっと厄介になる。
とはいえ、C言語が組み込み開発やドライバ開発等でよく使われるという特性上、実際の開発ではこのポインタのキャストが頻出する。
実はめちゃくちゃ理解が必要なポイントだ。
実際に手を動かすことが大事!
プログラミング学習には、手を動かしながら理解することが大切。
そこでまず最初に、最高なサイトの紹介。
Web上で簡単にコーディングでき、実行までしてくれる。
そして、メジャーな言語を豊富にサポートしている。
実はC言語は、非常に学習しやすい言語だ。
なぜなら、ポインタによって全てを覗き見ることができるからだ。
「これ、どうなってんだろう」「こうしたら、どうなるんだろう」って思ったときに、上のサイトにアクセスしてとりあえず実装してみればよい。
で、printfでポインタの中でも見てみればよい。
時間をかけて書籍をただ読むよりも、大幅に理解が進むこと間違いない。
あれほど危険で、難しいポインタを逆手に取ってやろう。
型に対する正しい理解
キャストとは、型変換だ。
int型をchar型に変換したり、int型をdouble型に変換したり、何でも変換できる。
型においては、何ビットか?というサイズの情報が非常に大切だ。
今回は、「サイズ」に重きをおいてメジャーな型をまとめてみよう。
厳密にはプラットフォームによって異なるのだが、以下は昨今、一般的なコンピュータでよく使われているものとする。
型名 | 意味 | サイズ |
---|---|---|
char / unsigned_char | 8ビット整数型(「文字」としても扱える) | 8ビット (1バイト) |
short / unsigend_short | 16ビット整数型 | 16ビット (2バイト) |
int / unsigend_int | 32ビット整数型 | 32ビット (4バイト) |
long / unsigend long | 32ビット整数型 | 32ビット (4バイト) |
long long / unsigend long long | 64ビット整数型 | 64ビット (8バイト) |
float | 単精度浮動小数点数型 | 32ビット (4バイト) |
double | 倍精度浮動小数点数型 | 64ビット (8バイト) |
よく教科書で、「char型が文字型である」という風に書いてあると思う。
間違ってないのだが、こういう風に覚えてしまうとあまり実用的な理解には至らない。
char型はあくまでも整数型の一種であり、一番小さい8ビットの値を表す型である。
文字型というよりも、char型の8ビット整数を使って、文字として扱うこともできるというのが正しい。
8ビット整数は0~255の値であるが、これらの整数と文字との対応を用意してある。(ASCII)
ASCIIコード表にあるように、例えば整数65が文字「A」、整数66が文字「B」に対応している。
そしてプログラム上で文字を扱いたいというときには、char型の整数と先ほどの対応を照らし合わせて、文字として認識するようにしている。
(※これはC言語に限った話で、JavaやPythonでは「文字型」と認識するのが正しい。)
#include <stdio.h>
int main(void){
char a = 65;
// または char a = 'A';
printf("整数: %d\n", a); // 整数: 65
printf("文字: %c\n", a); // 文字: A
return 0;
}
paiza.ioのサイトで上記コードを打って実行してみると、char型が整数であり、文字としても表せるのが分かると思う。
そして今回型のサイズを強調した理由だが、ポインタのキャストでは、通常のキャストよりもサイズの意識が大切になってくるのである。
ポインタのキャスト
下記をpaiza.ioで実行してみよう。
結果がどうなるか分かるだろうか。
#include <stdio.h>
int main(void){
int data = 1084944399;
char* pc_data = (char*)&data;
short* ps_data = (short*)&data;
int* pi_data = (int*)&data;
printf("char*: %p\n", pc_data);
printf("short*: %p\n", ps_data);
printf("int*: %p\n", pi_data);
return 0;
}
6~8行目で、dataのアドレスをintとは別の型のポインタにキャストしているのが分かると思う。
これがポインタのキャストであるが、結果はどうなるか?
char*: 0x7fff65de01ac
short*: 0x7fff65de01ac
int*: 0x7fff65de01ac
キャストしようがしまいが、全部同じアドレスを示していることが分かると思う。
全て同じ値(data)を指すアドレスを格納しているのだから、その型がchar*型だろうが、short*型だろうが変わらない。
型が異なると、何が変わるのだろうか?
ポインタの型は、主に以下2つの情報を持っている。
- ポインタから値を参照したとき(*ptrのように、*をつけたとき)に、どういう型として扱うか?の情報
- ポインタがどれくらいの範囲で動くか?という情報
①ポインタから値を参照したときに、どういう型として扱うか?
paiza.ioで以下のコードを実行してみよう。
#include <stdio.h>
int main(void){
int data = 1084944399;
char* pc_data = (char*)&data;
short* ps_data = (short*)&data;
int* pi_data = (int*)&data;
printf("char: %d\n", *pc_data);
printf("short: %d\n", *ps_data);
printf("int: %d\n", *pi_data);
return 0;
}
以下のような実行結果が得られただろうか?
char: 15
short: -4081
int: 1084944399
dataの値、1084944399は2進数で下記のように表される。
01000000 10101010 11110000 00001111
これはメモリ上で以下のように配置されている。
「1000番地」とかはあくまで例で適当に設定したアドレス値である。
メモリ上では、4バイトごとに一つのまとまりとして配置されており(現在のコンピュータでは8バイトごとだったりするが、簡略化のため今回は4バイトとして考える)、4バイト(32ビット)整数の場合、大体のコンピュータはまとまりの最初のバイトから順に、整数の下位1バイトとして埋められていく。
そして上図の例において&dataが示しているのは、このデータが入っている起点アドレス(まとまりの一番最初のアドレス)である1000番地である。
このアドレスにある値を参照するときに、どういう型として参照するか?が、ポインタの型の情報である。
つまり上図のように、char*なら最初の1バイト、short*なら最初の2バイト、int*なら4バイト全体を参照する。
同じアドレスを指しているポインタでも、このように型によって、参照先の値をどう扱うかが変わる。
②ポインタがどれくらいの範囲で動くか?
次に、paiza.ioで下記コードを実行してみよう。
#include <stdio.h>
int main(void){
int data = 1084944399;
char* pc_data = (char*)&data;
short* ps_data = (short*)&data;
int* pi_data = (int*)&data;
printf("char*: %p\n", pc_data);
printf("short*: %p\n", ps_data);
printf("int*: %p\n", pi_data);
pc_data++; // ここでポインタ加算
ps_data++; // ここでポインタ加算
pi_data++; // ここでポインタ加算
printf("char*: %p\n", pc_data);
printf("short*: %p\n", ps_data);
printf("int*: %p\n", pi_data);
return 0;
}
私の環境では以下のようになった。
char*: 0x7ffe3a6a9124
short*: 0x7ffe3a6a9124
int*: 0x7ffe3a6a9124
char*: 0x7ffe3a6a9125
short*: 0x7ffe3a6a9126
int*: 0x7ffe3a6a9128
ポインタの増減においては、その型が表すサイズ単位でアドレスが増減する。
上の結果のように、charでは1バイトごと、shortでは2バイトごと、intでは4バイトごとに変わる。
long longでもdoubleでもfloatでもこれは同じである。
ここを間違えて実装すると、メモリ破壊に繋がる重要なポイントである。
つまり、ポインタのキャストを行うと?
結局のところ、深掘りして説明したのは「ポインタの型」の説明になってしまった。
というのも「ポインタをキャストする」ということは、ただただポインタの型のルール(先ほど説明した2点)に従った通りに扱いが変化する、というだけである。
つまりまとめると、ポインタのキャストを行うとポインタが示すアドレス自体は何も変わらないが、
- アドレスが示す先を参照したときの値の扱い方が、キャストした型のものになる
- ポインタを増減するときの増減の単位が、キャストした型のサイズになる
ということである。
これを知っておくと(当然の知識ではあるが)、メモリ上のデータを手のひらの上で転がすように自由自在に行ったり来たり、操れるようになる。
勿論大変危険なことで、C言語が安全でないと言われる所以の一つでもあるが、低レイヤの開発をするうえで必要な要素でもあるのだ。
今回はいったんここで終わりにし、次回から実際の開発ではどのようなことにポインタのキャストが使われるのか?という話をしてみよう。
↓↓↓ 次回はこちら ↓↓↓
コメント