[C/C++言語講座] 第3回:構造体

構造体

構造体(struct)は、C言語で導入された新しいデータセットでした。異なる型のデータを一か所に集め、それに名前を付けるというユニークなアイデアにより、プログラミングを飛躍的に容易にしました。
構造体は、メモリ領域に異なる型のデータを連続して定義します。メンバ変数はコンパイラで規定されるアラインメントにより構造体の領域内に配置されます。構造体のメモリ領域を占めるサイズは、他の変数型と同様に sizeof 演算子により知ることができます。
次は構造体の定義の例です。

struct personalInfo {
	short	age;	// 年齢
	char	gender;	// 性別
	char*	name;	// 名前
	int	income;	// 年収
};

まず ‘struct’ 宣言子に続き構造体に名前を付けます。ここでは’personalInfo’という名称で参照される構造体を定義します。この後{}内にメンバー変数を通常の変数定義と同様に記述します。この例では、「年齢」、「性別」、「名前へのポインタ」、「年収」の4つのそれぞれ異なる型のメンバー変数から構成される、個人情報を格納する構造体を定義しました。この構造体定義では構造体の実体は作られません。
構造体の実体の作成は、通常の変数と同様に次のように宣言します。

struct personalInfo suzuki;

通常の変数型と同様に、関数内でこの宣言を行えば局所変数となり、関数外で行うとグローバル変数となります。また、structの前に’static’を記述すればスタティック変数となります。ここでは「suzuki」という個人情報変数を作成しました。
C++では宣言時にstructを省略することができます。C言語でC++と同様にstructを省略したい場合は次のようにtypedef宣言を使用します。

typedef struct personalInfo personalInfo;

上記の構造体定義でtypedef宣言を行うこともできます。

さて、メモリ上ではこの構造体のメンバ変数はどのように配置されているでしょうか。64ビット環境で考えてみます。

構造体の先頭アドレスを0としたとき、上図のように構造体のメンバが配置されます。
上図でage、genderは合計3バイトです。変数型によるアラインメントについては後述しますが、genderはchar型でアラインメントは1なので、ageの直後に配置されます。次のポインタnameのアラインメントは8であるためにgenderとの間に5バイトのスペースが余分に入ります。この部分は使用されません。int型のincomeはアラインメント4なのでnameの直後に配置されます。その後の4バイト(ピンクの部分)は、構造体にポインタが含まれ、全体のアラインメントが8である必要があり、このために追加される部分です。結果、上記の構造体のサイズは24バイトとなり、そのうち9バイトが未使用部分となるため効率がよくありません。

そこで、上図のように incomeをgenderの次に移動すると、上図のように5バイトの空きの部分にすっぽりと入ります。しかも、incomeの後ろにあったスペースもなくなり、構造体のサイズは16バイトになり、8バイトも減らすことができました。構造体の宣言は次のようになります。

struct personalInfo {
	short	age;	// 年齢
	char	gender;	// 性別
	int	income;	// 年収
	char*	name;	// 名前
};

さて、サイズを小さくすることでどのようなメリットがあるでしょう。上記のような構造体は配列として利用することが多く、配列のサイズが大きくなるほど使われないメモリが大きくなります。最近のPCは昔と比べて搭載メモリ量が飛躍的に増え、16Gバイトは普通になっています。そのため、多少の無駄は気にしなくても良いと思う人も多いでしょう。
しかし、プログラマとして気を使わなければならないのは、1回の読み込みでどの程度の配列のメンバがキャッシュに読み込まれるかです。前回の記事で書いたように、キャッシュラインは32バイトや64バイトが多いのですが、最初の例の構造体では、キャッシュラインが32バイトの場合は、1.33個の構造体が読み込まれることになりますが、次の例では2個の構造体がキャッシュに収まることになります。配列のサイズが大きくなると、この差がパフォーマンスに影響を及ぼしてきます。特にゲームでは多数の構造体を利用しますが、例えばパーティクルシステムなどは同一の構造体の配列で大量の粒子を管理するため、構造体一つ当たりのサイズはパフォーマンスに大きく影響を与えることになります。

Visual Stuido 2017による64ビット環境(x64)では、変数型のアラインメントは次の表の通りです。

変数型アラインメント
bool1
char1
short2
int4
long4
long long8
float4
double8
pointer8

long long、double、すべてのポインタ(pointer)のサイズは8バイトであるため、アラインメントは8になります。
尚、余談ですがWindowsではlong型は4バイトですが、Unix系では8バイトの場合が多いです。longが4バイトのプログラミング環境をLLP64(long longとポインタが64ビットの意味)、longが8バイトのプログラミング環境をLP64(longとポインタが64ビットの意味)と呼びます。

ところで、例では変数のアラインメントの説明のために変数型を使っていますが、実際のプログラミングをする場合は、あらかじめ値の範囲がわかっている場合がほとんどですから、その値の幅に合わせた型を使用すると良いです。
例えば、年齢 age はせいぜい110歳程度が最高です。char型では、-128~127までの範囲を格納できます。あるいはunsigned char型にすれば、0~255まで表現でき、年齢には十分すぎる値の範囲となります。
年収 income は、人によっては32ビット int より大きな型が必要になるかもしれません。int型では最大値は2147483647、unsigned int型では最大値4294967295であり、42.9億円を超す年収には対応できません。実際にそんな年収の人は少ないでしょうが、可能性としてはありうるため、プログラマとしては対応せざるを得ません。この場合は、long long型(またはint64_t型)を使用した方が良いでしょう。