見出し画像

C言語教室 第18回 - ビットフィールドと typedef

さて構造体(と共用体)については、もうひとつ覚えることがあります。ビットフィールドという仕組みです。構造体(または共用体)のメンバ変数を格納する領域をビット単位で指定できるというものです。

ビットフィールド

例として年月日を格納する構造体を考えます。年は11ビットだと 2047 と実に微妙なところまでしか入らないので、12ビットにしておきます(紀元前のことは忘れましょう)。月は1~12までなので4ビットでいいですね。日は最大で31なのでちょうど5ビットです。

struct ymd {
  unsigned int year : 12;
  unsigned int month : 4;
  unsigned int day : 5;
};

これで、それぞれのメンバ変数には指定したビット数の領域が割り当てられ、代入するとその領域で格納できるサイズを越えた分は捨てられます。このように必要な大きさを具体的に指定することにより、構造体として宣言された変数の大きさを必要最低限にすることが出来ます。なお指定できるビット数は、その型のサイズまでなので、unsigned short で 20ビットの指定は出来ません。

他にも多くのフラグがある場合も同じように書けます。

struct flags {
  unsigned int flag1 : 1;
  unsigned int flag2 : 1;
  unsigned int flag3 : 1;
  unsigned int flag4 : 1;
};

もちろんビット演算子である ‘&’ ‘|’ または “>>” “<<” などを使って、同じ目的の処理をすることも出来ますが、ビットフィールドを使ったほうがわかりやすく扱うことが出来ます。

変数をひとつだけ扱う時は、あまり効果はありませんが、配列などにして多くの値を扱う時には必要な領域の大きさをかなり減らす効果もあります。但し、実際にどの程度の領域が必要になるかは、処理系に依存しますし、構造体のところで出てきたパディング指定によっても変化します。実際に使われる領域の大きさは、やはり sizeof 演算子で確認できます。

ビットフィールド

もうひとつの使い方として、ビットごとに使い方が決まっているデータにアクセスするときです。例えばIPv4パケットのヘッダの必須部分は、

struct ipv4_header {
  unsigned int version : 4;
  unsigned int ihl : 4;
  unsigned int dscp : 6;
  unsigned int ecn : 2;
  unsigned int length : 16;
  unsigned int id : 16;
  unsigned int flags : 4;
  unsigned int fragment : 12;
  unsigned int ttl : 8;
  unsigned int protocol : 8;
  unsigned int checksum : 16;
  unsigned int source : 32;
  unsigned int destination : 32;
};

と書けます。ただ実際にこの構造体のそれぞれのメンバ変数が、対象とするデータとまったく同じように配置されるかは別問題です。これを合わせるために、名前の無いメンバ変数を挟んだり(アクセスは出来ませんが指定した隙間を作ってくれます)、ビット数0のメンバ変数を入れることもあります(これでパディングをしてくれる)。

これを処理系毎に行うのは結構大変なので、実際にはデータを受け取った段階で、ビットフィールドを持つ構造体にビット演算で代入し、元の構造に戻すときにもビット操作でデータを作るという手順を踏むことが多いです。この方法でもそれぞれのメンバ変数が持つ値の範囲を限定できるので、毎回ビット操作を行うよりは可読性が格段に上がります。

IPヘッダのフォーマットとサイズの基本

ビットフィールドの注意点としては、メンバ変数のアドレスを取ることは出来ません。また構造体だけではなく共用体に対してもビットフィールドを指定することも出来ます。


今まで、構造体を定義して、その変数を宣言するのに、素直に struct 何とかと書いてきましたが、これを少し簡単にする方法があります。

C言語では typedef という構文で、型の別名を作ることができます。例えば unsigned というキーワードを打つのは実に面倒くさいので、

typedef  unsigned char byte;

とすれば、以降は byte という型で unsigned char を扱うことが出来ます。毎回 unsigned と打たなくて済みます。

byte b[8];

これは良し悪しなのですが、

typedef int * int_p;

という別名を作れば、

int_p ip;

という形で整数へのポインタを宣言することが出来ます。型名にポインタであることを示唆する何かが無いと、ポインタ型であることがわからなくなるのが難点です。

さて当然、構造体に対しても別名を作ることが出来ます。

typedef struct {
  int x;
  int y;
} position;

という定義を行えば、

position point;

という形で position と名付けられた構造体を struct を付けずに宣言できます。この場合も型名に構造体であることを示唆する何かがないと、この変数が構造体であることがわからないので、型名を大文字で始めるとか末尾に特定の文字を付けるなどのルールを作っている人も多くいます。

この方法が一般化する以前は define を使うことが多かったです。今は推奨されない方法ですが、

#include <stdio.h>
#define POSITION struct position

POSITION {
  int x;
  int y;
};

void main() {
  POSITION pos = { 1, 1 };
  printf("(%d,%d)\n", pos.x, pos.y);
}

という使い方を良くしていました。構造体名を大文字にしたものがマクロ名にします(これもブラウザ環境では何故か駄目ですね)。これだと統合環境でもうまくメンバ変数などを補ってくれなくなることもあり、typedef を使ったほうが良さそうです。

補足ですが、C++ では構造体変数を宣言するときに struct を省略することができます(定義には必要)。構造体名の名前空間が他の名前空間と重なるので構造体名の名前に工夫が必要となります。

typedef

構造体


ビットフィールドに関しては、やはりブラウザ版では動作しないようですし、今回の課題はお休みとします。

次回は、構造体を使ったリスト構造です。

ヘッダ画像は、今回もいらすとやさんから使わせて頂きましたhttps://www.irasutoya.com/2020/04/blog-post_97.html

#C言語 #構造体 #共用体 #プログラミング講座 #ビットフィールド #typedef  

この記事が気に入ったらサポートをしてみませんか?