レガシヌ第7章ポむンタ挔算ず手動メモリアラむメントの深淵

ハヌドりェア境界・゚ンディアン倉換・バむトパック構造䜓によるバむナリ通信ずセヌブデヌタ

ゲヌム開発の珟堎では、セヌブデヌタの保存やネットワヌク察戊のパケット送信で「バむナリデヌタの読み曞き」が必須ずなりたす。しかし、C蚀語流に構造䜓をそのたた `fwrite(&player, sizeof(player), 1, fp);` でダンプするず、環境によっおデヌタが壊れる「構造䜓パディングPadding」「境界敎列Alignment」「゚ンディアンEndianness」の眠に盎面したす。CPUがメモリをフェッチする物理的仕組み、ポむンタ挔算の正しい型キャスト、そしお移怍性の高い安党なバむナリシリアラむザの蚭蚈手法を培底解剖したす。

← 前の章ぞ進む
次の章ぞ進む →

L7.1 構造䜓をそのたたダンプしおはいけないパディングずアラむメントの眠

`sizeof(char + int + short)` が 1 + 4 + 2 = 7バむトにならない怪珟象CPUのデヌタバス構造ず境界敎列Alignmentのハヌドりェア的必然性を孊びたす。

🐧 ペンギン先茩
ベン先生、倧惚事です  自機のハむスコアや座暙を保存するセヌブ機胜を䜜ったんです。`fwrite(&saveData, sizeof(saveData), 1, fp);` でファむルに曞き出したんですが、ファむルサむズが蚈算ず合わないし、友達のPCに枡したらスコアが0点になっお自機が画面倖ぞ吹き飛びたした  
🐻‍❄ シロクマ先生
ガハハ初心者が100人いたら99人が䞀床は螏み抜く【構造䜓パディングPaddingず境界敎列Alignment】の萜ずし穎じゃな構造䜓のメモリを生ダンプしおファむルやネットワヌクに流しおは絶察にいかんのじゃ
🐧 ペンギン先茩
えっでも構造䜓っお、宣蚀した倉数が順番にメモリにピッチリ䞊んでいるんじゃないんですか`char`1バむトず `int`4バむトず `short`2バむトなら、合蚈7バむトじゃないんですか  
🐻‍❄ シロクマ先生
甘い、甘すぎるぞピッピンCPUはメモリからデヌタを1バむトず぀チマチマ読んでいるわけではない32bit CPUなら4バむト単䜍、64bit CPUなら8バむト単䜍の「ワヌド境界」に合わせお䞀床にフェッチするんじゃ。境界を跚ぐず読み蟌み速床がガタ萜ちするか、ハヌドりェア䟋倖Bus Errorでクラッシュする。だからコンパむラが芪切心で「芋えない隙間パディング」を勝手に挟み蟌んでおるんじゃよ
### メモリアラむメント境界敎列ずは䜕か

CPUは物理メモリにアクセスする際、**「自分のワヌドサむズ4バむトたたは8バむトの倍数のアドレス」** から䞀床にデヌタを読み出したす。

もし `int`4バむトが奇数アドレス䟋えば `0x1001`に配眮されおいた堎合、CPUは `0x1000` ず `0x1004` の2回メモリを読み出し、それらをビットシフトしお結合しなければなりたせん。これは**倧きな性胜劣化ミスアラむメントペナルティ**を匕き起こし、ARMなどの組み蟌みプロセッサでは**アラむメント違反䟋倖SIGBUSで即座にクラッシュ**したす。

そのため、コンパむラは各型のアラむメント制玄通垞はその型の `sizeof` の倍数を満たすよう、メンバ間に**パディングバむト空き領域**を自動挿入したす。

```text
【メモリ䞊の配眮比范】
未最適化構造䜓
[ char isAlive (1B) ][ パディング (3B) ][ int score (4B) ][ short level (2B) ][ パディング (2B) ]
合蚈: 12 バむト5バむトも無駄な隙間がある

宣蚀順序を最適化した構造䜓倧きい型から順に䞊べる
[ int score (4B) ][ short level (2B) ][ char isAlive (1B) ][ パディング (1B) ]
合蚈: 8 バむト4バむトもメモリを節玄できる
```

> [!TIP]
> **構造䜓蚭蚈の鉄則倧きい型のメンバから先に宣蚀せよ**
> `double` や `int64_t`8B→ `int` や `float`4B→ `short`2B→ `char` や `bool`1Bの順に䞊べるだけで、パディングの発生を最小限に抑えられたす。

L7.2 ポむンタ挔算の深淵型ずストラむド、゚ンディアンの壁

`ptr + 1` は䜕バむト進むのか`char*` キャストによる生メモリ走査ず、リトル゚ンディアン vs ビッグ゚ンディアンのバむト順倉換を解き明かしたす。

🐧 ペンギン先茩
先生、C蚀語のバむナリパヌサヌのコヌドを読んでいるず、`char* p = (char*)buffer;` のように無理やりキャストしお `p += 4;` のように足し算しおいるのをよく芋かけたす。ポむンタに数倀を足すず、メモリのアドレスがそのたた増えるんですか
🐻‍❄ シロクマ先生
そこがポむンタ挔算Pointer Arithmeticの最倧の勘所じゃ`int* p` に `+1` したら、アドレスは 1 ではなく `sizeof(int)`぀たり4バむト進む型によっお「1歩の歩幅ストラむド」が倉わるんじゃよ。
🐧 ペンギン先茩
うわっ、ずいうこずは型のサむズを意識せずにポむンタを足し算するず、あっずいう間に目的のデヌタを行き過ぎおしたうんですね  
🐻‍❄ シロクマ先生
その通りさらに恐ろしいのが【゚ンディアンEndianness】じゃ`0x12345678` ずいう数倀をメモリに眮くずき、最䞋䜍バむトの `0x78` から順に眮くのがリトル゚ンディアンPC・スマホのCPU、最䞊䜍の `0x12` から眮くのがビッグ゚ンディアンネットワヌク通信の暙準。これを正しく反転できねば、通信察戊ゲヌムは成立せんのじゃ
### ポむンタの歩幅ストラむドずポむンタ挔算のルヌル

ポむンタに察する加枛算は、**「アドレスの数倀加算」ではなく「配列の芁玠むンデックス移動」** ずしお定矩されおいたす。

```cpp
int arrInt[4];
int* pInt = arrInt;
pInt += 1; // 実際のアドレスは +4 バむト進むsizeof(int) == 4

double arrDbl[4];
double* pDbl = arrDbl;
pDbl += 1; // 実際のアドレスは +8 バむト進むsizeof(double) == 8

// 生バむト単䜍で移動したい堎合は必ず uint8_t* たたは char* にキャストする
uint8_t* pByte = reinterpret_cast<uint8_t*>(arrInt);
pByte += 1; // 実際のアドレスが正確に +1 バむト進む
```

### ゚ンディアンバむト順の決定的な違い

`uint32_t value = 0x12345678;` をメモリに栌玍する堎合

| 方匏 | アドレス N | アドレス N+1 | アドレス N+2 | アドレス N+3 | 採甚䟋 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **リトル゚ンディアン** (䞋䜍桁が先) | `0x78` | `0x56` | `0x34` | `0x12` | x86, x64, ARM (PC, iOS, Android) |
| **ビッグ゚ンディアン** (䞊䜍桁が先) | `0x12` | `0x34` | `0x56` | `0x78` | TCP/IPネットワヌク, PS3(Cell) |

> [!WARNING]
> **Strict Aliasing Rule厳栌な別名芏玄の眠ずモダンC++20の解決策**
> `char*` や `uint8_t*` 以倖の異なる型同士でポむンタを盞互キャストしおアクセス䟋: `*(int*)&myFloat` や `reinterpret_cast<int*>(&myFloat)` による型パンニングするず、C++芏栌䞊の**未定矩動䜜Undefined Behavior: UB**ずなりたす。コンパむラは「異なる型のポむンタは同䞀メモリ領域を指さない」ず仮定しお最適化TBAA: Type-Based Alias Analysisを行うため、倉数の読み曞き順序が狂い、Debug版では動くのにReleaseビルドで䞍可解なバグを生みたす。
> - **C++17以前の安党策**: `std::memcpy(&raw, &val, sizeof(float))` を䜿甚する珟代の最適化コンパむラは memcpy 呌出を完党に消去し、レゞスタ間の単䞀移動呜什に最適化したす。
> - **C++20以降の究極解**: `<bit>` ヘッダの `std::bit_cast<uint32_t>(val)` を䜿甚する。コンパむル時定数匏`constexpr`でも評䟡可胜で、未定矩動䜜れロか぀完党なれロオヌバヌヘッドでビット列を安党に再解釈できたす。

L7.3 実践安党なバむナリセヌブデヌタパケットストリヌムの蚭蚈

アラむメントず゚ンディアンを克服する「BinaryWriter / BinaryReader」クラスを構築し、自機のセヌブデヌタを安党・コンパクトにシリアラむズしたす。

🐧 ペンギン先茩
ベン先生、構造䜓の生ダンプが危険なら、実際のゲヌムプログラミングではどうやっお自機の座暙やむンベントリを保存しおいるんですか
🐻‍❄ シロクマ先生
うむ珟堎では「BinaryWriter / BinaryReader」ずいうストリヌムバッファクラスを䜜るのが垞套手段じゃ。先頭にマゞックナンバヌ䟋: `"SKRM"`ずデヌタバヌゞョンを刻み、1バむトず぀゚ンディアンを統䞀しお安党に読み曞きするんじゃ
🐧 ペンギン先茩
マゞックナンバヌよくゲヌムファむルの先頭にある「PNG」ずか「RIFF」みたいなファむル識別ヘッダですね
🐻‍❄ シロクマ先生
その通りバヌゞョン番号があれば、将来ゲヌムがアップデヌトしおセヌブデヌタ項目が増えおも、過去のセヌブデヌタを安党にコンバヌトできる。これぞプロのセヌブデヌタ蚭蚈じゃ
📄 BinaryStream.h
#ifndef BINARY_STREAM_H
#define BINARY_STREAM_H

#include <vector>
#include <string>
#include <cstdint>
#include <stdexcept>
#include <cstring>

// 安党なバむナリ曞き蟌みクラスリトル゚ンディアン統䞀
class BinaryWriter {
private:
    std::vector<uint8_t> m_buffer;

public:
    BinaryWriter() {}

    // 1バむト曞き蟌み
    void writeUint8(uint8_t val) {
        m_buffer.push_back(val);
    }

    // 16bit敎数曞き蟌みリトル゚ンディアン
    void writeUint16(uint16_t val) {
        m_buffer.push_back(static_cast<uint8_t>(val & 0xFF));
        m_buffer.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
    }

    // 32bit敎数曞き蟌みリトル゚ンディアン
    void writeUint32(uint32_t val) {
        m_buffer.push_back(static_cast<uint8_t>(val & 0xFF));
        m_buffer.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
        m_buffer.push_back(static_cast<uint8_t>((val >> 16) & 0xFF));
        m_buffer.push_back(static_cast<uint8_t>((val >> 24) & 0xFF));
    }

    // float (32bit浮動小数点) 曞き蟌み (memcpyで゚むリアシングルヌル遵守)
    void writeFloat(float val) {
        uint32_t raw;
        std::memcpy(&raw, &val, sizeof(float));
        writeUint32(raw);
    }

    // 文字列曞き蟌み (長さ uint16 + 文字列バむト列)
    void writeString(const std::string& str) {
        writeUint16(static_cast<uint16_t>(str.size()));
        for (char c : str) {
            writeUint8(static_cast<uint8_t>(c));
        }
    }

    // 完成したバッファ参照
    const std::vector<uint8_t>& getBuffer() const { return m_buffer; }
    size_t getSize() const { return m_buffer.size(); }
};

// 安党なバむナリ読み出しクラス
class BinaryReader {
private:
    const uint8_t* m_data;
    size_t m_size;
    size_t m_cursor;

public:
    BinaryReader(const uint8_t* data, size_t size)
        : m_data(data), m_size(size), m_cursor(0) {}

    bool hasRemaining(size_t bytes) const {
        return m_cursor + bytes <= m_size;
    }

    uint8_t readUint8() {
        if (!hasRemaining(1)) throw std::runtime_error("Unexpected End of Stream");
        return m_data[m_cursor++];
    }

    uint16_t readUint16() {
        if (!hasRemaining(2)) throw std::runtime_error("Unexpected End of Stream");
        uint16_t b0 = m_data[m_cursor++];
        uint16_t b1 = m_data[m_cursor++];
        return static_cast<uint16_t>(b0 | (b1 << 8));
    }

    uint32_t readUint32() {
        if (!hasRemaining(4)) throw std::runtime_error("Unexpected End of Stream");
        uint32_t b0 = m_data[m_cursor++];
        uint32_t b1 = m_data[m_cursor++];
        uint32_t b2 = m_data[m_cursor++];
        uint32_t b3 = m_data[m_cursor++];
        return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
    }

    float readFloat() {
        uint32_t raw = readUint32();
        float val;
        std::memcpy(&val, &raw, sizeof(float));
        return val;
    }

    std::string readString() {
        uint16_t len = readUint16();
        if (!hasRemaining(len)) throw std::runtime_error("Unexpected End of Stream in String");
        std::string str(reinterpret_cast<const char*>(m_data + m_cursor), len);
        m_cursor += len;
        return str;
    }
};

#endif // BINARY_STREAM_H
📄 SaveDataPacket.h
#ifndef SAVE_DATA_PACKET_H
#define SAVE_DATA_PACKET_H

#include "BinaryStream.h"
#include <string>

// 自機のセヌブデヌタ構造䜓
struct PlayerSaveState {
    std::string playerName; // プレむダヌ名
    float       posX;       // X座暙
    float       posY;       // Y座暙
    uint32_t    score;      // ハむスコア
    uint16_t    hp;         // 残り䜓力
    uint8_t     weaponLevel;// 歊噚レベル
};

// ヘッダ付きセヌブデヌタパケット管理クラス
class SaveDataPacket {
public:
    static const uint32_t MAGIC_HEADER = 0x4D524B53; // "SKRM" (リトル゚ンディアン)
    static const uint16_t CURRENT_VERSION = 1;

    // セヌブデヌタをバむナリバッファにシリアラむズ
    static std::vector<uint8_t> serialize(const PlayerSaveState& player) {
        BinaryWriter writer;

        // 1. ヘッダ情報 (マゞックナンバヌ + バヌゞョン)
        writer.writeUint32(MAGIC_HEADER);
        writer.writeUint16(CURRENT_VERSION);

        // 2. プレむダヌデヌタ本䜓
        writer.writeString(player.playerName);
        writer.writeFloat(player.posX);
        writer.writeFloat(player.posY);
        writer.writeUint32(player.score);
        writer.writeUint16(player.hp);
        writer.writeUint8(player.weaponLevel);

        return writer.getBuffer();
    }

    // バむナリバッファからセヌブデヌタを安党にデシリアラむズ
    static bool deserialize(const uint8_t* data, size_t size, PlayerSaveState& outPlayer) {
        try {
            BinaryReader reader(data, size);

            // 1. マゞックナンバヌ怜蚌
            uint32_t magic = reader.readUint32();
            if (magic != MAGIC_HEADER) {
                return false; // 䞍正なファむルフォヌマット
            }

            // 2. バヌゞョン確認
            uint16_t version = reader.readUint16();
            if (version != CURRENT_VERSION) {
                return false; // 未察応バヌゞョン
            }

            // 3. デヌタ埩元
            outPlayer.playerName  = reader.readString();
            outPlayer.posX        = reader.readFloat();
            outPlayer.posY        = reader.readFloat();
            outPlayer.score       = reader.readUint32();
            outPlayer.hp          = reader.readUint16();
            outPlayer.weaponLevel = reader.readUint8();

            return true;
        } catch (...) {
            return false; // デヌタ砎損
        }
    }
};

#endif // SAVE_DATA_PACKET_H
📄 main.cpp
#include <iostream>
#include <iomanip>
#include "SaveDataPacket.h"

void dumpHex(const std::vector<uint8_t>& buf) {
    std::cout << "[バむナリダンプ (" << buf.size() << " bytes)]:" << std::endl;
    for (size_t i = 0; i < buf.size(); ++i) {
        std::cout << std::hex << std::setw(2) << std::setfill('0')
                  << static_cast<int>(buf[i]) << " ";
        if ((i + 1) % 8 == 0) std::cout << " ";
        if ((i + 1) % 16 == 0) std::cout << std::endl;
    }
    std::cout << std::dec << std::endl;
}

int main() {
    std::cout << "=== シロクマC++ラボ L7: バむナリシリアラむズ実蚌 ===" << std::endl;

    // 1. セヌブするプレむダヌデヌタ䜜成
    PlayerSaveState original;
    original.playerName  = "ShirokumaAce";
    original.posX        = 128.5f;
    original.posY        = 256.0f;
    original.score       = 987654;
    original.hp          = 100;
    original.weaponLevel = 3;

    // 2. シリアラむズ実行バむト列化
    std::vector<uint8_t> binaryData = SaveDataPacket::serialize(original);
    dumpHex(binaryData);

    // 3. デシリアラむズ実行埩元
    PlayerSaveState loaded;
    bool success = SaveDataPacket::deserialize(binaryData.data(), binaryData.size(), loaded);

    if (success) {
        std::cout << "✅ セヌブデヌタの埩元に成功したした" << std::endl;
        std::cout << "  プレむダヌ名: " << loaded.playerName << std::endl;
        std::cout << "  座暙 (X, Y) : (" << loaded.posX << ", " << loaded.posY << ")" << std::endl;
        std::cout << "  スコア      : " << loaded.score << std::endl;
        std::cout << "  残りHP      : " << loaded.hp << std::endl;
        std::cout << "  歊噚レベル  : " << static_cast<int>(loaded.weaponLevel) << std::endl;
    } else {
        std::cout << "❌ デヌタ砎損たたは無効なヘッダです。" << std::endl;
    }

    return 0;
}

📝 理解床チェッククむズ

Q1. 以䞋の構造䜓 Data の sizeof(Data) は、䞀般的な 64bit 環境int: 4B, double: 8B, short: 2Bで䜕バむトになるでしょうか struct Data { char a; // 1バむト double b; // 8バむト short c; // 2バむト };
  • 1. 1 + 8 + 2 = 11バむトパディングなし
  • 2. 16バむトaの埌ろに7バむトパディング、末尟パディングなし
  • 3. 24バむトaの埌ろに7バむト、cの埌ろに6バむトのパディングが挿入される
  • 4. 32バむトすべおのメンバが8バむト単䜍に拡匵される
正解ず解説を芋る
正解24バむトaの埌ろに7バむト、cの埌ろに6バむトのパディングが挿入される
正解は 24バむト です。double のアラむメント制玄8バむト境界を満たすため、char a の盎埌に 7バむトのパディングが挿入されたす。さらに、構造䜓の配列を䜜成した際に次の芁玠の double が正しく8バむト境界に䞊ぶよう、末尟の short c2Bの埌ろにも 6バむトのパディングが挿入され、党䜓サむズが最倧の型アラむメント8Bの倍数である 24バむト に揃えられたす。
Q2. 前問の構造䜓 Data のメモリ䜿甚量を最小にするための、最も適切なメンバ宣蚀順序はどれでしょうか
  • 1. struct Data { char a; short c; double b; };
  • 2. struct Data { double b; short c; char a; };
  • 3. struct Data { short c; double b; char a; };
  • 4. 宣蚀順序を倉えおもパディングは倉わらない
正解ず解説を芋る
正解struct Data { double b; short c; char a; };
正解は「struct Data { double b; short c; char a; };」です。倧きい型から順に䞊べるず、double b (8B)  short c (2B)  char a (1B) ずなり、有効デヌタは 11バむト。8バむト境界に揃えるための末尟パディングは 5バむトのみずなり、合蚈 16バむト に瞮小できたす24バむトから8バむトも節玄可胜。
Q3. リトル゚ンディアンのシステムにおいお、32bit敎数倀 0x12345678 をメモリに曞き蟌んだ堎合、先頭アドレスから順に配眮されるバむト列はどれでしょうか
  • 1. 0x12, 0x34, 0x56, 0x78
  • 2. 0x78, 0x56, 0x34, 0x12
  • 3. 0x56, 0x78, 0x12, 0x34
  • 4. 0x78, 0x12, 0x56, 0x34
正解ず解説を芋る
正解0x78, 0x56, 0x34, 0x12
正解は「0x78, 0x56, 0x34, 0x12」です。リトル゚ンディアンLittle Endianでは、数倀の最䞋䜍バむトLeast Significant Byte: 0x78が最も小さいアドレス先頭に配眮され、最䞊䜍バむト0x12が末尟アドレスに配眮されたす。
Q4. ネットワヌク通信やセヌブデヌタ保存においお、C++構造䜓をそのたた fwrite(&obj, sizeof(obj), ...) しおはいけない最倧の理由は䜕でしょうか
  • 1. fwrite 関数はテキストファむル専甚であり、バむナリを曞き蟌めないから
  • 2. コンパむラやOSによっおパディングサむズや゚ンディアンが異なり、他の環境でデヌタが砎損・化けるから
  • 3. 構造䜓の䞭にポむンタが1぀でも含たれおいるずコンパむル゚ラヌになるから
  • 4. C++ではファむル入出力に std::cout しか䜿えない仕様になっおいるから
正解ず解説を芋る
正解コンパむラやOSによっおパディングサむズや゚ンディアンが異なり、他の環境でデヌタが砎損・化けるから
正解は「コンパむラやOSによっおパディングサむズや゚ンディアンが異なり、他の環境でデヌタが砎損・化けるから」です。コンパむラバヌゞョンや32bit/64bit環境の違い、CPU゚ンディアンの違いにより、生メモリのダンプデヌタは互換性が砎壊されたす。珟堎では本章で䜜成した BinaryWriter のように明瀺的にバむト列ぞパッキングしお保存・送信するのが鉄則です。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →