ホーム » スタッフ » 斉藤徹 » ビットフィールドと共用体

2008年11月
« 10月   12月 »
 1
2345678
9101112131415
16171819202122
23242526272829
30  

最近の投稿(電子情報)

アーカイブ

カテゴリー

ビットフィールドと共用体

構造体を使った演習も終わったので、応用ネタ。 最初に、ビットフィールドの説明。 生年月日のデータをメモリに効率よく格納する方法として、 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() ;
}