本文へスキップ
BecomeCoder

C言語コース · 第15章 モダンCとデバッグ · レッスン54

unionとアライメント・assert

ブラウザで完結

導入

第7章で union は「複数のメンバが同じメモリ領域を共有する型」だと学びました。この性質を活かすと、「今この変数にはどの型の値が入っているか」を別の変数(タグ)で管理するタグ付きunionが作れます。異なる型の値を、必要なだけのメモリで1つの変数として扱える、実務でもよく使われるテクニックです。

説明

タグ付きunion ― 「今どの型か」をenumで管理する

#include <stdio.h>

enum ValueType { TYPE_INT, TYPE_DOUBLE };

typedef struct {
    enum ValueType type;   /* 今どちらの型が入っているかを示すタグ */
    union {
        int i;
        double d;
    } value;
} Variant;

void printVariant(Variant v) {
    if (v.type == TYPE_INT) {
        printf("int:%d\n", v.value.i);
    } else {
        printf("double:%.1f\n", v.value.d);
    }
}

int main(void) {
    Variant a;
    a.type = TYPE_INT;
    a.value.i = 42;

    Variant b;
    b.type = TYPE_DOUBLE;
    b.value.d = 3.5;

    printVariant(a);
    printVariant(b);
    return 0;
}

Variantintdouble のどちらか一方だけを、必要なメモリ(大きい方のサイズ)で保持できます。type フィールドが「今 value の中身をどう読めばよいか」の目印になっており、これを見てから正しいメンバにアクセスするのがタグ付きunionの使い方です(type を見ずに間違ったメンバを読むと、意味のないビット列を別の型として解釈することになります)。

アライメント ― メモリ上の並び方

構造体のメンバは、メモリ上で単純に隙間なく並ぶとは限りません。多くの環境では、int は4バイト境界、double は8バイト境界のように、型ごとに「都合のよい開始位置(アライメント)」があり、コンパイラはそこに合わせるためにパディング(隙間)を挟むことがあります。

#include <stdio.h>

struct Example {
    char c;
    int i;
};

int main(void) {
    printf("sizeof(char)=%zu sizeof(int)=%zu\n", sizeof(char), sizeof(int));
    printf("sizeof(struct Example)=%zu\n", sizeof(struct Example));
    printf("_Alignof(int)=%zu\n", (size_t)_Alignof(int));
    return 0;
}

struct Examplechar(1バイト)と int(4バイト)を持つので、単純に足すと5バイトに思えますが、実際には int のアライメントに合わせてパディングが入り、それより大きい値になることがあります(値は環境によって変わります)。C11の _Alignof 演算子を使うと、ある型が要求するアライメントの値を調べられます。

assert ― 前提条件を実行時にチェックする

<assert.h>assert(条件) は、「ここではこの条件が必ず成り立っているはずだ」という前提をコードに書いておく道具です。条件が真であれば何も起きませんが、もし偽になったら、プログラムはその場でエラーメッセージを出して異常終了します。

#include <stdio.h>
#include <assert.h>

int divide(int a, int b) {
    assert(b != 0);        /* b が0でないことを前提とする */
    return a / b;
}

int main(void) {
    printf("%d\n", divide(10, 2));
    return 0;
}

assertバグを未然に発見するための道具であって、if のように失敗を優雅に処理するものではありません(失敗したら即座に止まります)。開発中は assert をたくさん入れて前提条件の破れをすぐ見つけられるようにし、NDEBUG というマクロを定義してコンパイルすると、すべての assert が無効化されて本番のコードから取り除かれる、という使い分けもよくされます。

やってみよう

Variant の値を TYPE_INTTYPE_DOUBLE それぞれで色々な値に変えて printVariant の結果を確かめましょう。divide(10, 0) のように b0 を渡すとどうなるか(assert が失敗して異常終了します)も、余裕があれば試してみてください。

演習

上の VariantprintVariant をそのまま使い、TYPE_INT で値 100 を持つ変数と、TYPE_DOUBLE で値 2.5 を持つ変数を1つずつ作り、この順に printVariant で表示してください(正しく書けると int:100double:2.5 の2行になります)。

ヒント1を見る

Variant 変数を作ってから 変数名.type = TYPE_INT;変数名.value.i = 100; の順に値を入れます。

ヒント2を見る

もう1つの変数は type = TYPE_DOUBLE;value.d = 2.5; とし、printVariant を2回呼びます。

実際に動かしてみよう

下のエディタにCを書いて「コンパイル & 実行」を押すと、ブラウザ内で本物のCコンパイラ(clang + dlmalloc)がその場でコンパイルして実行し、出力が表示されます。構造体・共用体・malloc/free・関数ポインタ・ファイル入出力などがそのまま動きます。本文の例を書き換えて試しながら進めましょう(初回だけコンパイラの読み込みに時間がかかります。以降はキャッシュされます。この回は標準入力(scanf)は使わず、値はコードに直接書きます)。

C/C++ — ブラウザ内で本物のコンパイラで実行

ブラウザ内で本物のC/C++コンパイラ(clang + libc++ / WebAssembly)を動かします。構造体・malloc/free・std::string・vector・クラス・テンプレート・STL などがそのまま実行できます(初回のみコンパイラの読み込みに時間がかかります。以降はブラウザにキャッシュされます)。
スクロールして表示された時点でも自動で読み込まれます。