構造体

C言語でビット単位で扱うときの構造体のビットフィールドの使い方

組み込みプログラムでは、メモリの節約のため、tureかfalseかのようなフラグを扱うとき、1バイトよりも1ビットで表現することがあります。

そうすることで、1バイトで8つのフラグを持つことが出来るからです。

このとき、1ビット単位を扱うことが可能なビットフィールドについてお話ししようと思います。

ビットフィールドは構造体を用いる

構造体の作り方は、わかりますよね?

もし、わからない場合は、こちらのページを参照してください。

C言語 構造体を作ってみよう

ビットフィールドを扱うには構造体を用いて、このように宣言します。

struct タグ名 {
    メンバ変数1 : 1;
    メンバ変数2 : 1;
        ・
    ・
    ・
};

メンバ変数の後ろに「:ビット数」を記述し、何ビット使用するのかを指定します。1ビットなら、仮にメンバ変数の型がlongであっても最下位ビットのみ使用することになります。

これだけで、ビットフィールドは定義できます。

ここで、注意してもらいたいのは、プログラムではバイト単位で処理されます。したがって、ビットフィールドを用いるときは8ビット単位になるように宣言しましょう。

余るビットがある場合は、ダミーを宣言しておくといいです。

例えば、以下のような感じです。

struct st_ex {
    int m_data1 : 1;
    int m_data2 : 2;
    int m_data3 : 3;
  int m_dummy : 2;
};

6ビットしか使わないの簡単な例です。また、ビット数の設定を「0」にすると、メモリの境界までパディングされます。

ビットフィールドの使い方

ビットフィールドも構造体ですから、通常の構造体と同じように利用できます。

#include <stdio.h>

struct st_ex {
    int m_data1 : 1;
    int m_data2 : 2;
    int m_data3 : 3;
  int m_dummy : 2;
};

void main(void)
{
    struct st_ex ex_bit;

    ex_bit.m_data1 = 1;
    ex_bit.m_data2 = 2;
    ex_bit.m_data3 = 5;
}

ただし、利用するビットは決まっていますので、m_data1には2以上の値を代入することは出来ません。仮に代入しても、最下位ビットの1か0しか反映されません。

同様にm_data2には4以上、m_data3には8以上の値は上位のビットが切り捨てられたものが代入されます。

ビットフィールドを使う場面はあるのか?

こういってしまうと、身も蓋もないのですが、ビットフィールドを使うことは、ほとんどありません。

なぜなら、ビット演算子が用意されているからです。

例えば、ある変数の最下位ビットを1にしたい場合は、

a = a | 0x01;

とすれば、aの最下位ビットを1にできますし、逆に0にしたい場合は、

a = a & ~0x01;

とすれば、実現できます。

処理速度を考えた場合、ビットフィールドを使うより、こちらの方が有利です。

では、どういうときにビットフィールドを使うのか、ということになりますね。

組み込みマイコンでは、デバイスドライバと言われる、いわゆるハードウェアのレジスタ(アドレス)を操作するときに、構造体のビットフィールドでヘッダファイルが定義されています。

したがって、そのときはビットフィールドを使うことになります。

先ほどもお話ししたように、ビットフィールドは構造体なので、アクセスの仕方は何も変わりません。

そういった意味では、プログラマーがビットの処理を扱っているという感覚なしに使えるところが、メリットかもしれません。

まとめ

ビットフィールドの使い方についてお話ししました。構造体が理解できているのなら、この分野はさほど難しい話ではないと思います。

ビットフィールドを使わなくても、ビット演算子を用いれば、プログラムは書けますので、ビットフィールドがあるということだけ知っておけばいいと思います。

実際に自分で作って使用する機会は、ほぼないと思うからです。

しかし、構造体そのものを利用することはあります。その時、「この部分だけ、ビット扱いだな。」というときに、メンバに含めてしまえば、データ処理が一元化できるので、覚えておいて損はないでしょう。

C言語 構造体を作ってみよう

構造体とは、複数の異なる型を一塊として扱えるものです。配列では同じ型しか持つことができません。そこが、構造体のよいところです。

構造体と聞くと、宣言が面倒だとか、メンバ変数への記述が長くなるから嫌だとか、いろいろあると思います。

もちろん、構造体を使わなくても、プログラムは書けます。ただ、関連性のあるデータを一塊にして扱えるほうが便利な時があります。

嫌がっていたら、いつまでたっても使えないままになるので、とりあえず作ってみましょう。

続きを読む

C言語 構造体を使ってリスト構造を作るプログラム

構造体は、関連した複数のデータを扱うのに最適な方法です。そういうデータのかたまりを複数扱うことが多くあると思います。

構造体を配列で持てば、済む話かもしれませんが、それでは、配列を確保した以上のデータを扱う時に困りますよね。プログラムを変更する必要がでてきます。

そこで、構造体の中のメンバに自分自身のポインタを持たせることで、プログラム変更がいらないようにすることができます。そうすることで、配列でデータを扱うよりも、データの追加、削除が容易になります。

これが、構造体をリスト構造化するメリットです。デメリットはポインタの扱いになりますので、ややプログラムが複雑になるところです。

構造体とは何かを知りたい場合は、こちらの記事を読んでください。

———————————————————————————————
C言語のプログラムを3か月で身につけるための第4週でやるべきこと
———————————————————————————————

では、リスト構造化のプログラムを作る手順を書いていきます。

構造体を定義する

当たり前の話ですが、まずは、リスト構造にする構造体のメンバを定義しましょう。

ここでは、生徒の学籍番号、名前、5教科の点数を持つ構造体を作ってリスト構造化することにします。

構造体の宣言です。

struct student {
    int  id;       /* 学籍番号 */
    char name[20]; /* 名前 */
    int  kokugo;   /* 国語の点数 */
    int  suugaku;  /* 数学の点数 */
    int  eigo;     /* 英語の点数 */
    int  rika;     /* 理科の点数 */
    int  syakai;   /* 社会の点数 */
};

これでは、ただ単に構造体を定義しただけですね。これをリスト化するために、この構造体のポインタをメンバに追加します。

struct student {
    int  id;       /* 学籍番号 */
    char name[20]; /* 名前 */
    int  kokugo;   /* 国語の点数 */
    int  suugaku;  /* 数学の点数 */
    int  eigo;     /* 英語の点数 */
    int  rika;     /* 理科の点数 */
    int  syakai;   /* 社会の点数 */
    struct student *next; /* 次の生徒のデータのポインタ */
};

何か気持ち悪いかもしれませんが、これは問題ありません。コンパイルエラーにもなりません。しかし、ポインタではなく、実体を持とうとするとコンパイルエラーになります。

なぜなら、構造体のメンバをコンパイラが解析している段階では、構造体の実体ができあがっていないからです。

リスト構造体の使い方

構造体を作成したら、次はそれを使っていきます。その方法を実際にプログラムを書いて解説していきます。

ここでは、生徒の各教科の情報を扱う構造体を宣言し、データの追加、削除、表示ができるプログラムを書いていますので、まずはざっと読んでみてください。

#include <stdio.h>
#include <string.h>
#include <strlib.h>

struct student {
    int  id;       /* 学籍番号 */
    char name[20]; /* 名前 */
    int  kokugo;   /* 国語の点数 */
    int  suugaku;  /* 数学の点数 */
    int  eigo;     /* 英語の点数 */
    int  rika;     /* 理科の点数 */
    int  syakai;   /* 社会の点数 */
    struct student *next; /* 次の生徒のデータのポインタ */
};

/* リストを追加するための関数 */
char  addList(struct student* list, struct student* sp) {
    struct student* roop_list;  /* 現リストのループ用 */
    struct student* add_list;   /* 追加リスト */

    /* 追加リストがなかったら、何もしない */
    if (sp == NULL) {
        return 0;
    }

    /* 追加リストの領域確保 */
    add_list = (struct student*) malloc(sizeof(struct student)));

    /* 追加リストの領域確保ができなかったら何もしない */
    if (add_list == NULL) {
        return 0;
    } else {
        /* 追加リストのデータをコピー */
        add_list->id = sp->id;
        strcpy(add_list->name, sp->name);
        add_list->kokugo = sp->kokugo;
        add_list->suugaku = sp->suugaku;
        add_list->eigo = sp->eigo;
        add_list->rika = sp->rika;
        add_list->syakai = sp->syakai;
        add_list->next = NULL;
        
        /* 新規の追加リストなら、確保領域のアドレスをコピー */
        if (list == NULL) {
            list = add_list;
        } else {
            /* 追加リストを一番最後に追加 */
            roop_list = list;
            while (roop_list != NULL) {
                if (roop_list->next == NULL) {
                    roop_list->next = add_list;
                    break;
                }
                roop_list = roop_list->next;
            }
        }
    }
    
    return 1;
}

/* 指定リストを削除する関数 */
void deleteList(struct student* list, int pos)
{
    struct student* p;
    int i;

    i = 0;
    while (list != NULL) {     /* 次ポインタがNULLまで処理 */
        if (pos == i) {        /* 指定リストなら削除 */
            if (pos == 0) {    /* 先頭のリストを削除する場合 */
                p = list->next;
                free(list);
                list = p;
            } else {           /* 先頭以外のリストを削除する場合 */
                p->next = list->next;
                free(list);
            }
            break;
        }
        p = list;
        list = list->next;
        i++;
    }
}


/* リストをすべて削除する関数 */
void deleteListAll(struct student* list)
{
    struct student* p;

    while (list != NULL) {     /* 次ポインタがNULLまで処理 */
        p = list->next;
        free(list);
        list = p;
    }
}



void main(void) {
        struct student* data;     /* 生徒のリスト情報 */
        struct student* tmp;      /* 生徒のリスト情報退避用 */
        struct student  add_data; /* 追加生徒の情報 */
        int             del_pos;  /* 削除する生徒のリスト番号 */
        int             select;   /* 操作選択 */

        data = NULL;

        while (1) {
            printf("操作を選んでください。\n");
            printf("0:表示\n");
            printf("1:追加\n");
            printf("2:削除\n");
            printf("3:終了\n");
            scanf("%d", select);

            swhitch (select) {
            case 0:
                tmp = data;
                while (data != NULL) {
                    printf("学籍番号 = %d\n", data->id);
                    printf("名前 = %s\n", data->name);
                    printf("国語の点数 = %d\n", data->kokugo);
                    printf("数学の点数 = %d\n", data->suugaku);
                    printf("英語の点数 = %d\n", data->eigo);
                    printf("理科の点数 = %d\n", data->rika);
                    printf("社会の点数 = %d\n\n", data->syakai);
                    data = data->next;
                }
                data = tmp;
                break;

            case 1:
                printf("学籍番号 = ");
                scanf("%d", &(add_data.id));
                printf("名前 = ");
                scanf("%s", add_data.name);
                printf("国語の点数 = ");
                scanf("%d", &(add_data.kokugo));
                printf("数学の点数 = ");
                scanf("%d", &(add_data.suugaku));
                printf("英語の点数 = ");
                scanf("%d", &(add_data.eigo));
                printf("理科の点数 = ");
                scanf("%d", &(add_data.rika));
                printf("社会の点数 = ");
                scanf("%d", &(add_data.syakai));

                /* リストに追加 */
                addList(data, &add_data);
                break;

            case 2:
                printf("削除するリスト番号 = ");
                scanf("%d", &del_pos);

                /* 指定されたリストを削除 */
                deleteList(data, del_pos);
                break;

            default:
                /* 使用メモリの開放 */
                deleteListAll(data);
                exit;
            }
        }
    }        
}

確保された領域にデータを保持し、ポインタで関連付けをしていくイメージです。次のデータのポインタをメンバに持っておくことで、次のリストにアクセスすることができます。

注意して欲しいのは、リストの先へ進むことはできますが、リストの前に戻ることはできないので注意が必要です。

したがって、先頭のリストのポインタは常に覚えておく必要があります。

メイン関数で、何の操作を実行するかを選択させるようにしました。「終了」を選択しない限り、プログラムを終了しないようにwhile(1)で無限ループにしています。

ここで注意点ですが、この選択時に、誤って数値以外を入力してしまうと、プログラムが暴走してしまいます。他の”%d”で読み込んでいるscanf文全てで同じことが言えます。

これを暴走しないようにしようと思うと、一旦文字で受け取り、それを数値に変換するなどして対応することが考えられます。

0の表示を選択すると、今もっているリストデータを全て表示するようにしているだけです。何ももっていなければ、何も表示されず、操作選択に戻ります。

1の追加を選択すると、各種データを入力させる項目が出力され、入力が全て終わると、リストの一番最後に追加されるようにしています。一番最後の追加にしたので、addList関数は少々複雑になっています。

2の削除を選択すると、削除したいリスト番号を入力させるようにし、それが入力されると、指定されたリストを削除し、その前のリストにあるポインタを削除したリストの次のデータのポインタにするようにしています。

3の終了を選択すると、使用しているメモリを全て開放して、プログラムの実行を終了します。

始めにデータを追加しないと、削除も表示も意味がありません。プログラム実行終了時には、入力したデータは全て失われてしまいます。

ここから派生して、リスト構造をうまく利用するプログラムへ発展させましょう。例えば、ソーティングや、逆順にもリストをたどれるようにする、データをファイル保存するなど、色々やれることはあると思います。

リスト構造で逆順にもたどれるようにするには、逆順用のポインタをメンバに用意すればいい

先ほど、少し触れましたが、先ほどのリストは先頭から後ろへ順にたどっていくしかありませんでした。これでは少々不便ですよね。どうしたらいいかというと、リストの逆順にもたどれるようにしたらいいのです。

そうすると、いちいちリストの先頭のアドレスを覚えておく必要はなくなります。今のリスト情報から、たどれるからです。

どうやってやるかは、もうおわかりですね。もう1つ、逆順用のポインタをメンバに用意すればいいのです。

struct student {
    int  id;       /* 学籍番号 */
    char name[20]; /* 名前 */
    int  kokugo;   /* 国語の点数 */
    int  suugaku;  /* 数学の点数 */
    int  eigo;     /* 英語の点数 */
    int  rika;     /* 理科の点数 */
    int  syakai;   /* 社会の点数 */
    struct student *prev; /* 前の生徒のデータのポインタ */
    struct student *next; /* 次の生徒のデータのポインタ */
};

前のデータのポインタprevを追加しました。これに現在のリストの前にあるリストのポインタを覚えておけばいいのです。

先ほどのプログラム例で挙げたリストを追加する関数にポインタprevを設定する処理を追加します。

/* リストを追加するための関数 */
char  addList(struct student* list, struct student* sp) {
    struct student* roop_list;  /* 現リストのループ用 */
    struct student* add_list;   /* 追加リスト */

    /* 追加リストがなかったら、何もしない */
    if (sp == NULL) {
        return 0;
    }

    /* 追加リストの領域確保 */
    add_list = (struct student*) malloc(sizeof(struct student)));

    /* 追加リストの領域確保ができなかったら何もしない */
    if (add_list == NULL) {
        return 0;
    } else {
        /* 追加リストのデータをコピー */
        add_list->id = sp->id;
        strcpy(add_list->name, sp->name);
        add_list->kokugo = sp->kokugo;
        add_list->suugaku = sp->suugaku;
        add_list->eigo = sp->eigo;
        add_list->rika = sp->rika;
        add_list->syakai = sp->syakai;
        add_list->prev = NULL;          /* この部分を追加 */
        add_list->next = NULL;
        
        /* 新規の追加リストなら、確保領域のアドレスをコピー */
        if (list == NULL) {
            list = add_list;
        } else {
            /* 追加リストを一番最後に追加 */
            roop_list = list;
            while (roop_list != NULL) {
                if (roop_list->next == NULL) {
                    roop_list->next = add_list;
                    add_list->prev = roop_list;       /* この部分を追加 */
                    break;
                }
                roop_list = roop_list->next;
            }
        }
    }
    
    return 1;
}

これだけで済みます。これで、list->prevでlistの前のデータ、list->nextでlistの次のデータへアクセスできるようになりました。

これで好きなようにリスト構造になっているデータへアクセスできるようになり、データの利用の幅が広がりましたね。

まとめ

リスト構造について、解説しました。構造体とポインタを駆使していく形なので、かなりややこしかったと思います。

しかし、配列でリストの間にデータを追加したり、データを削除したりするのは、手間がかかるのと、処理時間もかかってしまいます。

リスト構造にしておくと、ポインタの付け替えで済むので、圧倒的に有利なんですよ。

組み込みマイコンでは、あまりやることはないでしょうが、知っておいて損はないと思います。

私が、C言語の勉強の練習に使った中でも、一番ややこしい話です。

ですが、これが理解できるようになって、構造体、ポインタ、配列、関数という類の扱いが格段にできるようになりました。

だから、あなたもややこしいからと逃げずにやっていってほしいと思います。