T.TAO
Back to Blog
/14 min read/Data Structure

C++#2 Struct と Union

C++#2 Struct と Union

このノートは主に C++ における struct 構造体とそれに似た union の使用方法、およびそれらがメモリ内で実際に占有するサイズについてです。この種のデータ構造は異種データ構造 (Heterogeneous Data Structure) とも呼ばれます。

Struct

声明

struct の宣言は非常にシンプルです。StructName という名前の構造体を定義する構文は以下の通りです。

C++struct StructName {
    DataType1 member1;
    DataType2 member2;
    // ...
};

構造体変数を宣言する方法は以下の通りです。

C++StructName myStruct;

ここで、構造体のメンバーには、intfloatchar などの異なる型のデータメンバー、または他の構造体、配列、ポインタを含めることができます。

構造体のメンバーにアクセスするには演算子(.)を使用します。例えば、myStruct.member1 のようにします。

初始化

宣言時に初期化することができます。

C++StructName myStruct = {value1, value2};

構造体を指すポインタを宣言することもできます。その後、アロー演算子(->)を使用して、このポインタが指す構造体のメンバーにアクセスします。

C++StructName myStruct = {value1, value2};
StructName *myStructPtr = &myStruct;
std::cout << (myStructPtr -> member1); // not myStructPtr.member1 !

内存对齐

構造体において、memory alignment は重要な戦略であり、アクセス速度を最適化し、メモリ空間の無駄を減らすために、構造体のメンバーをメモリ内にどのように配置するかに関わります。

以下は memory alignment の基本原則です:

  1. 構造体の開始アドレスの alignment。例えば、構造体の中で最大のメンバーが uint32_t で 4 バイトを占有する場合、構造体の開始アドレスは 4 バイトの境界に alignment される可能性が高くなります。
  2. 構造体メンバーの alignment。構造体内の各メンバーは、構造体の開始アドレスに対して、通常そのメンバー型の自然な alignment 境界に配置されます。例えば、4 バイトの int メンバーは通常 4 バイトの境界に alignment されます。
  3. 構造体の合計サイズの alignment。構造体の合計サイズは通常、最大メンバーの alignment 境界に合うように padding されます。これは、構造体のサイズがすべてのメンバーのサイズの合計よりも大きくなる可能性があることを意味します。

上記の alignment の基本原則を満たすために、コンパイラは構造体メンバーの間や構造体の末尾に追加の未使用メモリを追加することがあり、これは padding とも呼ばれます。このような memory padding は、各メンバーが適切なメモリアドレスに配置されることを保証するのに役立ちます。

示例

以下のような構造体の定義があると仮定します。

C++struct SomeExample {
	char a; // 1 byte
	int b;  // 4 bytes
	char c; // 1 byte
}

alignment の規則に基づくと、この構造体のメモリ内でのレイアウトは以下のようになる可能性があります:

  • メモリ内で最大のメンバーは int で、4 バイトを占有するため、4 バイトを境界として alignment します。
  • char a :1 バイトを占有。3 バイトを padding します。
  • int b:4 バイトを占有。
  • char c: 1 バイトを占有。3 バイトを padding します。

この時、構造体全体のサイズは 12 バイトになります。その開始位置が 4 の倍数である限り、終了位置の次の位置も常に 4 の倍数になります。

構造体において、コンパイラは通常メンバーの順序に従ってメンバーをレイアウトし、各メンバーの alignment 要件に応じて必要な padding を追加します。したがって、構造体の宣言順序を調整すると:

C++struct SomeExample {
	char a; // 1 byte
	char c;  // 1 byte
	int b; // 4 bytes
}

すると、memory alignment の規則に基づき、この構造体のメモリ内でのレイアウトは以下のようになる可能性があります:

  • char a :1 バイトを占有。padding なし。
  • char ca の直後で、1 バイトを占有。2 バイトを padding します。
  • char c: 4 バイトを占有。padding なし。

この時、構造体全体のサイズはわずか 8 バイトとなり、以前よりちょうど 1/3 減りました。しかも依然として memory alignment の要件を満たしています。

場合によっては、プログラマーがポインタ演算の問題から alignment 戦略を調整することがあります。また、上記の2番目のケースが常に1番目のケースより優れているとは限りません。なぜなら、1番目のケースでは、すべて int のサイズに合わせて開始位置を調整することで、各メンバーのアドレスを素早く取得できるのに対し、2番目のケースではポインタを移動するステップ幅が 1 だったり、2 だったり、4 だったりするからです。最終的には、プロジェクトの具体的な要件に応じて構造体の宣言を調整する必要があります。

内存位置

C++ では、データ型は宣言方法とライフサイクルに応じて異なる場所に保存されます。クラスや構造体も含め、それらの宣言方法の違いにより、メモリ内の異なる場所に保存される可能性があります。

メモリ内で保存できる領域には以下のいくつかがあります:

  1. text segment: プログラムの実行可能コード(機械語命令)を保存するために使用されます。プログラムが誤って命令を変更するのを防ぐため、通常は読み取り専用です。OS がプログラムをロードする際、text segment を読み取り専用メモリとしてマッピングします。
  2. data segment: 初期化されたグローバル変数と静的変数を保存します。data segment はプログラムの起動時に OS によって初期化され、プログラムの終了時に解放されます。data segment はプログラムのライフサイクル全体にわたって存在します。
  3. read-only data segment: 文字列リテラルや定数などの読み取り専用データを保存します。read-only data segment は通常読み取り専用であり、プログラムが誤ってこれらのデータを変更するのを防ぎます。
  4. BSS segment (Block Started by Symbol): 未初期化のグローバル変数と静的変数を保存します。BSS segment はプログラムの実行開始時にゼロで初期化されます。data segment とは異なり、ファイル内の実際のスペースを占有せず、プログラムのロード時に OS によって割り当てられ、ゼロクリアされます。
  5. heap: 動的に割り当てられたメモリを保存します。heap メモリはプログラマーによって管理され、プログラムの実行時に動的に割り当ておよび解放できます。heap メモリのサイズは通常 stack よりも大きいですが、手動で解放する必要があり、そうしないとメモリリークを引き起こす可能性があります。
  6. stack: ローカル変数、関数パラメータ、リターンアドレスなどを保存します。stack メモリは OS によって自動的に管理され、後入れ先出し(LIFO)の特性を持ちます。関数が呼び出されるたびに stack frame が割り当てられ、関数が戻る時に stack frame が解放されます。

以下は一般的なデータ型とそれらのメモリ内での保存場所です。

局部变量(栈)

ローカル変数は通常 stack 上に保存され、そのライフサイクルはそれが存在する関数のスコープ内にあります。関数が呼び出されると stack frame が割り当てられ、関数が戻ると stack frame が解放されます。

例えば、

C++void foo() {
	int x = 10; // x 是 foo() 中的局部变量,存储在栈上
}

全局变量和静态变量(数据段)

グローバル変数と静的変数は data segment に保存され、これらの変数はプログラムの開始時に割り当てられ、プログラムの終了時に解放されます。

C++int globalVar = 10; // 全局变量,存储在数据段中

void foo() {
	static int staticVar = 20; // 静态变量,在数据段中
}

动态分配的变量(堆)

newmalloc などによって動的に割り当てられた変数は heap 上に保存されます。これらの変数のライフサイクルはプログラマーによって制御され、deletefree を使用して手動で解放する必要があります。

C++void foo() {
	int* p = new int(10); // p 指向的内存存储在堆上
	delete p; // 手动释放堆内存
}

常量(代码段或者只读数据段)

文字列リテラルやその他の定数は通常、text segment または read-only data segment に保存されます。

C++const char* str = "Hello World!"; // 字符串字面量存储在只读数据段中。

上記の紹介に基づくと、C++ における構造体やクラスオブジェクトの保存場所も、それらの宣言および割り当て方法に依存すると判断するのは難しくありません。以下はいくつかの一般的なケースとその保存場所です:

構造体やクラスオブジェクトがローカル変数として関数内で宣言された場合、それらは通常 stack 上に割り当てられます。例えば、

C++void foo() {
	struct MyStruct {
		int a;
		int b;
	};

	MyStruct s; // s 存储在栈上
}

全局变量或静态变量(数据段)

構造体やクラスオブジェクトがグローバル変数または静的変数として宣言された場合、それらは data segment に割り当てられます。例えば、

C++struct MyStruct {
	int a;
	int b;
};

MyStruct globalStruct; // globalStruct 在数据段中

void foo() {
	static MyStruct staticStruct;  // staticStruct 也在数据段中
}

动态分配(堆)

構造体やクラスオブジェクトが動的割り当て(例えば、new 演算子を使用)によって作成された場合、それらは heap 上に割り当てられます。例えば、

C++struct MyStruct {
	int a;
	int b;
};

void foo() {
	MyStruct *p = new MyStruct(); // p指向的对象在堆上
	delete p; // 手动释放内存
}

类成员

クラスオブジェクトが他のクラスや構造体をメンバーとして含んでいる場合、それらのメンバーの保存場所は、それらを含むオブジェクトの保存場所に依存します。例えば、

C++struct InnerStruct {
	int a;
	int b;
};

struct OuterStruct {
	InnerStruct inner;
};

void foo() {
	OuterStruct outer; // outer 和 outer.inner 都存储在栈上
	OuterStruct *p = new OuterStruct(); // p 指向的 outer 和 outer.inner 都存储在堆上
	delete p;
}

これらの規則は C++ におけるほとんどのケースに適用されます。ただし、カスタムの memory allocator を使用するなどの特殊なケースでは、保存場所が異なる場合があります。