【C言語】共用体(union)の使い方や特徴・メリットを分かりやすく解説

共用体についての解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語の共用体(union)について説明していきます。

共用体は、使い方に関しては構造体と似ていますが、全く異なる特徴を持つデータの構造になります。これらの違いを理解することが、共用体の特徴の理解につながります。

そのため、共用体を理解するためには構造体の知識が必須となります。他にもメモリやアドレスなどの知識も必要で、C言語の基本を一通り理解していないと共用体の特徴を理解することは難しいと思います。なので、実は共用体の使い方を理解していても、特徴やメリットを理解していない人は多いのではないかと思います。

このページでは、共用体の特徴が理解しやすいように、分かりやすく共用体についてまとめていますので、是非このページを読んで共用体の理解に挑戦してみてください!

また、特に構造体に関しては下記ページで解説していますので、構造体をご存知ない方は事前に下記ページを読んでおくことをオススメします。

構造体解説ページのアイキャッチ 【C言語】構造体について初心者向けに分かりやすく解説

共用体とは

では、さっそく共用体について説明していきます。

共用体は「同じ1つのデータを複数のメンバーで共用して管理するデータの構造」のことを言います。この一文は共用体の特徴全てを表していると言っても過言ではないです。ただ、この一文だけだと特徴がイメージしにくいと思いますし、この特徴のメリットを理解できない方も多いと思いますので、この共用体の特徴に関しては 共用体と構造体の違い 以降で詳細を説明していきたいと思います。

C言語では、共用体の型を自身で定義し、その型の変数を宣言して利用することが可能です。例えば、intfloat など、プログラミング言語の仕様として予め用意されている型と同様に変数宣言して利用することが可能です。ただし、前述のとおり、共用体の型に関しては、自身で定義してから使用する必要があります。

共用体の使い方

続いて共用体の使い方について解説していきます。

スポンサーリンク

型の定義

共用体はデータの構造であり、その構造を持つ変数を利用するためには型を定義しておく必要があります。

共用体の型は下記のような形式で定義することができます。union が「この型を共用体として定義する」ことを指示する識別子となります。そして、下記のように定義を行うことで、以降、union タグ名 が新たに定義した共用体の型の「型名」として扱われるようになります。

共用体の型の定義
union タグ名 {
    型 メンバー名1;
    型 メンバー名2;
    // 略
};

そして、この型のデータは、 {} の内側で指定した型のメンバーを持つことになります。メンバーとは、この定義した型が持つ要素のことです。上記では2つのメンバーを持たせていますが、もっと多くのメンバーを持たせることも可能です。

また、 には既に定義済みの型であれば基本的に何でも指定することが可能です。intfloat はもちろん、事前に定義した構造体の型を指定しても良いです。

例えば、下記は union _Number という型を共用体として定義する例となります。

共用体の型の定義例
union _Number {
    short short_num;
    int int_num;
    float float_num;
    double double_num;
};

変数宣言

型として定義してやれば、後は通常の型の時と同様の使い方で共用体のデータを利用することが出来るようになります。例えば int 型の変数は変数宣言を行ってから、その宣言した変数をソースコード上で利用することになります。共用体の型のデータも同様の手順で利用していくことになります。

より具体的には、下記のように union タグ名 変数名; を記述することで共用体の型の変数を宣言することが可能です。そして、この宣言した変数はプログラム内で利用することが可能となります。

変数宣言
union タグ名 変数名;

例えば、前述で示した例では union _Number を定義していますので、下記のような記述を行うことで変数 number を利用することが出来るようになります。

変数宣言の例
union _Number number;

もちろん、定義した型の配列を宣言することも可能です。

配列の変数宣言の例
union _Number numbers[100];

変数の利用

変数宣言してやれば、後はその変数を利用して実現したい処理をソースコードに記述していけば良いだけになります。ただし、基本的には共用体の型の変数は、変数そのものではなく、その変数の持つメンバーを使って値の代入や値の取得を行うことになります。構造体と同様に、メンバーは 変数名 の後ろに .メンバー名 と記述することで利用することが出来ます。

変数の利用
union タグ名 変数名;

変数名.メンバー名 = 代入したい値;
printf("%d\n", 変数名.メンバー名);

例えば下記は、変数 number のメンバー int_num100 を代入し、さらに int_num の値を出力する例となります。

変数の利用例
union _Number number;

number.int_num = 100;
printf("%d\n", number.int_num);

スポンサーリンク

アロー演算子も利用可能

また、これも構造体と同様に、共用体の型のデータを指すポインタ変数からメンバーを利用する際には “アロー演算子(->)” を利用することになります。

例えば下記は、変数 number を指すポインタ p_number から number のメンバー int_num を利用する例となります。

アロー演算子の利用例
union _Number number;
union _Number *p_number = &number;

p_number->int_num = 100;
printf("%d\n", number.int_num);

アロー演算子については下記ページで詳しく説明していますので、アロー演算子について知りたい方は是非下記ページを読んでみていただければと思います。

C言語のアロー演算子(->)を分かりやすく、そして深く解説

typedef でシンプルな型名を付けることも可能

また、構造体等の他の型と同様に、typedef を利用して共用体の型を “新たな名前の型” として定義することも可能です。

共用体の型に対して typedef によって新たな型を定義する際には、下記のように typedef の直後に union タグ名 を、その後ろ側に 新たな型名 を指定してやれば良いです。typedef によって共用体の型に新たに型名を付けることで、わざわざ共用体の型の変数を宣言する際に型名に union を指定する必要がなくなります。

typedefによる新たな型の定義
typedef union タグ名 新たな型名;

例えば、下記のように typedef してやれば、新たな型 NUMBER が定義されることになり、この NUMBERunion _Number と全く同じ構造のデータの型となります。

typedefによる新たな型の定義例
union _Number {
    short short_num;
    int int_num;
    float float_num;
    double double_num;
};

typedef union _Number NUMBER;

さらに、下記のように共用体の型を定義する際に typedef も同時に行うことで、共用体の定義と同時に新たな型を定義することが可能となります。この場合は タグ名 も省略することが可能です。

型の定義と一緒にtypedef
typedef union {
    short short_num;
    int int_num;
    float float_num;
    double double_num;
} NUMBER;

共用体と構造体の違い

さて、ここまで共用体の使い方について説明してきました。ここまでの説明で理解していただけたと思いますが、共用体の型の変数の使い方は構造体の型の変数の使い方と同様になります。

使い方としては同じなのですが、この2つにはデータの構造に決定的な違いがあります。そして、その違いが共用体の特徴となります。ということで、次は共用体と構造体の違いについて説明していきたいと思います。

スポンサーリンク

各メンバーが同じメモリを共用する

共用体と構造体の決定的な違いは、各メンバーのメモリ上の配置位置となります。

まず、構造体では各メンバーが必ずメモリ上の異なるアドレスに配置されることになります。そして、各メンバーのデータが重なり合うことはなく、それぞれのメンバーが独立して存在することになります。つまり、各メンバーの変更は、他のメンバーの変更に影響を及ぼすことはありません。

構造体の各メンバーが別々のアドレスに配置される様子

それに対し、共用体では、各メンバーがメモリ上の同じアドレスに配置されることになります。より正確に言えば、各メンバーの先頭アドレスが同じ位置となります。そのため、各メンバーが同じメモリ領域を共用することになります。そのため、各メンバーの変更は他のメンバーの変更に影響を及ぼすことになります。

共用体の各メンバーが同じアドレスに配置される様子

例えば、下記のようなソースコードについて考えてみましょう。ここでは構造体の型 S_DATA と共用体の型 U_DATA の2つを定義し、それぞれの変数を s_datau_data として宣言しています。S_DATAU_DATA のメンバーは同じとしており、各メンバーの先頭アドレスを printf 関数で出力しています。

各メンバーのアドレスの比較
#include <stdio.h> // printf

typedef struct {
    int num1;
    int num2;
    int num3;
} S_DATA;

typedef union {
    int num1;
    int num2;
    int num3;
} U_DATA;

int main(void) {
    S_DATA s_data;
    U_DATA u_data;

    printf("s_data.num1 : %p\n", &s_data.num1);
    printf("s_data.num2 : %p\n", &s_data.num2);
    printf("s_data.num3 : %p\n", &s_data.num3);

    printf("u_data.num1 : %p\n", &u_data.num1);
    printf("u_data.num2 : %p\n", &u_data.num2);
    printf("u_data.num3 : %p\n", &u_data.num3);
}

ここまでの説明を読んでいただいた方であれば予想はつくと思いますが、上記をコンパイルして実行すると次のような出力結果が得られることになります。下記は私の PC で実行した結果であり、皆さんが実行した場合も、出力されるアドレスは異なるものの、s_data の各メンバーのアドレスがそれぞれ異なることと、u_data の各メンバーのアドレスがそれぞれ同じであることが確認できると思います。

s_data.num1 : 0x7fffffffde4c
s_data.num2 : 0x7fffffffde50
s_data.num3 : 0x7fffffffde54
u_data.num1 : 0x7fffffffde48
u_data.num2 : 0x7fffffffde48
u_data.num3 : 0x7fffffffde48

プログラムにおいては、各変数はメモリ上に自動的に配置されることになります。これは構造体や共用体のメンバーも同様です。構造体の場合は、各メンバーが定義時に指定した順序で上から順に自動的にメモリ上に配置されることになります(1番上に持たせているメンバーは変数の先頭アドレスと一致します)。

これらは独立して別々に扱われるため、これらのメンバーが同じアドレスに配置されることも、重なり合って配置されることもありません。それに対し、共用体の各メンバーは同じアドレスに配置されることになります。そのため、各メンバーは重なり合って配置されることになります。

共用体の変数で管理できるデータは基本的に1種類のみ

共用体の各メンバーは同じアドレスに重なり合って配置されるため、特定のメンバーに対して数値を代入したりデータをコピーすると他のメンバーのデータも変化することになります。

例えば下記のソースコードに注目してみましょう。下記では、構造体と共用体の各メンバーに対して整数の代入を行い、最後に printf で各メンバーに格納されている整数を出力しています。

各メンバーへの代入結果の比較
#include <stdio.h> // printf

typedef struct {
    int num1;
    int num2;
    int num3;
} S_DATA;

typedef union {
    int num1;
    int num2;
    int num3;
} U_DATA;

int main(void) {
    S_DATA s_data;
    U_DATA u_data;

    s_data.num1 = 1;
    s_data.num2 = 2;
    s_data.num3 = 3;

    u_data.num1 = 1;
    u_data.num2 = 2;
    u_data.num3 = 3;

    printf("s_data.num1 : %d\n", s_data.num1);
    printf("s_data.num2 : %d\n", s_data.num2);
    printf("s_data.num3 : %d\n", s_data.num3);

    printf("u_data.num1 : %d\n", u_data.num1);
    printf("u_data.num2 : %d\n", u_data.num2);
    printf("u_data.num3 : %d\n", u_data.num3);
}

上記ソースコードをコンパイルして実行した場合、printf で出力される結果は下記のようなものになるはずです。

s_data.num1 : 1
s_data.num2 : 2
s_data.num3 : 3
u_data.num1 : 3
u_data.num2 : 3
u_data.num3 : 3

この結果からも分かるように、構造体の各メンバーは独立して別々に存在するため、各メンバーに代入した値は他のメンバーへの代入等に影響されず保持されることになります。そのため、構造体の場合はメンバーの数だけ別々のデータを管理することが出来ることになります。

構造体の各メンバーが独立している点を説明する図

それに対し、共用体の場合は同じアドレスに各メンバーが存在するため、特定のメンバーへの数値の代入やデータのコピーが行われると、他のメンバーに代入した数値等が上書きされてしまうことになります。その結果、上記のようなソースコードの場合はすべてのメンバーの出力結果が同じ値となっています。

共用体の各メンバーが同じデータであることを示す図

共用体の場合、各メンバーは同じアドレスに配置されているため、これらは同じデータということになります。なので、上記のように特定のメンバーへの数値の代入やデータのコピーが行われると、他のメンバーに代入した数値等が上書きされてしまうことになります。

つまり、共用体では、メンバーがいくつ存在したとしても、基本的に1つの変数で管理できるデータは1つのみであることになります。そして、その1つのデータを複数のメンバーからアクセス(変更や取得)することができるようになっています。

構造体と共用体との管理できるデータの個数の違いを示す図

では、1つのデータに複数の異なるメンバーからアクセスできるメリットは何でしょうか?ここが共用体を理解する1つのポイントになると思います。このあたりについては後述で解説していきます。

共用体の型のサイズはメンバーの最大サイズとなる

また、構造体の型のサイズは「全てのメンバーのサイズの和(+データの整列用の調整サイズ)」となります。したがって、メンバーの数が増えれば増えるほど構造体の型のサイズは大きくなることになります。構造体の場合、メンバーの数だけのデータを管理することになりますので、管理するデータが増える分、型のサイズも大きくなることになります。

それに対し、共用体の場合は管理するデータは1つのみであり、共用体の型のサイズは「一番サイズの大きなメンバーのサイズ(+データの整列用の調整サイズ)」となります。

例えば下記のようなソースコードについて考えてみましょう。下記では構造体の型と共用体の型のサイズを printf で出力しています。

型のサイズの比較
#include <stdio.h> // printf

typedef struct {
    char num1;
    int num2;
    double num3;
} S_DATA;

typedef union {
    char num1;
    int num2;
    double num3;
} U_DATA;

int main(void) {
    printf("size of S_DATA : %ld\n", sizeof(S_DATA));
    printf("size of U_DATA : %ld\n", sizeof(U_DATA));
}

私の PC で実行した結果は下記となりました。私の PC では char 型のサイズが 1 バイト、int 型のサイズが 4 バイト、double 型のサイズが 8 バイト、さらに構造体や共用体の各メンバが 4 バイト単位で整列されるようになっているので下記のような結果になりましたが、これらが異なる環境で実行した結果は下記とは異なる結果になると思います。

size of S_DATA : 16
size of U_DATA : 8

が、いずれにせよ構造体の型のサイズは「全てのメンバーのサイズの和(+データの整列用の調整サイズ)」となり、共用体の型のサイズは「一番サイズの大きなメンバーのサイズ(+データの整列用の調整サイズ)」として出力されるはずです。

ここで、少しだけ、データの整列用の調整サイズについて説明しておくと、構造体や共用体の特定の型のメンバーは、先頭アドレスが特定のバイトの倍数となるように調整されるようになっています。また、これらのサイズも特定のバイトの倍数となるようになっています。

各メンバーが整列するようにデータが勝手に追加される様子

そのため、構造体のサイズは各メンバーのサイズを単に足し合わせたサイズと一致しない場合がありますし、共用体のサイズは一番大きなメンバーのサイズと一致しない場合があります。

ちょっとここがややこしいので、まずは構造体のサイズは「各メンバーのサイズの和」に基づいて決まり、共用体のサイズは「一番大きなメンバーのサイズ」に基づいて決まる、くらいで覚えておいても良いと思います。要は、構造体の各メンバーは別々の位置に重なり合わないように配置されるため、最低でも各メンバーのサイズの和が必要になります。それに対し、共用体の各メンバーは同じ位置に重なり合うように配置されるため、一番大きなメンバーのサイズさえあれば十分ということになります。

スポンサーリンク

共用体と構造体の違いのまとめ

ここで、ここまで説明してきたことを一旦まとめておきたいと思います。

  • 各メンバーが配置される位置
    • 共用体:各メンバーが重なって同じアドレスに配置される
    • 構造体:各メンバーが重ならないように別々のアドレスに配置される
  • 管理できるデータの数
    • 共用体:1つのみ
    • 構造体:メンバーの数だけ
  • サイズ
    • 共用体:1番大きなメンバーのサイズ+α
    • 構造体:各メンバーのサイズの和+α

こうやって見ると、同じような記述で定義ができ、使い方も同様ではあるものの、共用体と構造体は全く異なるものであることが理解していただけると思います。

構造体の用途は明確で、1つの変数で複数のデータをまとめて関連付けて管理したい時に使用します。例えば「人」には、その人を特徴づけるデータがたくさんあります。例えば名前・年齢・身長などなど。これらを関連付けてまとめて管理することで、複数のデータを管理しやすくし、さらにソースコードも読みやすくなります。

それに対し、共用体はどんな時に利用するのでしょうか?

わざわざ複数のメンバーを持たせているのに、1つのデータしか管理することができないなんて意味不明ですよね…。

なんですが、実は共用体の用途は存在し、共用体も利用するメリットもあります。次は、共用体を利用するメリットについて説明していきたいと思います。

共用体のメリット

では、共用体を利用するメリットについて解説していきます。ここを理解すれば適切に共用体や他の型とを使い分けることができるようになると思います!

異なる型へのデータ変換が容易

まず、共用体のメリットの1つ目として、データを異なる型に簡単に変換できるという点が挙げられます。前述のとおり、共用体の型の1つの変数で管理できるデータは1つのみです。そして、その1つのデータを複数の異なるメンバーから取得することが出来ます。そして、その取得できるデータは、取得を試みたメンバーの型に変換されたデータとなります。

そのため、1つのデータを様々な型や構造のデータに変換することが容易となります。

利用する共用体のメンバーの型に応じたデータの変換が行われる様子

具体例を見てみましょう!

下記は、共用体の型である U_DATA を定義し、U_DATA の変数である u_data のデータを各メンバーから取得する例になります。

複数のメンバーからのデータの出力
#include <stdio.h> // printf

typedef union {
    int int_num;
    short short_num;
    unsigned char bytes[4];
    float float_num;
} U_DATA;

int main(void) {
    U_DATA u_data;

    u_data.int_num = 1094713344;

    // int型で出力
    printf("int_num : %d\n", u_data.int_num);

    // short形で出力
    printf("short_num : %d\n", u_data.short_num);

    // 1バイトずつ出力
    printf("bytes:0x");
    for (int i = 0; i < 4; i++) {
        printf("%02x", u_data.bytes[i]);
    }
    printf("\n");

    // float型で出力
    printf("float_num : %f\n", u_data.float_num);
}

前述のとおり、u_data で管理できるデータは1つのみです。そして、u_data には、u_data.int_num = 1094713344 によって int 型で 1094713344 という整数が格納されていることになります。

上記のソースコードでは、このu_data を各メンバーから取得して printf で出力を行っています。そして、この出力結果は下記のようになります。もしかしたら環境によって異なる出力結果になっているかもしれませんが、多くの方が下記のような出力結果を得ることが出来ているのではないかと思います。

int_num : 1094713344
short_num : 0
bytes:0x00004041
float_num : 12.000000

int_num メンバーの出力結果が上記のようになるのは納得がいくと思います。しかし、他のメンバーの出力結果はどうして上記のようなものになるのでしょうか?

この理由が、共用体のメリットにつながります。ということで、上記のような出力結果となる理由について説明しておきます。

コンピューター内部で管理されるデータ

まず前提として、コンピューター内部で扱われるデータはすべて 1 or 0 となります。この2種類のデータを組み合わせたり、見方や扱い方を変えることで無限の種類のデータをコンピューターで表現することが出来るようになります。そして、この 1 or 0 の数字からのみ表現される数を2進数と呼びます。

例えば、上記では int_num メンバーに 1094713344 という値を代入していますが、この値を2進数で表現すると下記になります。

01000001010000000000000000000000

コンピューターで実際に扱われるデータは 1 or 0 になるのですが、これだと人間には分かりにくいので、バイト単位でデータを表現することが多いです。コンピューターで実際に扱われるデータ(1 or 0)のサイズを表す単位はビットであり、このビットを 8 個分まとめたデータのサイズの単位がバイトとなります。そして、1バイトのデータは16進数で表現することが多いです。これは、16進数で表すことで、1バイトが必ず2桁以下の数値となってバイト単位の区切りが人間にとって分かりやすくなるからです。

例えば、1094713344 という整数を16進数で表すと下記のようになります(0x は、その数値が16進数表記であることを示します)。 

0x41400000

つまり、1094713344int_num に代入した場合、16進数で考えると 0x41400000 という値が代入されることになります。そして、この2桁ずつの値が1バイトのサイズとなりますので、0x410x400x000x00 の合計4バイトが udata.int_num のアドレスに格納されることになります(代入とは、変数の先頭アドレスにバイトデータを格納する処理であると考えられます)。

で、この4バイト分のデータを先頭から順に int_num の先頭アドレスから並べて格納した場合、udata.int_num の先頭アドレスから4バイト分のデータは、1094713344 の代入によって下記のように変化することになりそうですね。

0x41 0x40 0x00 0x00

なんですが、このデータの並びはデータの先頭から順ではなく、データの末尾から順に特定のアドレスに格納されることが多いです。このあたりは CPU によって異なり、具体的にはエンディアン(リトルエンディアン or ビッグエンディアン)によって並び順は変わります。私の PC の場合は CPU がリトルエンディアンなので、1094713344 を代入した際には 0x41400000 の末尾側の2桁から順に udata.int_num のアドレスにデータが格納されることになります。

udata.int_numが配置されているアドレスに格納されるデータ

つまり、int_num メンバーへの 1094713344 代入後、udata.int_num の先頭アドレスから4バイトは下の図のようになることになります。つまり、int_num メンバーの先頭アドレスから順に、0x00 0x00 0x40 0x41 が格納されていることになります。

udata.int_numの中身

そして、共用体の場合、各メンバーは同じアドレスに配置されることになります。したがって、short_num の先頭アドレスから4バイトも、bytes の先頭アドレスから4バイトも、float_num の先頭から4バイトも同様に 0x00 0x00 0x40 0x41 が格納されていることになります(下図では各メンバーのデータを別々に記載していますが、これらは同じアドレスのデータとなります)。

各メンバーの先頭から4バイトを示した図

ここで重要なのは、ここまでも説明してきたように、メンバーがいくつあろうが共用体の型の変数で管理できるデータは1つのみで、各メンバーの先頭アドレスからは同じデータが格納されているという点になります。

では、この状態で各メンバーの値を出力すると、どういったデータが出力されることになるのか、という点について説明していきます。

int 型のメンバーの出力

まずは int_num の値の出力について考えていきましょう。int_num には直接 1094713344 が代入されているので 1094713344 が出力されることは当然ではあるのですが、ここでは int_num の先頭から4バイトのデータに注目して出力される値を考えていきたいと思います。

前述の通り、int_num メンバーの先頭アドレスから4バイトには 0x00 0x00 0x40 0x41 が格納されています。さらに、int 型のサイズが4バイトなので、ここで int_num メンバーの先頭アドレスから4バイトを取得し、さらにエンディアンの関係で逆順にしていた並びを元に戻して4バイト分のデータを結合すると、16進数で 0x41400000 が得られます。そして、これを int 型の10進数の整数に変換すると 1094713344 となり、これが int_num メンバーの値として出力されることになります。

int_numを出力する際のデータの変化を示す図

つまり、メンバーの先頭アドレスから格納されている「メンバーの型のサイズ分のデータ」を取得し、データの並びを元に戻し、さらに「メンバーの型に応じたデータ」に変換することで、出力する値が決まることになります。これは、メンバーが int 型の場合だけでなく、メンバーが他の型の場合でも同様になります。

そして、int_num メンバーを出力すると 0x00 0x00 0x40 0x41 の4バイトを int 型として扱った場合の値が出力されることになり、その出力結果は 1094713344 となります。

int_num : 1094713344

short 型のメンバーの出力

では、次は short_num の値の出力について考えていきましょう。

short_num の型は short であり、私の PC では short は2バイトで扱われます。したがって、short_num の先頭アドレスから2バイト分のみを取得し、データの並びを元に戻し、さらに short 型の10進数の整数に変換した結果が short_num を出力した時に得られる値となります。今回の場合、short_num の先頭アドレスから2バイト分のデータは 0x0000 となりますので、並びを元に戻しても 0x0000 であり、これは short 型の10進数で 0 となります。

short_numを出力する際のデータの変化を示す図

したがって、short_num メンバーを出力した場合、0x00 0x00 0x40 0x41 の2バイトを short 型として扱った場合の値が出力されることになり、その出力結果は 0 となります。

short_num : 0

unsigned char 型の配列のメンバーの出力

次は bytes の場合についても考えてみましょう!

bytes の場合はループの中で bytes[0] から bytes[3] の値の出力を行なっている&16進数で出力しているので少し複雑に思えますが、考え方は今までと同様です。

まず、最初のループでは bytes[0] の出力が行われます。bytesunsigned char 型の配列であり、unsigned char 型のサイズは1バイトです。そして、bytes[0] は、bytes の先頭アドレスの1バイトのデータになります。

したがって、bytes[0] を出力する際には、bytes の先頭アドレスの1バイトのデータが取得され、データの並びが元に戻され、さらにそのデータが unsigned char 型に変換されることになります(1バイトなのでデータの並びは変化しません)。

bytes[0]を出力する流れ

さらに、これらの bytes のデータを出力する際には printf のフォーマットとして "%02x" が指定されています。このようにフォーマットを指定した場合は2桁の16進数として値が出力されることになります。上図でも示しているように、bytes[0] を最終的に unsigned char 型に変換したデータは 0 なので、bytes[0] の出力結果としては 00 が得られることになります。

bytes[1] から bytes[3] に関しても、bytes[1] から bytes[3] のアドレスのデータに同様の変換が行われて出力されることになります。

bytesの各要素が出力される流れ

したがって、bytes メンバーを先頭から1バイトずつ出力した場合、0x00 0x00 0x40 0x41 の各バイトそれぞれが unsigned char 型として扱った場合の値が出力されることになり、その出力結果は 00004041 となります。

bytes:0x00004041

float 型のメンバーの出力

最後の float_num はちょっとややこしいです。

float_num の先頭アドレスから float のサイズ分のデータを取得し、データの並び順を元に戻すところまでは、今までの説明と同様になります。

float_numを出力する流れ(途中まで)

問題は、この並び順が元に戻された結果の 0x41400000 がどのようにして float 型に変換されるのか、という点になります。

まず、float は単精度の浮動小数点数を扱う型であり、この単精度の浮動小数点数は下記のような形式の4バイトのデータで扱うことが IEEE754 の規格によって定められています。

単精度浮動小数点数の内部データの形式

そして、この4バイトのデータの各ビットの値から求まる「符号部」・「指数部」・「仮数部」を用いて、下記の式によって計算される値が、その4バイトのデータを浮動小数点数として扱った場合の値となります。

単精度浮動小数点数の計算式

何を言っているか意味不明…と思われた方は、ぜひ下記ページを読んでみてください。浮動小数点数のコンピューター内部でのデータの扱いについて詳しく説明しています。

浮動小数点数における数値と内部データの変換方法解説ページアイキャッチ 【C言語】浮動小数点数における「数値⇔内部データ(符号部・指数部・仮数部)」の変換

さて、先ほど並び順を元に戻して得られた 0x41400000 を上記の考え方に基づいて float 型のデータに変換してみましょう!まずは、この 0x41400000 を2進数に変換すると、結果は下記のようになります。

01000001010000000000000000000000

さらに、この2進数における 01 の並びを、先ほど図示した IEEE754 の規格で定められたデータの形式に当てはめると、符号部、指数部、仮数部はそれぞれ下記のようになります。

  • 符号部:0
  • 指数部:10000010
  • 仮数部:10000000000000000000000

これらの符号部・指数部・仮数部のそれぞれを10進数に変換し、さらに前述で示した計算式に当てはめると、式は次のようになります。そして、この計算結果は 12.0 となります。

例における単精度浮動小数点数の計算式

したがって、float_num メンバーを出力した場合、0x00 0x00 0x40 0x41 の4バイトを float 型として扱った場合の値が出力されることになり、その出力結果は 12.0 となります(小数点以下の桁数はもっと多くなります)。

float_num : 12.000000

メンバーの型に応じてデータの扱い方が変わる

長々と説明してきましたが、ここまでの例での最大のポイントは、0x00 0x00 0x40 0x41 の4バイトのデータが、利用するメンバーに応じて変化するという点になります。

ここまで説明してきたように、変数や共用体のメンバー等に値を代入した場合、コンピューター内部では、その変数やメンバーのアドレスに、代入先の変数やメンバーの型のサイズ分の 0 or 1 のデータが格納されることになります。そして、その 0 or 1 の一連のデータは表現の仕方によって異なるデータとして扱うことが出来ます。

前述のとおり、バイト単位の方が人間にとって扱いやすく、バイト単位で考えることも多いのですが、いずれにせよ、ここで重要なのは、コンピューター内部で管理されるデータは意味のない単なるビットのデータの集合 or バイトのデータの集合であるという点になります。そして、それらのデータに表現の仕方・扱い方というエッセンスを加えることで、これらのデータの集合が意味のある “整数” であったり、”浮動小数点数” であったり、”文字” であったり “色” に変化するのです。

単なるバイトのデータが意味のあるデータに変化する様子

共用体において、その表現の仕方を指定するのが各メンバーの “型” となります。異なる型のメンバーを共用体の型に持たせておけば、1つの共用体の型の変数で管理するデータは1つのみではあるものの、様々な表現の仕方でデータを扱うことが可能となります。そして、どのメンバーからデータにアクセスするかを指定するだけで、そのメンバーの型に応じた値の出力や値の代入等を行うことが可能となります。

単なるバイトのデータが利用するメンバーの型によって意味のあるデータに変化する様子

つまり、共用体を利用すれば、メンバーを使い分けるだけで同じ1つのデータを異なる表現の仕方にすることができます。これは、異なる型への変換が容易に行えることを意味します。

同じ1つのデータを様々な型のデータに変換する様子

例えばですが、共用体を利用すれば、先ほども説明したように特定の浮動小数点数のコンピューター内部での管理のされ方を簡単に確認することができます。

このような、浮動小数点数のコンピューター内部での扱い方を調べるというのはマニアックな共用体の使い方であって、実際のプログラム開発では、このような目的で共用体を利用することは少ないと思います。ただ、unsigned char 型の配列をメンバーに用意しておけば、単なるバイトのデータの集合を、他のメンバーの型に応じた様々な形式のデータに一瞬で変換することもできますし、逆に意味のある構造体等のデータを単なるバイトのデータに変換するようなことも可能です。そして、こういったテクニックは通信などを行う際にはよく利用されるので、こういった使い方が可能であることは覚えておくと良いと思います。

スポンサーリンク

サイズが節約可能

共用体を利用するメリットとして、プログラムの使用メモリサイズが節約可能である点も挙げられます。おそらく、共用体のメリットとして一番に挙げられるのはこの点になるのではないでしょうか?

少ないメモリでたくさんのデータが管理できるというわけではない

ただ、勘違いしないで欲しいのは、共用体に複数のメンバーを持たせておくことで、少ないメモリでたくさんのデータを管理できるというわけではないという点になります。前述でも説明しましたが、1つの共用体の型の変数で管理できるデータは1つのみです。なので、どれだけたくさんのメンバーを持たせたとしても、1つの共用体の型の変数で管理できるデータは1つのみで、少ないメモリでたくさんのデータが管理できるというわけではありません。

なんですが、共用体には1つのデータに対して複数の扱い方を行うことができるという特徴があり、これを上手く利用することでメモリの節約を実現することが可能です。

これに関しては、具体例で考えるのが理解しやすいと思いますので、具体例を挙げて説明していきたいと思います。

共用体で使用メモリの節約を行う具体例

例えば、下記のような2つの構造体(S_STUDENTS_EMPLOYEE)について考えたいと思います。

S_STUDENTとS_EMPLOYEE
typedef enum {
    E_ELEMENTARY, // 小学生
    E_JUNIOR_HIGH, // 中学生
    E_SENIOR_HIGH, // 高校生
    E_UNIVERSITY, // 大学生
} E_SCHOOL_TYPE;

typedef struct {
    char name[256]; // 名前
    unsigned int age; // 年齢
    E_SCHOOL_TYPE school; // 学校の種類
    unsigned int grade; // 学年
    char club[256]; // 所属クラブ名
} S_STUDENT;

typedef struct {
    char name[256]; // 名前
    unsigned int age; // 年齢
    char company[256]; // 会社名
    char profession[256]; // 職種名
    unsigned int salary; // 給料
} S_EMPLOYEE;

ちなみに、E_SCHOOL_TYPE は列挙型(enum 型)であり、この列挙型に関しては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。

列挙型・enum の解説ページアイキャッチ 【C言語】列挙型(enum)について解説

上記の2つの構造体において、S_STUDENT は学生を表現する構造体、S_EMPLOYEE は会社員を表現する構造体となっています。それぞれの特徴を表すメンバーを持たせているつもりです。

定義した構造体の説明図

これらの型のサイズは、環境によって異なる可能性もありますが、私の PC では S_STUDENT524 バイト、S_EMPLOYEE776 バイトとなります。ここからは、各構造体がこれらのサイズであることを前提に解説していきます。

さて、ここで、合計で 100 人分の学生 or 会社員の情報を管理するプログラムを開発することを考えたいと思います。合計で 100 人分であることは確定していますが、管理する学生や会社員は無作為に抽出するものとし、それぞれの人数はプログラム開発時には未確定であるとしたいと思います。

そして、これらの学生やサラリーマンを配列で管理するとすれば、両方とも最大で 100 人である可能性があるため、サイズ 100S_STUDENT と S_EMPLOYEE の配列を宣言しておくことが一番シンプルな管理方法になると思います。

最大人数分の配列の定義
S_STUDENT students[100];
S_EMPLOYEE employees[100];

このとき、S_STUDENT の型のサイズは 524 バイトなので、サイズ 100 の配列では 52400 バイトのメモリが使用されることになります。同様に、S_EMPLOYEE の型のサイズは 776 バイトなので、サイズ 100 の配列では 77600 バイトのメモリが使用されることになります。つまり、これらの配列の合計で 130000 バイトのメモリが必要になることになります。

学生と会社員のデータを合計100人分管理するために必要なサイズ

で、このメモリ使用量を節約するのに役立つのが共用体となります。

結論としては、下記のように構造体と共用体を定義して配列を宣言してやれば、100 人分の学生 or 会社員のデータを管理することが出来るようになります。この時に必要な配列のメモリは 78000 のみとなり、先ほど説明して算出した 130000 よりも必要なメモリを節約できることになります。

共用体を利用したメモリの節約
typedef enum {
    E_STUDENT,
    E_EMPLOYEE,
    E_OCCUPATION_MAX
} E_OCCUPATION_TYPE;

typedef enum {
    E_ELEMENTARY, // 小学生
    E_JUNIOR_HIGH, // 中学生
    E_SENIOR_HIGH, // 高校生
    E_UNIVERSITY, // 大学生
} E_SCHOOL_TYPE;

typedef struct {
    char name[256]; // 名前
    unsigned int age; // 年齢
    E_SCHOOL_TYPE school; // 学校の種類
    unsigned int grade; // 学年
    char club[256]; // 所属クラブ名
} S_STUDENT;

typedef struct {
    char name[256]; // 名前
    unsigned int age; // 年齢
    char company[256]; // 会社名
    char profession[256]; // 職種名
    unsigned int salary; // 給料
} S_EMPLOYEE;

typedef union {
    S_STUDENT student;
    S_EMPLOYEE employee;
} U_OCCUPATION;

typedef struct {
    E_OCCUPATION_TYPE occupation_type;
    U_OCCUPATION occupation;
} S_PERSON;

S_PERSON persons[100];

ポイントはやっぱり共用体の型として定義している U_OCCUPATION になります。U_OCCUPATIONS_STUDENT の型のメンバーと S_EMPLOYEE の型のメンバー変数を持っていますので、共用体の型のサイズはメンバーの最大サイズとなる で説明したように、これらのサイズの大きい方が U_OCCUPATION のサイズとなります。具体的には S_EMPLOYEE の型のサイズである 776 バイトが U_OCCUPATION のサイズとなります。

U_OCCUPATION型のサイズ

また、U_OCCUPATION の変数で管理できるデータの数は結局1つのみではあるのですが、 S_STUDENT の型のメンバーと S_EMPLOYEE の型のメンバーを持っていますので、1つのデータを S_STUDENT の型のデータとしても S_EMPLOYEE の型のメンバーとしても扱うことが出来ます。つまり、U_OCCUPATION の1つの変数で扱えるデータは1つのみではあるものの、そのデータは S_STUDENT でも S_EMPLOYEE でも良い抽象的な変数となります。

S_OCCUPATION型のデータからS_STUDENTとS_EMPLOYEEへの変換が可能であることを示す図

どちらの型でも扱うことが出来るため、S_STUDENT の型のデータと S_EMPLOYEE の型のデータの数の合計が 100なのであれば、U_OCCUPATION 型の配列の場合はサイズは 100 で済むことになります。とにかくどちらのデータも U_OCCUPATION 型の配列に格納し、後は扱い方を変えてやれば良いだけです。異なる型へのデータ変換が容易 で説明したように、共用体を利用すれば型の変換は容易に行うことができます。

U_OCCUPATION型のデータからS_STUDENT型やS_EMPLOYEE型への変換を行う様子

ただし、U_OCCUPATION 型の配列に単に S_STUDENT の型のデータ or  S_EMPLOYEE の型のデータを格納した場合、後からどの要素がどちらの型のデータであるかが分からなくなってしまいます。そのため、どの要素がどちらの型のデータであるかを判別するためのデータも管理してやった方がよいでしょう。

上記では、それを管理するために S_PERSON を定義しています。この構造体の occupation メンバーは U_OCCUPATION 型で、前述のとおり、この occupation では S_STUDENT の型のデータも  S_EMPLOYEE の型のデータも管理することが可能です。さらに、E_OCCUPATION_TYPE 型のメンバー occupation_type も持たせており、このメンバーで occupation で実際にどちらの型のデータを管理しようとしているかを判別できるようにしています。

どの型でデータを利用するかを管理するメンバーを別途持たせている様子

具体的には、S_PERSON の型の変数や配列の要素の occupation にデータをセットする際、そのデータが S_STUDENT の型のデータの場合に occupation_type に E_STUDENT をセットし、そのデータが S_EMPLOYEE の型のデータの場合に occupation_type に E_EMPLOYEE をセットしてやれば、上の図のように後から occupation_type を参照して occupation にセットされているデータが S_STUDENT or S_EMPLOYEE のどちらであるかを判別することができるようになります。

ということで、単にデータを記録するだけであれば U_OCCUPATION 型で十分ですが、後からデータを扱う際には、S_PERSON のように “どの型としてデータを扱えば良いかを判別するためのメンバー” を持たせた構造体の型を用意しておくのが良いと思います。

そして、この場合でも S_PERSON のサイズは 780 となり、100 人分の学生 or 会社員のデータを管理するために必要になるメモリ使用量は 78000 となります。単に2つの構造体でデータを管理する時の 130000 よりも大きくサイズを削減することが可能となります。

S_STUDENTとS_EMPLOYEEとで管理する場合とS_PERSONで管理する場合との必要なメモリ量の比較

単に1つの種類のデータを管理する場合はハッキリ言って共用体を使うメリットはほとんどないですが、上記の例のように複数の種類のデータを管理し、さらに動的に管理する種類のデータが変化するような場合は共用体を利用することで必要なメモリ使用量を大きく削減することができます。そのため、特に使用可能なメモリ量が少ない環境での開発では、この共用体が活躍します。

汎用的な関数が作成可能

また、共用体のメリットとして「汎用的な関数が作成可能」である点も挙げられます。

C言語では、関数を定義する際には具体的な仮引数の型を指定する必要があります。そして、基本的には、関数呼び出し時には仮引数の型に合わせた実引数を指定して関数を呼び出す必要があります(もちろん、無理やり仮引数とは異なる型の実引数を指定することも可能ですが、その場合は変に型変換が行われて意図通りに関数が動作しなかったりバグの原因になったりする可能性が高いです)。

仮引数の型のデータを実引数に指定する様子

ここまでの説明にも登場した S_STUDENT と  S_EMPLOYEE の例で具体的に考えてみたいと思います。例えば、S_STUDENT の型のデータや S_EMPLOYEE の型のデータを引数として受け取り、引数として受け取ったデータの各種メンバーの値を出力する関数を定義したいとします。

共用体を利用しない場合の関数定義

この場合、下記のように2つの関数を別々に定義する方が多いのではないかと思います。S_STUDENT と  S_EMPLOYEE は比較的似ている構造体ではあるのですが、関数の仮引数に型を具体的に指定する必要があるため、下記のように構造体の型ごとに関数を定義する必要があります。まぁ、このように定義するのも悪くはないのですが、構造体の型が増えるたびに関数を追加する必要があって少し面倒ですね…。

構造体のメンバーを出力する関数群
void printStudent(S_STUDENT *student) {
    printf("name         : %s\n", student->name);
    printf("age          : %d\n", student->age);
    printf("school       : %d\n", student->school);
    printf("grade        : %d\n", student->grade);
    printf("club         : %s\n", student->club);
}

void printEmployee(S_EMPLOYEE *employee) {
    printf("name         : %s\n", employee->name);
    printf("age          : %d\n", employee->age);
    printf("company      : %s\n", employee->company);
    printf("profession   : %s\n", employee->profession);
    printf("salary       : %d\n", employee->salary);
}

ここまでの解説を読んでくださった方であれば既に察してくださっているかもしれないですが、共用体を利用することで上記の2つの関数を1つにまとめることができます。

共用体を利用する場合の関数定義

今回の例であれば、U_OCCUPATION 型を関数の仮引数の型に指定しておけば、U_OCCUPATION 型のデータを関数が受け取ることができるようになります。そして、その U_OCCUPATION 型のデータを S_STUDENT 型のデータとして扱いたいのであれば U_OCCUPATION 型のデータの持つ student メンバーを利用し、S_EMPLOYEE 型のデータとして扱いたいのであれば U_OCCUPATION 型のデータの持つ employee メンバーを利用するようにしてやれば、1つの仮引数で2つの型のデータを扱うことができることになります。

U_OCCUPATION型のデータからS_STUNDENT型のデータやS_EMPLOYEE型のデータを扱う様子

異なる型へのデータ変換が容易 で説明したように、共用体では1つのデータしか扱えませんが、その1つのデータの扱い方は容易に変更することが可能です。

下記は、仮引数を U_OCCUPATION * 型とすることで、構造体 S_STUDENTS_EMPLOYEE の各メンバーを出力する関数の例となります。 

仮引数を共用体とすることで関数を1つにまとめる
void printOccupation(U_OCCUPATION  *occupation) {

    if (S_STUDENTで扱いたい場合) {
        
        S_STUDENT student = occupation->student;

        printf("name         : %s\n", student.name);
        printf("age          : %d\n", student.age);
        printf("school       : %d\n", student.school);
        printf("grade        : %d\n", student.grade);
        printf("club         : %s\n", student.club);
    } else if (S_EMPLOYEEで扱いたい場合) {

        S_EMPLOYEE employee = occupation->employee;

        printf("name         : %s\n", employee.name);
        printf("age          : %d\n", employee.age);
        printf("company      : %s\n", employee.company);
        printf("profession   : %s\n", employee.profession);
        printf("salary       : %d\n", employee.salary);
    }
}

ただ、上記で if 文の条件が日本語になってしまっているように、単に U_OCCUPATION 型のデータを受け取るだけだと、そのデータを S_STUDENTS_EMPLOYEE のどちらで扱えば良いかが関数内部で判断できません。なので、関数内でデータを S_STUDENTS_EMPLOYEE のどちらで扱えば良いかを判断するためのデータが追加で必要になります。

例えば、前述でも紹介した S_PERSON であれば、U_OCCUPATION 型 の occupation メンバーを持っており、このメンバーのデータは S_STUDENTS_EMPLOYEE の両方の型のデータとして扱うことが可能です。さらに、S_PERSONoccupation メンバーをどちらの型で扱うべきかを判断するためのメンバーとして occupation_type メンバを持っています。

そのため、関数で S_PERSON 型を受け取るようにすることで、occupation メンバーを適切な型に変換することができるようになります。そして、その関数の例が下記となります。この関数であれば、1つの引数から2つの構造体の型のデータを適切に扱うことが可能です。

2種類の構造体のメンバーを出力する関数
void printPerson(S_PERSON *person) {
    if (person->occupation_type == E_STUDENT) {
        S_STUDENT student = person->occupation.student;

        printf("name         : %s\n", student.name);
        printf("age          : %d\n", student.age);
        printf("school       : %d\n", student.school);
        printf("grade        : %d\n", student.grade);
        printf("club         : %s\n", student.club);
    } else if (person->occupation_type == E_EMPLOYEE) {
        S_EMPLOYEE employee = person->occupation.employee;

        printf("name         : %s\n", employee.name);
        printf("age          : %d\n", employee.age);
        printf("company      : %s\n", employee.company);
        printf("profession   : %s\n", employee.profession);
        printf("salary       : %d\n", employee.salary);
    }
}

このように、関数の仮引数に共用体の型 or 共用体の型をメンバーに持つ構造体を指定すれば、そのメンバーから各型のメンバーにアクセスすることで、関数内部で複数の型のデータを扱うことが出来るようになります。これにより、1つの引数に対して1つの型のデータではなく複数の型のデータを扱うことが可能な関数を実現することができ、関数の汎用性を向上させることができます。

抽象的な型が実現可能

先ほどの 汎用的な関数が作成可能 で示したようなメリットが得られるのは、共用体を利用することで「抽象的な型」が実現可能となるからになります。

汎用的な関数が作成可能 では S_STUDENTS_EMPLOYEE の抽象的な型として U_OCCUPATION を導入し、この U_OCCUPATION を関数の仮引数の型とすることで、U_OCCUPATION の具体的な型となる S_STUDENTS_EMPLOYEE の両方のデータを扱うことが可能な関数を実現しています。

抽象的な型を関数の仮引数とし、関数内で具体化することで様々な型のデータを受け取ることができる関数が実現可能であることを示す図

このように、プログラムでは型を抽象化することで様々なメリットが得られることが多いです。

この型の抽象化は、イメージとしては他のプログラミング言語におけるサブクラスとスーパークラス(子クラスと親クラス)の関係に似ています。そして、この型の抽象化は、C言語においては共用体を利用することで実現することが可能です。これは、異なる型へのデータ変換が容易 で説明したように、共用体が1つのデータを複数のメンバーで扱うことができ、それによって他の型への変換が容易に行えるという特徴を持つからになります。結局、1つのデータを複数のメンバーで扱うことができるという一見意味のなさそうな特徴が、実は共用体の特徴の最大のポイントであり、最大の特徴になります。

具体的には、複数の具体的な構造体の型のメンバーを持つ共用体の型を定義することで、この抽象化を実現することが出来ます。ここまでの例であれば、U_OCCUPATION がこれにあたります。

U_OCCUPATIONが2つの構造体の型を抽象化したクラスであることを示す図

そして、U_OCCUPATION の変数は S_STUDENT や S_EMPLOYEE を抽象化した変数と考えられます。そして、この U_OCCUPATION の変数の stundent メンバーや employee メンバーを利用することで、具体化した型である S_STUDENTS_EMPLOYEE のデータを扱うことができるようになります。

共用体のメンバーを利用して具体的な型に変換する様子

ただし、実際には、共用体の型の変数や配列の要素がどの型のデータであるかを判別できるようにするメンバーを持たせる必要があるため、共用体の型のメンバーを持つ構造体を別途定義することが多いと思います。ここまでの例であれば、これは S_PERSON がこれにあたります。

ここまで説明してきたように、共用体 or 共用体の型のメンバーを持つ構造体を導入することで、型の抽象化を行い、よりハイレベルなコーディングができるようになります。是非、この抽象化も意識して実装等に取り組んでみていただければと思います!

スポンサーリンク

まとめ

このページでは、C言語における共用体(union)について説明しました!

共用体と構造体は、使い方に関しては似ていますが、これらは全く異なるデータの構造になります。

これらの決定的な違いは、構造体がメンバーの数だけのデータを管理できるのに対し、共用体では管理できるデータの数が1つのみである点になります。構造体の場合は、メンバーごとに異なるデータが存在し、異なるメンバーからは異なるデータにアクセスすることになります。それに対して共用体では管理するデータが1つのみで、その1つのデータを各メンバーの型に応じたデータとして扱うことができます。

共用体を利用することで使用メモリを削減できると考えている人もおられるかもしれませんが、1つのデータを少ないメモリで扱うことが出来るというわけではありません。圧縮など特別なことをしない限り、管理できるデータのサイズや数に応じて必要なメモリは決まっています。これはコンピューター上でデータを扱うときの原則であり、共用体を利用してもこれは変わりません。

ですが、使い方次第では共用体の導入によって使用できるメモリを削減できることもあります。具体的には、動的に扱うデータの型が異なるような場合です。

また、共用体を利用することで型を抽象化することも可能です。そして、これによって簡潔で読みやすく、さらにメンテナンス性の高いソースコードを実現することが可能です。

共用体の使い方や特徴は理解している人は多いかもしれませんが、共用体のメリットをしっかり理解している人は少ないのではないのかと思います。なので、使ったことが無い方も多いと思います。是非、このページで読んだ内容を頭の片隅にでもとどめておいていただき、設計や実装をしているときに共用体のメリットが活かせそうと感じた場合には、是非共用体を利用してみてください!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示