C++の基礎 - 構造体

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

構造体は、異なるデータ型の要素をグループ化するためのユーザ定義データ型である。
これにより、関連するデータを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

プロジェクトの設定において、追加のコンパイラオプションを指定できる。

  1. プロジェクトを右クリックして、[プロジェクトを編集]を選択する。
  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"を追加する。