C++の基礎 - 構造体
概要
構造体は、異なるデータ型の要素をグループ化するためのユーザ定義データ型である。
これにより、関連するデータを1つの単位として扱うことができる。
C++における構造体の特徴として、以下に示すものが挙げられる。
C言語とは異なることに注意する。
- メンバアクセス
- メンバには、
.
演算子でアクセスする。 - 構造体へのポインタを使用する場合、
->
演算子でメンバにアクセスする。
- メンバには、
- デフォルトはpublic
- クラスと異なり、構造体のメンバはデフォルトで
public
である。
- クラスと異なり、構造体のメンバはデフォルトで
- 継承可能
- 構造体は他の構造体やクラスを継承することができる。
構文
struct <構造体名> {
<データ型1> <メンバ1>;
<データ型2> <メンバ2>;
// ...
};
// 例:
struct Person {
std::string name;
int age;
double height;
};
int main()
{
Person p1;
p1.name = "田中太郎";
p1.age = 30;
p1.height = 175.5;
}
構造体の初期化
C++11以降では、一様初期化構文を使用することができる。
struct Point {
int x,
y;
};
Point p1 = {10, 20}; // 従来の初期化
Point p2 {30, 40}; // 一様初期化
Point p3 = Point{50, 60}; // コンストラクタ形式の一様初期化
ビットフィールド
メモリの使用を最適化するために、構造体内でビットフィールドが使用できる。
これは、個々のビットレベルでデータを管理することができる。
このようなビットフィールドの使用は、メモリが制限されている環境やハードウェアレジスタと直接やり取りする必要がある場合等、
効率的なメモリ使用が重要な場面で特に有用である。
struct Flags {
unsigned int flag1 : 1; // 1[bit]のフィールドとして定義されており、本質的にブール値 (0 または 1) が格納できる
unsigned int flag2 : 1; // 1[bit]のフィールドとして定義されており、本質的にブール値 (0 または 1) が格納できる
unsigned int data : 6; // 6[bit]のフィールドとして定義されており、0 から 63 (2^6 - 1) の範囲の値が格納できる
};
Flags f;
f.flag1 = 1; // フラグをオンに設定
f.flag2 = 0; // フラグをオフに設定
f.data = 42; // 6ビットの範囲内の値を設定
if (f.flag1) {
// flag1 がオンの場合の処理
}
// data の値を出力 (0 - 63の範囲)
std::cout << "Data value: " << f.data << std::endl;
この構造体の特徴とメリットを以下に示す。
- メモリ効率
- 一般的に、unsigned int型は32[bit] (4[byte]) を使用する。
- この構造体は合計で8[ビット] (1[byte]) しか使用しない。
- 結果として、メモリ使用量が75[%]削減される。
- パッキング
- コンパイラは、これらのビットフィールドを1つのunsigned int型にパックする。
- アクセス
- これらのフィールドは、通常の構造体のメンバのようにアクセスできるが、値は自動的に適切なビット数に制限される。
構造体ポインタ
構造体へのポインタを使用する場合、->
演算子でメンバにアクセスする。
Person* pPtr = &p1;
pPtr->name = "鈴木花子";
構造体の配列
構造体の配列を作成して、複数のデータセットを管理することができる。
Person people[3] = {
{"山田一郎", 25, 170.0},
{"佐藤二郎", 35, 180.0},
{"高橋三郎", 45, 165.5}
};
メソッド
C++では、構造体内にメソッドを定義することもできる。
struct Rectangle {
double width;
double height;
double area() {
return width * height;
}
};
匿名構造体
C++11以降では、構造体に名前のない入れ子構造体を定義することができる。
struct OuterStruct {
int x;
struct {
int y;
int z;
};
};
OuterStruct os;
os.x = 1;
os.y = 2; // 直接アクセス可能
構造体のムーブセマンティクス
C++11以降では、構造体にムーブコンストラクタとムーブ代入演算子を定義することができる。
struct Resource {
std::unique_ptr<int> data;
Resource(Resource&& other) noexcept : data(std::move(other.data))
{}
Resource& operator=(Resource&& other) noexcept
{
if (this != &other) {
data = std::move(other.data);
}
return *this;
}
};
メモリアライメント
メモリアライメントとは
構造体のメモリレイアウトは、コンパイラによりパディングが挿入される場合がある。
これは、メモリアクセスの効率化のためである。
以下の例では、パディングのため、実際の構造体のサイズは12[バイト]になる可能性がある。
ただし、#pragma pack
ディレクティブを使用して、アラインメントを制御することができる。
struct AlignmentExample {
char c; // 1 バイト
int i; // 4 バイト
short s; // 2 バイト
};
メモリアライメントの制御
以下に示す方法により、構造体のアライメントを制御することができる。
※注意 1
Clangは、以下に示す全ての方法をサポートしている。
ただし、特定のターゲットプラットフォームやClangのバージョンにより、サポートされる機能や動作が若干異なる可能性があることに注意する。
※注意 2
- アライメントの変更は、プラットフォーム依存の問題を引き起こす可能性がある。
- 一部のCPUアーキテクチャでは、アライメントされていないメモリアクセスがパフォーマンスの低下やクラッシュを引き起こす可能性がある。
- クロスプラットフォーム開発を行う場合は、異なるコンパイラやプラットフォームでの動作を確認する必要がある。
アライメントの変更が必要かどうかを検討して、可能な限り標準的なアライメントを使用することを推奨する。
GNU C/C++の属性を使用する方法
GCC / Clangで動作する。
struct __attribute__((packed)) MyStruct {
// 構造体のメンバ
};
プラグマを使用する方法
GCC / Clangで動作する。
#pragma pack(push, 1)
struct MyStruct {
// 構造体のメンバ
};
#pragma pack(pop)
Clang特有の属性を使用する方法
Clangのみ動作する。
struct [[gnu::packed]] MyStruct {
// 構造体のメンバ
};
C++11以降の標準属性を使用する方法
GCC / Clangで動作する。
struct alignas(1) MyStruct {
// 構造体のメンバ
};
※注意
__attribute__((packed))
や#pragma pack(1)
を使用する場合、構造体全体が1バイトアライメントになる。alignas(1)
を使用する場合、構造体全体のアライメントは1バイトになるが、個々のメンバは自然なアライメントを保持する。
完全に詰めたパッキングが必要な場合は、__attribute__((packed))
や#pragma pack(1)
の方が適している。- クロスプラットフォーム開発を行う場合は、異なるコンパイラやプラットフォームでの互換性を確認することが重要である。
- アライメントの変更は、特に必要な場合にのみ行うべきである。
不適切なアライメントは、パフォーマンスの低下や一部のアーキテクチャでは問題を引き起こす可能性がある。
Linux固有の情報
Linuxシステムにおいて、多くのシステムコールやライブラリ関数は構造体を使用してデータを受け渡しする。
例えば、struct statはファイル情報を格納するために使用される。
#include <sys/stat.h>
struct stat file_info;
if (stat("example.txt", &file_info) == 0) {
// file_infoを使ってファイル情報にアクセス
}
nullptrの代替
std::optional
を使用することにより、nullptr
のような振る舞いを実現することができる。
ただし、C++17以降で利用可能である。
この記述は、以下に示すようなメリットがある。
- 関数が値を持っているかどうかを明示的に示すことができる。
- 呼び出し側において、値の存在を確認した後に使用することを強制することができる。
#include <string>
#include <vector>
#include <optional>
struct HOGE {
int i;
double f;
bool b;
std::string str;
};
class SomeClass
{
private:
std::vector<HOGE> m_hoges;
public:
std::optional<HOGE> getHoge() const
{
if (m_hoges.empty()) {
return std::nullopt;
}
return m_hoges[0];
}
};
// 使用例
void someFunction()
{
SomeClass obj;
auto result = obj.getHoge();
if (result.has_value()) {
HOGE hoge = result.value();
// ...略
}
else {
// データが存在しない場合の処理
// ...略
}
}
また、C++17のif文と構造化束縛を使用して、より簡潔に記述することもできる。
if (auto hoge = obj.getHoge(); hoge) {
// *hogeを使用する処理
}
else {
// データが存在しない場合の処理
}
応用例
これらの高度な概念は、C++での構造体の強力さと柔軟性を示している。
構造体は、単純なデータグループ化から複雑なシステムプログラミングまで、幅広い用途に活用できる。
データシリアライゼーション
構造体を使用して、データをバイナリ形式でシリアライズすることができる。
#include <fstream>
#include <cstring>
struct Record {
int id;
char name[50];
double salary;
};
void saveRecord(const Record& r, const std::string& filename)
{
std::ofstream file(filename, std::ios::binary);
file.write(reinterpret_cast<const char*>(&r), sizeof(Record));
}
Record loadRecord(const std::string& filename)
{
Record r;
std::ifstream file(filename, std::ios::binary);
file.read(reinterpret_cast<char*>(&r), sizeof(Record));
return r;
}
メモリマッピングファイル (Linux)
構造体を使用して、メモリマッピングファイルの内容を直接操作することができる。
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
struct SharedData {
int value;
char message[256];
};
int main()
{
int fd = open("shared_memory.bin", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
ftruncate(fd, sizeof(SharedData));
SharedData* data = static_cast<SharedData*>(mmap(NULL, sizeof(SharedData),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
data->value = 42;
strcpy(data->message, "Hello, shared memory!");
munmap(data, sizeof(SharedData));
close(fd);
return 0;
}
ネットワークプロトコル
構造体を使用して、カスタムネットワークプロトコルを定義することができる。
#include <arpa/inet.h>
struct PacketHeader {
uint32_t magic;
uint16_t version;
uint16_t type;
uint32_t length;
void toNetworkOrder()
{
magic = htonl(magic);
version = htons(version);
type = htons(type);
length = htonl(length);
}
void toHostOrder()
{
magic = ntohl(magic);
version = ntohs(version);
type = ntohs(type);
length = ntohl(length);
}
};
IDEでの設定
Qt CreatorおよびCLionでは、直接アライメントを制御する設定は提供していない。
アライメントの調整は主にコンパイラレベルでの操作となる。
しかし、これらのIDEを使用してプロジェクトの設定を調整することにより、間接的にアライメントを制御することは可能である。
Qt CreatorおよびCLionにおいて、アライメントに関連する設定を行う方法を、以下に示す。
※注意
これらの設定はプロジェクト全体に適用される。
特定の構造体だけにアライメントを適用する場合は、ソースコード内で直接属性やプラグマを使用する方がよい。
これにより、より細かい制御が可能になり、意図しない副作用を避けることができる。
-fpack-struct
フラグは全ての構造体を1バイトアライメントにする。
これは広範囲に影響を与える可能性があるため、慎重に使用する必要がある。
クロスプラットフォーム開発を行う場合、これらの設定が全てのターゲットプラットフォームで同じように動作するとは限らない。
アライメントの調整について、IDEの設定を通じて行うよりも、ソースコード内で直接制御する方が一般的で安全である。
Qt Creator
方法 1
Qt Creatorでは、プロジェクトファイル (.pro) にコンパイラフラグを追加することができる。
-fpack-struct
フラグは、構造体のパッキングを有効にする。
QMAKE_CXXFLAGS += -fpack-struct
方法 2
プロジェクトの設定において、追加のコンパイラオプションを指定できる。
- プロジェクトを右クリックして、[プロジェクトを編集]を選択する。
- [ビルド設定] - [追加の引数]にコンパイラフラグを追加する。
CLion
方法 1
CLionでは、CMakeをビルドシステムとして使用しているため、CMakeLists.txtファイルに変数CMAKE_CXX_FLAGS
に-fpack-struct
フラグを設定する。
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpack-struct")
方法 2
[ファイル]メニューバー - [設定]を選択する。
[ビルド、実行、デプロイ] - [CMake] - [CMake options]項目に、-DCMAKE_CXX_FLAGS="-fpack-struct"
を追加する。