ビットフィールドと共用体
構造体を使った演習も終わったので、応用ネタ。 最初に、ビットフィールドの説明。 生年月日のデータをメモリに効率よく格納する方法として、 2進数演算により複数の数値を1つの整数型に代入する方法を説明する。 2進数計算の面倒なところを紹介した後、ビットフィールドでの文法を説明する。
ビットフィールドの例
struct YMD { int year ; // 0..2100年程度なら 2^12 = 4096 より 12bit で表現可能 int month ; // 1..12 なので 2^4 = 16 より 4bit で表現可能 int day ; // 1..31 なので 2^5 = 32 より 5bit で表現可能 } ; // 本来なら、21bitで十分だけど、4byte×3=12byte(96bit) // 整数に21bitを押し込む // YYYYYYYYYYYYMMMMDDDDD ←2進数 int ymd = (1965 ≪ 9) | (2 ≪ 5) | 7 ; // 1965年 2月 7日のつもり // ymd から年月日を取り出す int year = ymd ≫ 9 ; // YYYYYYYYYYYYMMMMDDDDD ymd // YYYYYYYYYYYY (ymd ≫ 9) int month = (ymd ≫ 5) & 0xF ; // YYYYYYYYYYYYMMMMDDDDD ymd // YYYYYYYYYYYYMMMM ymd ≫ 5 // & 0000000000001111 ... & 0xF // 000000000000MMMM int day = ymd & 0x1F ; // YYYYYYYYYYYYMMMMDDDDD ymd // & 000000000000000011111 ... & 0x1F // 0000000000000000DDDDD // ymd の月を「3月」に修正 ymd = (ymd & ~(0xF ≪ 5)) | (3 ≪ 5) ; // 000000000000111100000 0xF ≪ 5 // 111111111111000011111 ~(0xF ≪ 5) // YYYYYYYYYYYYMMMMDDDDD ymd // 111111111111000011111 ~(0xF ≪ 5) // YYYYYYYYYYYY0000DDDDD ymd & ~(0xF ≪ 5) // or 001100000 (3 ≪ 5) // こんな大変な2進数計算じゃ、書き間違いするって... // ビットフィールドを使うと、 struct YMD { unsigned int year : 12 ; unsigned int month : 4 ; unsigned int day : 5 ; } ; struct YMD saitoh = { 1965 , 2 , 7 } ; printf( "%d/%d/%d\n" , saitoh.year , saitoh.month , saitoh.day ) ; // 1965/2/7 saitoh.month = 3 ; printf( "%d/%d/%d\n" , saitoh.year , saitoh.month , saitoh.day ) ; // 1965/3/7
ワード境界
ビットフィールドで、メモリを効率よく使う話とは反対の話として、ワード境界について話す。 最近のコンピュータであれば、メモリとのデータのやりとりは32bitなり64bitの一括で行う。 ワード境界とは、この一括してやりとりするデータ単位で、次のデータ単位との境界。 (32ビットでメモリアクセスするコンピュータであれば、(4*N-1)バイトと4*Nバイトの境界) この際に、構造体の1データがワード境界をまたがって配置されていると、1ワードのデータ 読み出しでも2回のメモリアクセスが必要となり、速度が低下する。
このためC言語では通常、ワード境界をまたぐような要素の配置はせずに、 使わないメモリを混入させてくれる。
struct A { // C言語では、通常packしない char n[3] ; // 3byte int x ; // 4byte double y ; // 8byte } ; struct A a[3] ; | packした時 | packしない時 | ----------+-------------+--------------+-------------------------------------- memory 00 | n0 n0 n0 x0 | n0 n0 n0 -- | ★packした時 memory 04 | x0 x0 x0 y0 | x0 x0 x0 x0 | printf( "%d" , sizeof( struct A ) ) ; memory 08 | y0 y0 y0 y0 | y0 y0 y0 y0 | => 15 memory 12 | y0 y0 y0 n1 | y0 y0 y0 y0 | memory 16 | n1 n1 x1 x1 | n1 n1 n1 -- | ★packしない時 memory 20 | x1 x1 y1 y1 | x1 x1 x1 x1 | printf( "%d" , sizeof( struct A ) ) ; memory 24 | y1 y1 y1 y1 | y1 y1 y1 y1 | => 16 memory 28 | y1 y1 n2 n2 | y1 y1 y1 y1 | memory 32 | n2 x2 x2 x2 | n2 n2 n2 -- | memory 36 | x2 y2 y2 y2 | x2 x2 x2 x2 | memory 40 | y2 y2 y2 y2 | y2 y2 y2 y2 | memory 44 | y2 | y2 y2 y2 y2 | packしてある構造体だと、a[0].x を読み出す時に、 memory 00 と memory 04 の2回メモリアクセスが発生する。
参考資料:
共用体
これまた、メモリを効率よく使うための文法。 データを保存したいけど、文字データ、整数データ、実数データのいずれかで覚えるという場合、
struct STRorINTorREAL { // printf( "%d" , sizeof( struct STRorINTorREAL ) ) ; char string[ 8 ] ; // 8+4+8 => 20byte int integer ; double real ; } ;
これでは、どれか1つのデータしか覚えないのなら、メモリがもったいない。 こういう時には、共用体を使う。
union STRorINTorREAL { // printf( "%d" , sizeof( struct STRorINTorREAL ) ) ; char string[ 8 ] ; // max(8,4,8) => 8byte int integer ; double real ; } ; union STRorINTorREAL x ; strcpy( x.string , "saitoh" ) ; x.integer = 1965 ; x.real = 12.3456 ;
しかし、共用体の中に何が入っているのか分からないと、使えないので、一般的には…
struct STRorINTorREAL { int type ; // 本当は列挙型を使いたいけど説明は来週の予定... union { // 無名共用体 char string[ 8 ] ; int integer ; double real ; } ; } ; void set_integer( struct STRorINTorREAL* p , int x ) { p->type = 1 ; p->integer = x ; } void set_real( struct STRorINTorREAL* p , double x ) { p->type = 2 ; p->real = x ; } void set_string( struct STRorINTorREAL* p , char x[] ) { p->type = 3 ; strcpy( p->string , x ) ; } void print( struct STRorINTorREAL* p ) { switch( p->type ) { case 1 : printf( "%d\n" , p->integer ) ; break ; case 2 : printf( "%f\n" , p->real ) ; break ; case 3 : printf( "%s\n" , p->string ) ; break ; } } void main() { struct STRorINTorREAL a[ 3 ] ; set_integer( &a[0] , 12345 ) ; set_real ( &a[1] , 12.345 ) ; set_string ( &a[2] , "abcde" ) ; for( int i = 0 ; i < 3 ; i++ ) print( &a[i] ) ; // 12345 , 12.345 , abcde が表示。 }
おまけ(C++やオブジェクト指向を個人的に勉強している人へ…)
配列に違う型のデータでも入れられるのが共用体を使う理由。 だけど C++ であれば、仮想関数を使ってもっとスマートにかける。
#include #include class Object { // 仮想基底クラス public: virtual void print() = 0 ; // "=0"の意味:仮想基底クラスでは何もしない } ; class Integer : public Object { public: int integer ; public: Integer( int x ) { integer = x ; } virtual void print() { printf( "%d\n" , integer ) ; } } ; class Real : public Object { public: double real ; public: Real( double x ) { real = x ; } virtual void print() { printf( "%f\n" , real ) ; } } ; class String : public Object { public: char string[ 8 ] ; public: String( char x[8] ) { strcpy( string , x ) ; } virtual void print() { printf( "%s\n" , string ) ; } } ; void main() { Object *a[ 3 ] ; // ポインタじゃないと都合が悪いので... // 配列a[3]に、整数、実数、文字列の違うデータを入れる a[ 0 ] = new Integer( 12345 ) ; a[ 1 ] = new Real( 12.345 ) ; a[ 2 ] = new String( "abcde" ) ; // 異なるデータがうまく表示できる for( int i = 0 ; i < 3 ; i++ ) a[ i ]->print() ; }