C言語編 第37章 ポインタ⑦(構造体とポインタ)

先頭へ戻る

この章の概要

この章の概要です。

構造体へのポインタ

ここまでの章にも少しだけ登場していますが、構造体変数を指し示すポインタを作ることができます。

#include <stdio.h>

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

int main(void)
{
    Point point;
    Point* p = &point;

    point.x = 10;
    point.y = 20;

    printf( "%d %d\n", (*p).x, (*p).y );

    return 0;
}

実行結果:

10 20

構造体のメンバをアクセスするとき、通常はドット演算子を使います。それに忠実にならうなら、このサンプルプログラムのように、「(*p).x」といった少々面倒な記述が必要になります。つまり、まず間接参照を行って、構造体変数を参照し、そこから更にドット演算子を使って任意のメンバを参照します。

これはこれで正しいのですが、ポインタ経由で構造体のメンバにアクセスする機会は結構多いので、いちいちこういう記述をするのは面倒です。そこで、-> で表現されるアロー演算子(矢印演算子)を使うのが一般的です。「->」は「-」と「>」の2文字を合体させたものです。

アロー演算子は、(*p).x のような記述に対する構文糖であると言えます。

先ほどのサンプルプログラムを、アロー演算子を使って書き換えると、次のようになります。

#include <stdio.h>

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

int main(void)
{
    Point point;
    Point* p = &point;

    point.x = 10;
    point.y = 20;

    printf( "%d %d\n", p->x, p->y );

    return 0;
}

実行結果:

10 20

自己参照構造体

ある構造体のメンバに、自分自身と同じ型の構造体を含めたいことがあります。

struct Student_tag {
    char*               name;
    int                 grade;
    int                 class;
    int                 score;
    struct Student_tag  next;  /* コンパイルエラー */
};

しかし、これはコンパイルできません。構造体定義が完了するまで(} のところに到達するまで)は、この構造体は型として完全でないからです。

そこで、構造体をネストして使いたい場合には、ポインタを利用して次のようにします。

struct Student_tag {
    char*               name;
    int                 grade;
    int                 class;
    int                 score;
    struct Student_tag* next;  /* OK */
};

もちろんこれだと、メンバnext は構造体変数そのものではなく、構造体変数を指し示すポインタ変数になるので、実際に使う際には、自分でメモリアドレスを代入するなり、malloc関数などで確保を行い、得られたポインタを代入するなりしなければなりません。

ちなみに、自分自身のメモリアドレスを保持させることも可能です。このような構造体は、自己参照構造体と呼ばれることがあります。

#include <stdio.h>

struct Student_tag {
    char*               name;
    int                 grade;
    int                 class;
    int                 score;
    struct Student_tag* next;
};

int main(void)
{
    struct Student_tag student = { "Saitou Takashi", 2, 3, 80, NULL };
    
    student.next = &student;  /* 自身を指すポインタを代入 */

    printf( "%s\n", student.next->name );

    return 0;
}

実行結果:

Saitou Takashi

構造体変数の定義が終わらないと、メモリアドレスが決定されないので、構造体変数を宣言する時点で、いきなり自分自身のメモリアドレスを使うことはできません。そのため、初期値としては一旦、NULL を与えています。

自己参照構造体は、連結リストというデータ構造を形作る際に必須の手法です。C言語そのものの学習から外れてしまうので、これ以上深入りしませんが、プログラムを続けていると必ず登場する必須の知識ではありますから、調べてみると良いと思います(連結リストについては、アルゴリズムとデータ構造編【データ構造】第3章で解説しています)。


offsetof

同じ構造体に含まれているメンバを指すポインタ同士を、関係演算子で比較した場合、手前側(構造体定義内で先に宣言されているメンバ)にある方が小さいことになります。

実際、各メンバのメモリアドレスを出力すると、メンバの宣言順に昇順に並びます。

#include <stdio.h>

struct Data_tag {
    int    a;
    double b;
    char   c[16];
};

int main(void)
{
    struct Data_tag data = { 10, 1.5, "abcde" };
    
    printf( "%p\n", &data.a );
    printf( "%p\n", &data.b );
    printf( "%p\n", data.c );

    return 0;
}

実行結果:

006FF970
006FF978
006FF980

メンバ間に入るパディングの影響で、手前のメンバが使うメモリ領域の直後に、次のメンバが来ないことはあります。この実行結果でいうと、a と b の間に、4バイトのパディングがあるようです(a は 4バイト)。

offsetofマクロを使うと、構造体のメンバが、先頭からどれだけの距離のところにあるかを知ることができます。offsetofマクロは、stddef.h で以下のように定義されています。

#define offsetof(s, m)  /* 実装依存 */

s には構造体型の名前を、m にはメンバの名前を指定します。すると、s の先頭から m までのバイト数(オフセット)を表す定数式に置換されます。この結果は size_t型です。

offsetofマクロは、ビットフィールド(第56章)になっているメンバに対しては未定義の動作になります。

先ほどと同じ構造体型を使って確認してみます。

#include <stddef.h>
#include <stdio.h>

struct Data_tag {
    int    a;
    double b;
    char   c[16];
};

int main(void)
{
    printf( "%u\n", offsetof(struct Data_tag, a) );
    printf( "%u\n", offsetof(struct Data_tag, b) );
    printf( "%u\n", offsetof(struct Data_tag, c) );

    return 0;
}

実行結果:

0
8
16

a の大きさは 4バイトですが、b が 8バイト目のところにあることが明確になりました。このように、パディングの入り方を問わず、正しい位置を得ることができます。

アラインメント

ここまでの章でも、構造体のメンバ間や末尾に、パディング(詰め物)が入ることがあるという話をしました。そもそも、パディングが入る理由は、オブジェクトが、メモリ上の都合の良いメモリアドレスに配置されることを強制するためです。このような強制を行う要求を、アラインメント(境界調整)と呼びます。

具体的には、ある倍数のメモリアドレスにオブジェクトを配置させようとします。そうすることで、メモリアクセスが効率よく行えます。アラインメントが不適切だと、メモリアクセスの効率が低下するか、そもそもアクセス不可能となりエラーを発生させる可能性があります。

ここで起こるエラーは、アラインメントエラーとかバスエラーと呼ばれます。これは、C言語のレベルの話ではなく、ハードウェア側の問題です。このようなエラーが起こらないように、コンパイラは適切なアラインメントを行います。

このような事情から来るものなので、アラインメントが必要なのは構造体だけではありません。どんな型のオブジェクトであっても、それが適切な位置に配置されている必要性があります。

要求されるアラインメントの単位は、実行環境によって異なりますし、型ごとにも異なり得るものです。int型や double型などの基本的な型は、多くの場合、その型の大きさの倍数のアラインメントを要求します。int型が 4バイトなら 4 の倍数、double型が 8バイトなら 8 の倍数といった具合です。

また、ポインタが保持するメモリアドレスは、そのポインタが指し示す型の大きさに合わせたアラインメントを要求します。例えば、int型が 4バイトであれば、int* が保持するメモリアドレスは 4 の倍数であることを求めます。void* に関しては、1 の倍数であればよく、つまりは何でも構わないということになります。

配列要素の1つ1つも、要素の型に応じたアラインメントを要求します。配列の要素は隙間なく並ぶので、先頭要素が配置されたメモリアドレスが適切であれば、後続の要素も適切な位置に置かれるはずです。例えば、4 の倍数のアラインメントを要求し、先頭要素が 1000 という位置に置かれたなら、後続の要素は 1004、1008、1012・・・に置かれるので、すべての要素が自動的にアラインメントの要求を満たすことができます。

構造体の末尾にパディングが入る理由はここにあります。構造体型の配列を作ったとき、各要素が適切な位置に置かれるようにするには、構造体の大きさを適切な単位まで切り上げておかないといけません。例えば、4 の倍数のアラインメントを要求するとします。そして、構造体の大きさが 14バイト(4 の倍数でない)だったとします。先頭要素が 1000 という位置に置かれたなら、後続の要素は、(隙間なく詰めるという配列のルールに沿って)1014、1028、1042・・・に置かれてしまい、4 の倍数という要求を満たせないことがあります。末尾にパディングを加えて、構造体の大きさを 16バイト(4 の倍数)に調節してやれば、後続の要素は 1016、1032、1048・・・に置かれるようになりますから、すべての要素がアラインメントの要求を満たすことができます。

アラインメントの要求を満たすための作業は、コンパイラが適切に行うので、基本的には、任せていれば問題になりません。例えば、コンパイラが構造体にパディングを入れるのが、これに当たります。

また、malloc関数などの動的なメモリ割り当てを行う関数は、適切にアラインメントされたメモリアドレスを返してくれることが保証されています。

C11 (アラインメント)

C11 になって、アラインメントに関して機能が追加され、規格上の定義も詳細になりました。

C11 では、アラインメントとして有効な値は、2 のべき乗の正の値であると規定されました(これに 1 は含まれます)。また、その型は size_t型で表現すると記述されています。

_Alignof演算子

C11 では、ある型に要求されるアラインメント値を知る手段として、_Alignof演算子が追加されています。「_Alignof(型名)」のように使用すると、アラインメント値を size_t型の定数で得られます。sizeof演算子に似ていますが、式を指定することはできません。

#include <stdio.h>

struct Data_tag {
    short a;
    double b;
    int c;
};

int main(void)
{
    printf( "%zu\n", _Alignof(int) );
    printf( "%zu\n", _Alignof(double) );
    printf( "%zu\n", _Alignof(char) );
    printf( "%zu\n", _Alignof(struct Data_tag) );

    return 0;
}

実行結果:

4
8
1
8

なお、stdalign.h をインクルードすると、_Alignof の代わりとして alignof という名前が使えるようになります(単なるマクロによる置き換えです)。

_Alignas指定子

C11 では、オブジェクトに要求するアラインメントを、デフォルトよりも厳しいものに強制する _Alignas指定子が追加されています。こちらは、オブジェクトの宣言時に付加するものです。指定できる値は、実装によって制限されます。

VisualStudio 2015/2017 は、_Alignas に対応していません。代わりに、「__declspec(align(x))」が使えます(x にアラインメント値を指定)。

#include <stddef.h>
#include <stdio.h>

struct Data_tag {
    short a;
    char b;
    int c;
};

struct Data2_tag {
    short a;
    _Alignas(8) char b;
    int c;
};

int main(void)
{
    printf( "%zu %zu %zu\n",
        offsetof(struct Data_tag, a),
        offsetof(struct Data_tag, b),
        offsetof(struct Data_tag, c)
    );
    printf( "%zu %zu %zu\n",
        offsetof(struct Data2_tag, a),
        offsetof(struct Data2_tag, b),
        offsetof(struct Data2_tag, c)
    );
    
    return 0;
}

実行結果:

0 2 4
0 8 12

_Alignas の効果の確認に _Alignof を使いたいところですが、_Alignof には型名しか指定できないので、ここでは offsetofマクロを使って、位置を確認することにしています。各メンバの置かれる位置はきちんと変化しているようです。

なお、stdalign.h をインクルードすると、_Alignas の代わりとして alignas という名前が使えるようになります(単なるマクロによる置き換えです)。

aligned_alloc関数

C11 では、動的なメモリ割り当てを行うときにアラインメントを指定できるように、aligned_alloc関数が追加されています。stdlib.h に、次のように宣言されています。

void* aligned_alloc(size_t alignment, size_t size);

VisualStudio 2015/2017 は、aligned_alloc に対応していません。

malloc関数にアラインメントの指定が加わったものと考えられます。割り当てられたメモリ領域は初期化されておらず、不定な状態です。解放は、free関数で行います。

引数 alignment にアラインメント値を指定します。これは、実装がサポートしている大きさに限られます。

引数 size には、確保する大きさを指定します。これは、引数 alignment に指定した値の倍数でなければなりません。

メモリの割り当てに成功したら、そのメモリアドレスが返されます。失敗した場合は、ヌルポインタが返されます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int* p1 = malloc( sizeof(int) * 256 );
    int* p2 = aligned_alloc( 256, sizeof(int) * 256 );
    
    printf( "%p\n", p1 );
    printf( "%p\n", p2 );

    free( p2 );
    free( p1 );
        
    return 0;
}

実行結果:

0x180a010
0x180a500

p1 のメモリアドレスは 16 の倍数、p2 のメモリアドレスは 256 の倍数になりました。

パディングの調整

構造体のパディングに関していえば、プログラマの工夫によって入り方を調整する余地があります。例えば、次の構造体を考えます。

struct Data_tag {
    char   a;
    int    b;
    char   c[20];
    double d;
    short  e;
};

要求されるアラインメントは、char型は 1 の倍数、short型は 2 の倍数、int型は 4 の倍数、double型は 8 の倍数とします。

この場合、メモリの使われ方は次の図のようになると思われます(繰り返しになりますが、アラインメントは実行環境に応じて異なるので、必ずこの通りになる訳ではありません)。白いところがパディング、他の色のところはメンバが使っている部分です。

非効率なパディングの入り方

メンバ間に 2つと、末尾にパディングが入り、全体としては 48バイトになりました。例えば、b は 4 の倍数のアラインメントを要求するので、a の後ろに 3バイトのパディングが入っています。

次に、メンバの並び順を変更して、次のように変えたとします。

struct Data_tag {
    double d;
    int    b;
    short  e;
    char   c[20];
    char   a;
};

これは、要求されるアラインメントが大きい方から順に並べています。メモリは次の図のような使われ方をします。

効率的なパディングの入り方

メンバ間のパディングがなくなり、末尾の 5バイト分のパディングだけになりました。結果、構造体全体の大きさも 8バイト削減されました。

このように、アラインメントの要求が大きい方から並べるようにすると、メンバ間のパディングを削減できます。ただし、後からメンバの宣言順を変えると、メンバ同士のメモリアドレスの前後関係も変わってしまうことに注意して下さい。


練習問題

問題① 「パディングの調整」の項で見た、構造体の2つの形式について、自分の環境では各メンバがどのように配置されるか、offsetofマクロを使って確認してください。

問題② 2次元上の5つの点を結んで、循環する経路を作りたいと思います。自己参照構造体を使って、このような構造を表現して下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/6/1 内容のすべてを第38章へ上書き。この章の内容は完全に新規のものになった。
章のタイトルを変更(「ポインタ⑦(関数ポインタ)」->「ポインタ⑦(構造体とポインタ)」)
第31章から、「構造体へのポインタ」の項を移動。内容は「構造体メンバへのポインタ」に分割した。
第36章から、「自己参照構造体」の項を移動。

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/4/5 bsearch関数でサーチを行う対象の配列は、昇順でソートされていなければならないことを明記した。

'2018/3/13 全面的に文章を見直し、修正を行った。
章のサブタイトルを変更(高度な使用法 -> 関数ポインタ)
「volatile修飾子」の項を削除。
「コールバック(qsort関数、bsearch関数)」の項を、「qsort関数」と「bsearch関数」に分離。

'2018/3/9 「const修飾子」の項を削除。 すべての話題は、第32章で扱うことにした。
const に関する練習問題①を、第33章へ移動。

≪更に古い更新履歴を展開する≫





前の章へ(第36章 ポインタ⑥(データ構造の構築))

次の章へ(第38章 ポインタ⑧(関数ポインタ))

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS