今まで何度かvoid *の説明を求められましたが、なかなか納得してもらえませんでした。説明を工夫するだけでは限界があると感じたので、別の方法でどうにかならないかを考えてみました。
前回の記事の延長線上で説明します。サンプルコードを再掲します。
mov byte ptr [0x00000001], 0x12 mov word ptr [0x00000004], 0xfeca mov dword ptr [0x0000000a], 0xefbeadde
これを機械的にC言語に書き換えます。
*(char *)0x00000001 = 0x12; *(short *)0x00000004 = 0xfeca; *(long *)0x0000000a = 0xefbeadde;
これを提示した意図は、ポインタの意味に深入りしないで、出発点になる基本構文を覚えてもらおうということです。
レジスタ
アドレスをレジスタに入れてみます。
mov eax, 0x00000001 mov ebx, 0x00000004 mov ecx, 0x0000000a mov byte ptr [eax], 0x12 mov word ptr [ebx], 0xfeca mov dword ptr [ecx], 0xefbeadde
レジスタはただの数値なので、intで表現できます。
int eax = 0x00000001; int ebx = 0x00000004; int ecx = 0x0000000a; *(char *)eax = 0x12; *(short *)ebx = 0xfeca; *(long *)ecx = 0xefbeadde;
32bitではアドレスもintも32bitのため、うまく動きます。
【注】このコードには問題がありますが、次で説明します。
intptr_t
アドレスやintのサイズは環境によって異なります。64bit Windowsではアドレスが64bit、intが32bitのため、アドレスをintに入れることができません。32bitから64bitに移植する際、よく問題になります。
このような環境依存を避けるため、アドレスと同じサイズのintptr_t型が導入されました。
intptr_t eax = 0x00000001; intptr_t ebx = 0x0000000a; intptr_t ecx = 0x00000004; *(char *)eax = 0x12; *(short *)ebx = 0xfeca; *(long *)ecx = 0xefbeadde;
intptr_tは普通の整数型なので、普通に計算ができます。
#include <stdio.h> #include <stdint.h> int main(void) { intptr_t a = 1; intptr_t b = 2; intptr_t c = a + b; printf("%d\n", c); return 0; }
ポインタ
アドレスに型を付けたのがポインタです。
char *eax = (char *)0x00000001; short *ebx = (short *)0x00000004; long *ecx = (long *)0x0000000a; *eax = 0x12; *ebx = 0xfeca; *ecx = 0xefbeadde;
事前にキャストをすることで、代入時のキャストが不要になります。何度も代入するときには便利です。
void *
intptr_tが規格化されたのはC99です。それまではアドレスを表現するのにvoid *がよく使われていました。
※ 今でもvoid *を使う方が多いと思います。
void *は型を伴わないため、メモリへの書き込みに使用するときは、intptr_tと同じようにキャストする必要があります。
void *eax = (void *)0x00000001; void *ebx = (void *)0x0000000a; void *ecx = (void *)0x00000004; *(char *)eax = 0x12; *(short *)ebx = 0xfeca; *(long *)ecx = 0xefbeadde;
void *は整数型ではないため、intptr_tのように計算に使用することはできません。
void *a = (void *)1; void *b = (void *)2; void *c = a + b; // エラー
アドレス書き込み
アドレスはただの数値なので、普通にメモリに書き込めます。
mov eax, 0x00000080 mov dword ptr [eax], 0x00004550 mov dword ptr [0x0000003c], eax
intptr_tでC言語に変換します。
intptr_t eax = 0x00000080; *(long *)eax = 0x00004550; *(intptr_t *)0x0000003c = eax;
メモリに書き込むときの型は機械的に対応しています。
- longを書き込む → *(long *)
- intptr_tを書き込む → *(intptr_t *)
intptr_tをvoid *に置き換えてみます。
void *eax = (void *)0x00000080; *(long *)eax = 0x00004550; *(void **)0x0000003c = eax;
構文を適用した結果、機械的にvoid **が出て来ることに注目してください。
- void *を書き込む → *(void **)
void **の意味を考えるとわけが分からなくなるので、このように形式的なものだと割り切れば良いかもしれません。
コツ
キャストをデリファレンスする *(void **) のような構文は、左の*が右の*を打ち消すようにイメージすれば最終的に処理される型が分かりやすいと思います。
- *(int *) ⇒ int
- *(void **) ⇒ void *
アドレス計算
ここまでの例ではアドレスを即値指定していましたが、現実には即値を使うことはほとんどありません。
通常使うmalloc()に適用してみます。
1.c
intptr_t p1 = (intptr_t)malloc(10); intptr_t p2 = (intptr_t)malloc(10); *(char *)(p1 + 2) = 'a'; *(intptr_t *)(p1 + 4) = p2;
intptr_tは通常の整数型です。malloc()で返されたアドレスを計算してメモリを操作しています。
void *を使って書き換えてみます。void *は計算できないのでchar *にキャストします。
2.c
void *p1 = malloc(10); void *p2 = malloc(10); char *p3 = (char *)p1; *(p3 + 2) = 'a'; *(void **)(p3 + 4) = p2;
どうせchar *にキャストするなら、最初からchar *を使えばすっきりします。