C言語の規格は結構いい加減で、処理系依存と言うcompilerが勝手に決めてよ
い部分が何箇所かあります。そのため、AVRではうまく動くプログラムがH8に
もっていくと動かない等といったことが起こりえます。
せっかく書いたプログラムが別のマイコンでは使えないというのも寂しいので、
移植性のよいプログラムを書くための注意点をあげてみましょう。
処理系依存で一番有名なのは、sizeof(int)
でしょう。C言語の
整数型はshort
,int
,long
とありま
すが、それぞれ何bitであるかは、compilerが自由に決めて良く、
sizeof(short) <= sizeof(int) <= sizeof(long)
であることだ
けが規格で決められています。
ですので、short
もint
もlong
も
16bitという処理系もあれば、short
が16bit、int
が32bit、long
が64bitという処理系もあります。
例えば、RS-232Cで繋いだマイコンに整数型を出力するつもりで、以下のよう
なプログラムを書いたとしましょう。PCもマイコンもint
が
32bitなら良いのですが、PC側が32bitでマイコンが16bitだったりすると、正
しくは動きません。
write(tty, &i, sizeof(int));
この問題を回避するため、C99以降ではstdint.hが導入されています。bit長が
重要な処理では、int
等を使う代わりに、stdint.hをinclude し
て、int16_t
やint32_t
などを使いましょう。
整数型の大きさすら決まっていないのですから、当然
sizeof(void*)
のようなpointer型の大きさも処理系依存です。
そのため、以下のようなpointerを整数型に代入するような処理は移植性を著
しく下げます。sizeof(int)
=2, sizeof(void*)
=4
だったり、sizeof(int)
=4, sizeof(void*)
=8とな
る場合、どうなるか考えてみてください。
int i; void* p; i=(int)p;
型関係で地味なところではsigned char
とunsigned
char
の話もあります。char
型はsigned
か
unsigned
か決まっていません。
char
がunsigned char
として扱われる系だと、以
下のようなプログラムはまともに動きません。8bitの整数型が必要なら、
int8_t
かuint8_t
を使うようにしましょう。
int test_char(char c) { if( c < 0 ){ return -1; }else{ return 1; } }
基本型に続いて構造体での注意を考えてみます。以下のようなプログラムを書 いたとしましょう。意図する通りに動くでしょうか?
typedef struct{ uint8_t type; uint16_t size; }DATA_HEADER; void send_header(int tty, DATA_HEADER* hdr){ { write(tty, hdr, sizeof(DATA_HEADER)); }
大抵の系でsizeof(DATA_HEADER)
が3にはなりません。4になった
り8になったりします。type
とsize
の間に
compilerが勝手に1 byteや3 byteのつめものをしてくれます。
なぜこんなことをするかというと、偶数アドレスへの読み書きが奇数アドレス
への読み書きより速いCPU があり、構造体の中の変数も積極的に偶数アドレス
に配置されるようにcompilerが気を利かせてくれるのためです。もちろん4の
倍数アドレスが速いCPUや8の倍数アドレスが速いCPUもあるので、構造体のど
こに何byteのつめものがされるかはCPUとcompiler次第となります。
対策としては、構造体をまとめてread()
や
write()
で入出力しないようにするしかありません。面倒くさい
ですが、各メンバ変数ごとに入出力しましょう。
続いて、整数型の並び順の話題です。次のようなプログラムを作ると
u16
には何が入るでしょう?
uint8_t u8[2]; uint16_t u16; u8[0] = 0x12; u8[1] = 0x34; u16 = *( (uint16_t*)u8 );
これも処理系依存で、u16
は0x1234か0x3412のどちらかになりま
す。基本的にCPUの仕様でどちらか決まっていて、0x1234になる系をbig
endian、0x3412になる系をlittle endianと呼びます。
big endianのCPUとしては、Motorolaの68kやRenesasのH8などが、little
endianのCPUとしてはIntelのx86があります。PowerPCやMIPS、SHなどは設定の
切替えでbig endianでもlittle endianでも動くようになっています。
並び順問題でうっかりやってしまいがちな例を出してみます。RS-232Cへ16bit 送信するプログラムですが、処理系によって0x12、0x34の順で送信されるか 0x34、0x12の順で送信されるかわかりません。
uint16_t u16; u16 = 0x1234; write(tty, &u16, 2);
処理系非依存にするため、以下のように1byteごとに変換してから送受信する ようにしましょう。
void send16(int tty, uint16_t u16) { uint8_t u8[2]; u8[0] = u16 >> 8; u8[1] = u16 & 0xff; write(tty, u8, 2); } uint16_t recv16(int tty) { uint16_t u16; uint8_t u8[2]; read(tty, u8, 2); u16 = ( (uint16_t)u8[0] << 8 ) | u8[1]; return u16; }
整数型の並び順と同様に、構造体のbit fieldの順番も処理系依存になります。
typedef union{ uint8_t u8; struct{ uint8_t b0:1; uint8_t b1:1; uint8_t b2:1; uint8_t b3:1; uint8_t b4:1; uint8_t b5:1; uint8_t b6:1; uint8_t b7:1; }; }UINT8_BITS;
上記のような共用体を用意した場合、
u8 =
b0 +
b1*2 +
b2*4 +
b3*8 +
b4*16 +
b5*32 +
b6*64 +
b7*128
となる処理系と
u8 =
b7 +
b6*2 +
b5*4 +
b4*8 +
b3*16 +
b2*32 +
b1*64 +
b0*128
となる処理系があります。
移植を前提にするならbit fieldは使わずshift、and、or演算の組合せで書き ましょう。単純なbitのon/offなら以下のように書けます。
u8 &= 0xf7; /* b3=0 */ u8 |= 0x20; /* b5=1 */
最後に割算の話題です。C言語ではなんと四則演算すら処理系依存なのです。
q1 = 7/3; r1 = 7%3; q2 = (-7)/3; r2 = (-7)%3;
この例では(q1
, r1
)=(2,1)なのは処理系非依存で
す。問題はq2
、r2
でして、(-7) = (-3)*3+2とも
(-7) = (-2)*3-1ともとれます。Cでは商が負の値になった場合、どちらに切り
捨てるか決まっていなくて、(q2
,r2
)の組合せが
(-3,2)になる処理系と(-2,-1)となる処理系があります。
この問題を避けるため、stdlib.hにdiv()
という関数があって、
この関数を使うとどの処理系でも
(q2
,r2
)=(-2,-1)となる演算を行えます。
(商の絶対値が小さくなるよう切り捨てる)
C99以降では、割算演算子の"/
"は処理系非依存になっ
て、div()
関数と同等の演算をするように規格化されています。