第14章 前へならえ

 構造体のサイズを確認してみたことはありますか? 構造体をファイルに保存したことはありますか? やけにサイズが大きかったり、ゴミが保存されたりはしませんでしたか? 今回は、そういうことがなぜ起こるのかについてのお話です。


 では、今回の要点です。


 では、いってみましょう。


 次のような構造体を考えてみましょう。

strcut SPerson
{
    char  szName[21];  // 名前
    int   nAge;        // 年齢
    char  nBirthmonth; // 誕生月
    char  fSex;        // 性別
};

 さて、問題です。この構造体のサイズはいくらになるでしょうか?

 先ず、szName のサイズは21バイトです。そして、nAge のサイズは(32ビット機であれば)4バイトで、nBirthmonth と fSex のサイズはそれぞれ1バイトずつです。なので、合計27バイトであると思われます。

 さぁ、実際に確かめてみましょう。

プログラム実行結果
// Align1.cpp
#include <iostream.h>

struct SPerson
{
    char  szName[21];  // 名前
    int   nAge;        // 年齢
    char  nBirthmonth; // 誕生月
    char  fSex;        // 性別
};

int main()
{
    cout << "SPerson のサイズは " << sizeof SPerson
         << " バイトです。" << endl;

    return 0;
}
SPerson のサイズは 32 バイトです。

 あれ? 5バイトも多いですね。これは一体どうしたことでしょうか?

 確かにメンバのサイズの合計は27バイトです。しかし、構造体のサイズは32バイトである。つまり、どこかに5バイト余計なデータが入っていることになります。

 このデータのことをパディング(詰め物)と呼びます。実は、szName の後ろに3バイト、fSex の後ろに2バイト入っています。

 このように、構造体のメンバはぎっちりと詰まっているわけではないことがあるのです。しかし、こうなっていないこともあります。そして、この詰まり方がちょっと違うこともありますし、違えることができることもあります

 ということで、構造体のメンバがぎっちり詰まっていると仮定したプログラムはバグです。ぎっちり詰めるようになっている環境で作っているとバグには気が付きませんが、いざ別の環境でコンパイルしてみるととんでもない動作を行うということもあるでしょう。

 こうならないようにぎっちり詰めるように指定することもできるのですが、それは後で話します。


 それでは、何でこんな変な詰め物を入れているのでしょうか? それは、CPUの特性が関わっています。(なので、CPUによってはこのようなことをしても意味がないこともあるでしょう。)

 多くの(推測)CPUは、メモリにアクセスするときにはint のサイズごとにアクセスします。例えば32ビット機では、メモリを4バイトずつに区切ったその1つ1つの単位でアクセスするのです。この区切りを4バイト境界と呼びます。16ビット機では2バイト境界、64ビット機では8バイト境界が重要になります。(面倒なので、今からは32ビット機であるとして話します。)

 このため、この4バイト境界をまたぐデータを扱うためには、メモリに2回アクセスしなければなりません。つまり、この境界をまたがないようにデータを置いておくと、データのアクセスが速くなります。このことをアラインすると呼びます。

 構造体のデータがぎっちり詰まっていないのは、データがアラインされているからなのです。変数の先頭は4バイト境界に揃えられているのが普通で、メンバ変数を構造体の先頭からアラインしておくと、実際にメモリ上に確保されたときもアラインされているようになります。

 int は4バイトなので、4バイト境界に揃えられます。short は2バイトなので、2バイト境界に揃えたので問題ありません。char はバイト境界を気にしなくても、常に1回でアクセスできます。double は8バイトなので、一応同じように8バイト境界に揃えられます。

 しかし、これらはコンパイラによって変わることもあるでしょう。コンパイラの設定を変えることによって、変えることができることもあります。そして、ソースコード中で変える方法も用意されていることもあります(用意されていないこともあります)。


 このアラインする方法(アラインメント)を変える、つまり最大何バイト境界に揃えるかを変える命令は #pragma です。

 #pragma というのは機種依存のいろいろな設定を行う命令なので、この命令のできることはコンパイラによって異なります。なので、#pragma でアラインメントを変えることができるコンパイラもあれば、変えられないコンパイラもあります。

 #pragma の後にはさらに設定の種類を表す命令がくるのですが、設定する項目が同じであっても命令はコンパイラによって変わります(同じこともあるかもしれませんが)。自分はVC++を使っているので、例によってVC++を例にとって話します。

 VC++では #pragma pack というものでアラインメントを変えられます。

プログラム実行結果
// Align1b.cpp
#include <iostream.h>

#pragma pack(1)
struct SPerson
{
    char  szName[21];  // 名前
    int   nAge;        // 年齢
    char  nBirthmonth; // 誕生月
    char  fSex;        // 性別
};
#pragma pack()

int main()
{
    cout << "SPerson のサイズは " << sizeof SPerson
         << " バイトです。" << endl;

    return 0;
}
SPerson のサイズは 27 バイトです。

 #pragma pack(<最大バイト単位>) でアラインメントを変えることができます。1,2,4,8,16の中から選ぶことができます。

 そして、#pragma pack() で設定を元に戻せます。


 次は、メンバ変数の構造体の先頭からの位置を取得してみましょう。例えば、

strcut SPerson
{
    char  szName[21];  // 名前
    int   nAge;        // 年齢
    char  nBirthmonth; // 誕生月
    char  fSex;        // 性別
};

の nBirthmonth はどうでしょうか?

 えーと、sizeof szName + sizeof nAge でしょうか? と、これではあっていることも、間違っていることもあります。これでは不正確ですね。バグです。

 それとも、第1部第50章の4の倍数に揃えた値を求めるコードを使って、((sizeof szName + 3) &~ 3) + sizeof nAge でしょうか? これも不正確ですね。

 このようなときのために、stddef.h というヘッダファイルに offsetof というマクロが定義されています。

offsetof(SPerson, nBirthmonth)

とすれば、nBirthmonth の SPerson の先頭からの位置が取得できます。

 このマクロの定義を見てみましょう。

#define offsetof(s,m)   (size_t)&(((s *)0)->m)

 先ず、ヌルポインタを指定した構造体へのポインタでキャストしています。それから指定したメンバ変数を指定し、そのアドレスを取っています。最後に size_t(unsigned int の同義語で、sizeof の返す値の型を表す)でキャストしています。

 つまり、「構造体の先頭アドレスを0としたときのメンバ変数のアドレス」を返しているわけです。もちろん、これは「メンバ変数の構造体の先頭からの位置」に相当します。うまいもんですね。


 では、今回の要点です。


 今回はちょっと長かったですね。次回もどうなることやら(汗)。


第13章 精密作業 | 第15章 伸縮自在

Last update was done on 2000.9.7

この講座の著作権はロベールが保有しています