はじめに
誤解を避けるため、私はこのような書き方をほとんど使いません。
uint32_t *p;
p++;
p+=4;
代わりに、次のような形式だけを使います。
uint32_t *p;
uint32_t d;
d = p[4];
uint32_t *pt;
pt = &p[4];
しかし、ポインタを進めて走査する場合、後者の形式では走査用に必ず新しい変数が必要になります。一方、前者の形式は、関数に渡されたポインタをそのまま進められるので、O0 では必ず後者よりスタックを節約できます。
最近、ライブラリの性能を上げるため、可読性を犠牲にして直感的でない書き方に変更することにしました。そこで、以降のテストを行いました。
結論を先に述べます。ポインタに対して + 演算が行われたとき、加算される側のポインタの型が、実際の RAM アドレスの進み量を決めます。一覧は下の表のとおりです。
| ポインタの型 | RAMアドレスの進み量 |
|---|---|
| void* + 1 | 1 |
| uint8_t* + 1 | 1 |
| uint16_t* + 1 | 2 |
| uint32_t* + 1 | 4 |
| その他・構造体ポインタ + 1 | 型のサイズによる |
アドレスから値へ
以下のコードで簡単にテストします。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint8_t u8t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint16_t u16t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint32_t u32t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
/* 入口 */
int main(int argc, char *argv[])
{
uint8_t p8get;
uint16_t p16get;
uint32_t p32get;
p8get = u8t[1]; // = 1
p8get = *(uint8_t *)(u8t + 1); // = 1
p8get = u8t[2]; // = 2
p8get = *(uint8_t *)(u8t + 2); // = 2
p8get = u8t[3]; // = 3
p8get = *(uint8_t *)(u8t + 3); // = 3
p16get = u16t[1]; // = 1
p16get = *(uint16_t *)(u16t + 1); // = 1
p16get = u16t[2]; // = 2
p16get = *(uint16_t *)(u16t + 2); // = 2
p16get = u16t[3]; // = 3
p16get = *(uint16_t *)(u16t + 3); // = 3
p32get = u32t[1]; // = 1
p32get = *(uint32_t *)(u32t + 1); // = 1
p32get = u32t[2]; // = 2
p32get = *(uint32_t *)(u32t + 2); // = 2
p32get = u32t[3]; // = 3
p32get = *(uint32_t *)(u32t + 3); // = 3
return 0;
}
もちろん、結果は期待どおりです。たとえばポインタ u32t の +3 は、3 番目のバッファの値にアクセスします。とても合理的です.......でしょうか?
ご存じのとおり、RAM ではアドレス1つにつき1バイトが格納されます。すると u8t+1 は1バイト進み、u32t+1 は4バイト進むことになります。同じ +1 という操作でも、結果が異なるのです。
アドレスの数値
上記を確かめるため、次のコードで簡単にテストしました。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint8_t u8t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint16_t u16t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint32_t u32t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
/* 入口 */
int main(int argc, char *argv[])
{
uint8_t *p8get;
uint16_t *p16get;
uint32_t *p32get;
p8get = u8t; // = 0x7FF6757b4000 <u8t>
p16get = u16t; // = 0x7FF6757b4010 <u16t>
p32get = u32t; // = 0x7FF6757b4040 <u32t>
p8get = u8t + 1; // = 0x7FF6757b4001 <u8t+1>
p16get = u16t + 1; // = 0x7FF6757b4012 <u16t+2>
p32get = u32t + 1; // = 0x7FF6757b4044 <u32t+4>
p8get = u8t + 2; // = 0x7FF6757b4002 <u8t+2>
p16get = u16t + 2; // = 0x7FF6757b4014 <u16t+4>
p32get = u32t + 2; // = 0x7FF6757b4048 <u32t+8>
return 0;
}
このように、ポインタが表す実際の RAM アドレスは、同じ +1 を使っても数値の進み方が確かに異なります。
void* の場合
汎用ツールとして使う関数の多くは void* を受け取ります。システムの状態やフラグ条件に応じて、それを異なるポインタ型として解釈することがあるため、次のテストを行いました。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint8_t u8t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint16_t u16t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint32_t u32t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
/* 入口 */
int main(int argc, char *argv[])
{
void *pVget;
uint64_t pVV;
pVget = (void *)u8t; // = 0x7FF61F184000 <u8t>
pVget++;
pVV = (uint64_t)pVget; // = 0x7FF61F184001 <u8t+1>
pVget = (void *)u16t; // = 0x7FF61F184010 <u16t>
pVget++;
pVV = (uint64_t)pVget; // = 0x7FF61F184011 <u16t+1>
pVget = (void *)u32t; // = 0x7FF61F184040 <u32t>
pVget++;
pVV = (uint64_t)pVget; // 0x7FF61F184041 <u32t+1>
return 0;
}
void* は実際の RAM の挙動と完全に一致し、+n はそのまま +n であることがわかります。
void* ではないふりをする
void* を別の型のポインタとして解釈すると、次のテストになります。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint8_t u8t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint16_t u16t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
uint32_t u32t[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
/* 入口 */
int main(int argc, char *argv[])
{
void *pVget;
pVget = u8t; // = 0x7FF69FF64000 <u8t>
pVget = (uint8_t *)pVget + 1; // = 0x7FF69FF64001 <u8t+1>
pVget = u8t; // = 0x7FF69FF64000 <u8t>
pVget = (uint8_t *)(pVget + 1); // = 0x7FF69FF64001 <u8t+1>
pVget = u16t; // = 0x7FF69FF64010 <u16t>
pVget = (uint16_t *)pVget + 1; // = 0x7FF69FF64012 <u16t+2>
pVget = u16t; // = 0x7FF69FF64010 <u16t>
pVget = (uint16_t *)(pVget + 1); // = 0x7FF69FF64011 <u16t+1>
pVget = u32t; // 0x7FF69FF64040 <u32t>
pVget = (uint32_t *)pVget + 1; // 0x7FF69FF64044 <u32t+4>
pVget = u32t; // 0x7FF69FF64040 <u32t>
pVget = (uint32_t *)(pVget + 1); // 0x7FF69FF64041 <u32t+1>
return 0;
}
ここからわかるのは、+ 演算が行われるとき、加算される側の型が実際の RAM アドレスの進み量を決めるということです。
特別な場合
DSP チップでは1バイトが16ビットのため、(uint32_t *) 型に対する +1 は2アドレス分進みます。