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