Skip to content

🧠 Tư Duy Bộ Nhớ: Stack vs Heap Module 1

🎯 Mục tiêu

Sau bài học này, bạn sẽ:

  • Hiểu bố cục bộ nhớ (memory layout) của một tiến trình C++
  • Phân biệt rõ ràng StackHeap — khi nào dùng cái nào
  • Nắm được cơ chế hoạt động của stack frame và heap allocation
  • Sử dụng sizeof, alignof, alignas một cách chính xác
  • Tránh được các lỗi bộ nhớ phổ biến: stack overflow, memory leak, padding bất ngờ

1. Tại Sao Bộ Nhớ Quan Trọng?

C++ — Ngôn Ngữ Của Sự Kiểm Soát

Khi bạn viết Python hay Java, runtime sẽ lo việc cấp phát và thu hồi bộ nhớ cho bạn thông qua Garbage Collector (GC). Bạn new thoải mái, không cần delete — GC sẽ dọn dẹp.

C++ thì khác. Bạn chính là Garbage Collector.

Điều này mang lại hai mặt:

Python/JavaC++
Cấp phát bộ nhớRuntime tự quản lýLập trình viên kiểm soát
Thu hồi bộ nhớGC tự độngLập trình viên chịu trách nhiệm
Hiệu năngGC pause (stop-the-world)Deterministic, zero overhead
Rủi roThấp (GC bảo vệ)Cao (memory leak, dangling pointer)
Phù hợp choWeb app, scriptingGame engine, HFT, embedded

Câu Chuyện Thực Tế: Memory Leak Trong Fintech

💼 Production Story

Một hệ thống giao dịch tần suất cao (HFT) tại một công ty fintech xử lý 50,000 lệnh/giây. Mỗi lệnh cấp phát một object Order trên heap — chỉ 64 bytes.

Nếu quên delete chỉ 1% số lệnh:

  • 500 lệnh × 64 bytes = 32 KB/giây bị leak
  • 1 phút = 1.92 MB
  • 1 giờ = 115 MB
  • 8 giờ giao dịch = 920 MB — hệ thống bắt đầu swap, latency tăng vọt

Kết quả? Lệnh bị trễ → mất cơ hội giao dịch → mất tiền thật.

Đây là lý do tại sao hiểu bộ nhớ C++ không phải là "nice to have" — nó là bắt buộc.

Tư Duy Đúng Đắn

Trước khi viết bất kỳ dòng code C++ nào, hãy luôn tự hỏi:

"Biến này sống ở đâu? Sống bao lâu? Ai chịu trách nhiệm dọn dẹp nó?"

Ba câu hỏi này sẽ theo bạn suốt sự nghiệp lập trình C++. Hãy bắt đầu bằng việc hiểu nơi mà dữ liệu có thể sống.


2. Bố Cục Bộ Nhớ Của Một Tiến Trình C++

Khi một chương trình C++ được nạp vào bộ nhớ (RAM), hệ điều hành chia không gian địa chỉ thành các segment (phân đoạn) rõ ràng:

    ┌──────────────────────────────┐  0xFFFF...  (Địa chỉ cao)
    │                              │
    │          STACK               │  ← Lớn dần xuống dưới (grows ↓)
    │   (biến cục bộ, tham số,    │
    │    địa chỉ trả về)          │
    │                              │
    │          ↓ ↓ ↓               │
    ├──────────────────────────────┤
    │                              │
    │      VÙNG TRỐNG (FREE)       │  ← Stack và Heap tiến lại gần nhau
    │                              │
    ├──────────────────────────────┤
    │          ↑ ↑ ↑               │
    │                              │
    │          HEAP                │  ← Lớn dần lên trên (grows ↑)
    │   (new, malloc, smart ptr)   │
    │                              │
    ├──────────────────────────────┤
    │          BSS Segment         │  ← Biến toàn cục chưa khởi tạo
    │   (uninitialized globals)    │     (tự động = 0)
    ├──────────────────────────────┤
    │          Data Segment        │  ← Biến toàn cục đã khởi tạo
    │   (initialized globals,      │     + hằng số static
    │    static variables)         │
    ├──────────────────────────────┤
    │          Text Segment        │  ← Mã máy (machine code)
    │   (read-only, executable)    │     Chương trình của bạn nằm đây
    └──────────────────────────────┘  0x0000...  (Địa chỉ thấp)

Giải Thích Từng Segment

SegmentChứa gìĐặc điểm
TextMã máy (compiled code)Read-only, shared giữa các process
DataBiến toàn cục/static đã khởi tạoTồn tại suốt đời chương trình
BSSBiến toàn cục/static chưa khởi tạoTự động zero-filled bởi OS
HeapBộ nhớ cấp phát động (new/malloc)Lập trình viên quản lý
StackBiến cục bộ, tham số hàm, return addressTự động quản lý bởi compiler
cpp
#include <iostream>

// === DATA segment (initialized globals) ===
int g_maxConnections = 100;
static double g_pi = 3.14159;

// === BSS segment (uninitialized globals) ===
int g_counter;                 // tự động = 0
static char g_buffer[1024];    // tự động zero-filled

// === TEXT segment (machine code) ===
void processOrder(int orderId) {
    // === STACK (local variables) ===
    int quantity = 10;
    double price = 99.5;

    // === HEAP (dynamic allocation) ===
    int* data = new int[quantity];

    // ... xử lý ...

    delete[] data;  // PHẢI giải phóng!
}

💡 Mẹo Ghi Nhớ

Nghĩ về bộ nhớ như một tòa nhà:

  • Text = Bản thiết kế (blueprint) — không ai sửa được
  • Data/BSS = Kho chung tầng trệt — ai cũng truy cập được, tồn tại mãi
  • Heap = Kho thuê — bạn thuê (new) thì phải trả (delete)
  • Stack = Bàn làm việc — tự dọn khi bạn rời phòng (hàm return)

3. Stack — Bộ Nhớ Tự Động

3.1 Stack Hoạt Động Như Thế Nào?

Stack hoạt động theo nguyên tắc LIFO (Last In, First Out) — giống như một chồng đĩa. Đĩa đặt lên cuối cùng sẽ được lấy ra đầu tiên.

Mỗi khi một hàm được gọi, compiler tạo một stack frame chứa:

  • Tham số (parameters) của hàm
  • Biến cục bộ (local variables)
  • Địa chỉ trả về (return address)
  • Con trỏ frame cũ (saved frame pointer)
cpp
int multiply(int a, int b) {
    int result = a * b;   // result nằm trên stack
    return result;
}

int calculate(int x) {
    int doubled = x * 2;           // doubled trên stack
    int answer = multiply(doubled, 3); // gọi multiply → push frame mới
    return answer;
}

int main() {
    int value = 5;                 // value trên stack
    int output = calculate(value); // gọi calculate → push frame mới
    std::cout << output << "\n";   // 30
    return 0;
}

3.2 Minh Họa Stack Frame

Khi multiply(10, 3) đang thực thi, stack trông như thế này:

    ┌─────────────────────────┐
    │  multiply() frame       │  ← Stack Pointer (SP) — đỉnh stack
    │  ├─ result = 30         │
    │  ├─ b = 3               │
    │  ├─ a = 10              │
    │  └─ return address      │
    ├─────────────────────────┤
    │  calculate() frame      │
    │  ├─ answer = ???        │  (chưa gán, đang chờ multiply trả về)
    │  ├─ doubled = 10        │
    │  ├─ x = 5               │
    │  └─ return address      │
    ├─────────────────────────┤
    │  main() frame           │
    │  ├─ output = ???        │  (chưa gán)
    │  ├─ value = 5           │
    │  └─ return address      │
    ├─────────────────────────┤
    │  ... (OS startup code)  │
    └─────────────────────────┘

Khi multiply return:

  1. Giá trị trả về (30) được copy vào thanh ghi
  2. Stack frame của multiply bị pop (xóa) — chỉ cần di chuyển stack pointer lên
  3. Tất cả biến cục bộ của multiply biến mất ngay lập tức

⚡ Ghi Chú Hiệu Năng

Cấp phát trên stack chỉ tốn ~1 lệnh CPU — chỉ cần di chuyển stack pointer (thanh ghi RSP trên x86-64).

So sánh:

  • Stack allocation: ~1 nanosecond (di chuyển con trỏ)
  • Heap allocation: ~100-1000 nanoseconds (gọi OS, tìm vùng trống, cập nhật bảng quản lý)

Stack nhanh hơn heap 100-1000 lần cho việc cấp phát. Đây là lý do tại sao bạn nên ưu tiên stack bất cứ khi nào có thể.

3.3 Stack Overflow — Khi Stack Tràn

Stack có kích thước giới hạn — thường là 1-8 MB (tùy hệ điều hành và cấu hình).

Hai nguyên nhân phổ biến gây stack overflow:

Nguyên nhân 1: Đệ quy quá sâu

cpp
// ⚠️ NGUY HIỂM: Stack overflow với n lớn
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // Mỗi lần gọi = 1 stack frame mới
}

int main() {
    // factorial(10) → OK, 10 stack frames
    // factorial(100000) → 💥 STACK OVERFLOW!
    //   100,000 frames × ~64 bytes/frame ≈ 6.4 MB > stack size
    std::cout << factorial(100000) << "\n";
    return 0;
}

Nguyên nhân 2: Mảng cục bộ quá lớn

⚠️ Cạm bẫy

cpp
void processImage() {
    // ❌ SAI: 40 MB trên stack → instant crash!
    int pixels[10'000'000];

    // ✅ ĐÚNG: Dùng heap thông qua vector
    std::vector<int> pixels(10'000'000);  // heap allocation bên trong

    // ✅ CŨNG ĐÚNG: Dùng smart pointer nếu cần raw array
    auto pixels2 = std::make_unique<int[]>(10'000'000);
}

Quy tắc ngón tay cái: Nếu mảng cục bộ > 1 KB, hãy cân nhắc dùng std::vector hoặc heap allocation.

3.4 Ưu Điểm Và Hạn Chế Của Stack

Ưu điểm ✅Hạn chế ❌
Cấp phát cực nhanh (~1 ns)Kích thước giới hạn (1-8 MB)
Tự động giải phóng khi hàm returnDữ liệu không tồn tại ngoài scope
Không bị fragmentationKích thước phải biết tại compile-time
Cache-friendly (dữ liệu liền kề)Không thể chia sẻ giữa các thread dễ dàng
Zero overhead cho quản lýKhông thể resize

4. Heap — Bộ Nhớ Động

4.1 Khi Nào Cần Heap?

Stack tuyệt vời, nhưng có những lúc bạn buộc phải dùng heap:

  1. Kích thước chưa biết tại compile-time: Đọc file, nhận dữ liệu từ network
  2. Dữ liệu cần tồn tại ngoài scope hàm: Trả về object phức tạp
  3. Dữ liệu quá lớn cho stack: Mảng hàng triệu phần tử
  4. Polymorphism: Lưu trữ các kiểu dẫn xuất qua con trỏ base class

4.2 Cấp Phát Và Giải Phóng

cpp
#include <iostream>
#include <string>

struct Order {
    int id;
    double price;
    std::string symbol;
};

Order* createOrder(int id, double price, const std::string& symbol) {
    // Cấp phát trên HEAP — object tồn tại sau khi hàm return
    Order* order = new Order{id, price, symbol};
    return order;  // OK: heap memory vẫn sống
}

void processOrders() {
    Order* order1 = createOrder(1001, 150.75, "AAPL");
    Order* order2 = createOrder(1002, 89.20, "MSFT");

    std::cout << "Order " << order1->id
              << ": " << order1->symbol
              << " @ $" << order1->price << "\n";

    // ✅ PHẢI giải phóng khi không dùng nữa
    delete order1;
    delete order2;
}

4.3 Vấn Đề "Quên Delete" — Memory Leak

🔥 Production Anti-Pattern: Memory Leak Trong Hot Loop

cpp
// ❌ THẢM HỌA: Cấp phát heap trong vòng lặp nóng (hot loop)
void gameLoop() {
    while (gameRunning) {
        // Mỗi frame tạo object mới trên heap — KHÔNG BAO GIỜ delete
        auto* enemy = new Enemy(randomPosition());
        enemy->update();
        enemy->render();
        // enemy bị leak mỗi frame!
        // 60 FPS × 256 bytes/enemy = 15 KB/giây bị leak
        // Sau 1 giờ chơi game = 54 MB leaked → game lag → crash
    }
}

// ✅ ĐÚNG CÁCH: Pre-allocate + object pool
void gameLoopFixed() {
    // Cấp phát một lần trước vòng lặp
    std::vector<Enemy> enemyPool(MAX_ENEMIES);
    size_t activeCount = 0;

    while (gameRunning) {
        if (activeCount < MAX_ENEMIES) {
            enemyPool[activeCount].reset(randomPosition());
            enemyPool[activeCount].update();
            enemyPool[activeCount].render();
            ++activeCount;
        }
    }
}

Nguyên tắc vàng: Không bao giờ gọi new bên trong vòng lặp nóng. Hãy pre-allocate hoặc dùng object pool.

4.4 Chi Phí Của Heap Allocation

Tại sao heap chậm hơn stack? Vì mỗi lần gọi new, hệ thống phải:

  1. Tìm vùng trống đủ lớn trong heap (first-fit, best-fit algorithm)
  2. Cập nhật bảng quản lý (free list / bitmap)
  3. Có thể gọi system call (brk/mmap trên Linux) nếu heap cần mở rộng
  4. Trả về con trỏ đến vùng nhớ mới

Và khi delete:

  1. Đánh dấu vùng nhớ là "free" trong bảng quản lý
  2. Có thể merge các vùng free liền kề (coalescing)
  3. Không trả lại OS ngay lập tức (thường giữ lại cho lần cấp phát sau)

4.5 Heap Fragmentation — Kẻ Thù Vô Hình

Heap ban đầu (contiguous free space):
[████████████████████████████████████████]

Sau nhiều new/delete xen kẽ (fragmented):
[██░░██░░░░██░░██████░░░░██░░░░░░██░░██]
 ██ = đang dùng    ░░ = free

Vấn đề: Tổng free = 20 KB, nhưng mảnh lớn nhất chỉ 6 KB
→ Không thể cấp phát 10 KB liên tục dù tổng free đủ!

⚠️ Lưu Ý

Fragmentation là lý do tại sao các hệ thống real-time (game engine, embedded) thường dùng custom allocator hoặc memory pool thay vì new/delete trực tiếp.

4.6 So Sánh Stack vs Heap — Bảng Tổng Hợp

Tiêu chíStackHeap
Tốc độ cấp phát~1 ns (di chuyển SP)~100-1000 ns (tìm + cập nhật)
Giải phóngTự động (khi hàm return)Thủ công (delete) hoặc smart pointer
Kích thướcGiới hạn (1-8 MB)Giới hạn bởi RAM + swap
Thời gian sốngChỉ trong scopeCho đến khi delete
Thread safetyMỗi thread có stack riêngChung giữa tất cả threads
FragmentationKhông bao giờCó thể xảy ra
Cache friendlinessRất tốt (LIFO pattern)Kém hơn (phân tán)

5. sizeof Và Alignment — Hiểu Kích Thước Thật Của Dữ Liệu

5.1 sizeof Cho Kiểu Cơ Bản

sizeof cho biết số byte mà một kiểu chiếm trong bộ nhớ. Trên hệ thống 64-bit phổ biến:

cpp
#include <iostream>
#include <cstdint>

int main() {
    std::cout << "=== Kích thước kiểu cơ bản (64-bit system) ===\n";
    std::cout << "char:        " << sizeof(char)        << " byte\n";   // 1
    std::cout << "short:       " << sizeof(short)       << " bytes\n";  // 2
    std::cout << "int:         " << sizeof(int)         << " bytes\n";  // 4
    std::cout << "long:        " << sizeof(long)        << " bytes\n";  // 4 or 8
    std::cout << "long long:   " << sizeof(long long)   << " bytes\n";  // 8
    std::cout << "float:       " << sizeof(float)       << " bytes\n";  // 4
    std::cout << "double:      " << sizeof(double)      << " bytes\n";  // 8
    std::cout << "bool:        " << sizeof(bool)        << " byte\n";   // 1
    std::cout << "pointer:     " << sizeof(void*)       << " bytes\n";  // 8 (64-bit)

    std::cout << "\n=== Kiểu có kích thước cố định (C++11) ===\n";
    std::cout << "int8_t:      " << sizeof(int8_t)      << " byte\n";   // 1
    std::cout << "int16_t:     " << sizeof(int16_t)     << " bytes\n";  // 2
    std::cout << "int32_t:     " << sizeof(int32_t)     << " bytes\n";  // 4
    std::cout << "int64_t:     " << sizeof(int64_t)     << " bytes\n";  // 8

    return 0;
}

5.2 Struct Padding — Bí Ẩn Của sizeof

CPU đọc bộ nhớ hiệu quả nhất khi dữ liệu nằm ở địa chỉ chia hết cho kích thước của nó. Ví dụ: int (4 bytes) nên nằm ở địa chỉ chia hết cho 4.

Compiler tự động chèn padding bytes để đảm bảo alignment:

cpp
#include <iostream>

// ❌ Sắp xếp không tốt — nhiều padding
struct BadLayout {
    char  a;     // 1 byte  + 7 bytes padding (để align double)
    double b;    // 8 bytes
    char  c;     // 1 byte  + 3 bytes padding (để align int)
    int   d;     // 4 bytes
    char  e;     // 1 byte  + 7 bytes padding (để struct align = 8)
};
// sizeof(BadLayout) = 1+7 + 8 + 1+3 + 4 + 1+7 = 32 bytes
// Dữ liệu thật: 1+8+1+4+1 = 15 bytes → 17 bytes padding (53% lãng phí!)

// ✅ Sắp xếp tối ưu — giảm padding
struct GoodLayout {
    double b;    // 8 bytes (alignment = 8, đặt đầu tiên)
    int    d;    // 4 bytes
    char   a;    // 1 byte
    char   c;    // 1 byte
    char   e;    // 1 byte  + 1 byte padding (để struct size chia hết cho 8)
};
// sizeof(GoodLayout) = 8 + 4 + 1 + 1 + 1 + 1(pad) = 16 bytes
// Tiết kiệm 16 bytes so với BadLayout!

int main() {
    std::cout << "sizeof(BadLayout):  " << sizeof(BadLayout)  << "\n"; // 32
    std::cout << "sizeof(GoodLayout): " << sizeof(GoodLayout) << "\n"; // 16

    // Trong một hệ thống quản lý 1 triệu records:
    // BadLayout:  32 × 1,000,000 = 32 MB
    // GoodLayout: 16 × 1,000,000 = 16 MB
    // Tiết kiệm 16 MB chỉ bằng việc sắp xếp lại thứ tự field!

    return 0;
}

Minh họa memory layout:

BadLayout (32 bytes):
Offset: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
       [a][.][.][.][.][.][.][.][b  b  b  b  b  b  b  b]

Offset:16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
       [c][.][.][.][d  d  d  d][e][.][.][.][.][.][.][.]

[.] = padding byte (lãng phí!)

GoodLayout (16 bytes):
Offset: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
       [b  b  b  b  b  b  b  b][d  d  d  d][a][c][e][.]

🏋️ Bài Tập Nhanh

Hãy tính sizeof của struct sau trên hệ thống 64-bit. Đừng chạy code — tính bằng tay trước!

cpp
struct Mystery {
    int    x;     // 4 bytes, alignment = 4
    char   y;     // 1 byte,  alignment = 1
    double z;     // 8 bytes, alignment = 8
    short  w;     // 2 bytes, alignment = 2
};
👉 Xem đáp án
Offset 0-3:   x (4 bytes)
Offset 4:     y (1 byte)
Offset 5-7:   padding (3 bytes, để align double ở offset 8)
Offset 8-15:  z (8 bytes)
Offset 16-17: w (2 bytes)
Offset 18-23: padding (6 bytes, để tổng chia hết cho 8 — alignment lớn nhất)

sizeof(Mystery) = 24 bytes
Dữ liệu thật: 4 + 1 + 8 + 2 = 15 bytes
Padding: 9 bytes (37.5% lãng phí)

Cách tối ưu: Sắp xếp lại thành double z; int x; short w; char y; → chỉ 16 bytes.

5.3 alignas Và alignof (C++11/17)

C++11 giới thiệu alignof để kiểm tra alignment, và alignas để yêu cầu alignment cụ thể:

cpp
#include <iostream>

struct Normal {
    int data[4];
};

// Yêu cầu alignment 64 bytes (cache line size trên hầu hết x86 CPU)
struct alignas(64) CacheAligned {
    int data[4];
};

int main() {
    std::cout << "alignof(int):          " << alignof(int) << "\n";          // 4
    std::cout << "alignof(double):       " << alignof(double) << "\n";       // 8
    std::cout << "alignof(Normal):       " << alignof(Normal) << "\n";       // 4
    std::cout << "alignof(CacheAligned): " << alignof(CacheAligned) << "\n"; // 64

    std::cout << "sizeof(Normal):        " << sizeof(Normal) << "\n";        // 16
    std::cout << "sizeof(CacheAligned):  " << sizeof(CacheAligned) << "\n";  // 64

    return 0;
}

⚡ Ghi Chú Hiệu Năng

Tại sao alignment quan trọng cho hiệu năng?

CPU đọc dữ liệu theo cache line — thường là 64 bytes trên x86. Nếu một int nằm "chéo" giữa hai cache line (misaligned), CPU phải đọc hai cache line thay vì một → chậm gấp đôi.

Trong các hệ thống hiệu năng cao (game engine, HPC, trading system):

  • Sắp xếp struct field giảm padding → tiết kiệm bộ nhớ
  • alignas(64) cho dữ liệu shared giữa threads → tránh false sharing
  • Mỗi % cải thiện cache hit rate = hiệu năng tăng đáng kể

6. Ví Dụ Thực Hành Tổng Hợp

6.1 So Sánh Stack vs Heap Allocation

cpp
#include <iostream>
#include <chrono>
#include <vector>

struct Particle {
    double x, y, z;
    double vx, vy, vz;
    int lifetime;
};

void benchmarkStack() {
    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < 1'000'000; ++i) {
        Particle p{0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 100};
        // compiler may optimize this away — volatile or use result
        volatile auto id = p.lifetime;
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "Stack: " << ms.count() << " μs\n";
}

void benchmarkHeap() {
    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < 1'000'000; ++i) {
        Particle* p = new Particle{0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 100};
        volatile auto id = p->lifetime;
        delete p;
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "Heap:  " << ms.count() << " μs\n";
}

int main() {
    std::cout << "=== Benchmark: Stack vs Heap (1M allocations) ===\n";
    benchmarkStack();
    benchmarkHeap();

    // Typical output:
    // Stack: ~500 μs
    // Heap:  ~25000 μs  (50x slower!)

    return 0;
}

6.2 sizeof Khám Phá Struct

cpp
#include <iostream>
#include <cstddef>  // offsetof

struct NetworkPacket {
    uint8_t  version;    // 1 byte
    uint32_t sourceIP;   // 4 bytes
    uint16_t sourcePort; // 2 bytes
    uint64_t timestamp;  // 8 bytes
    uint8_t  flags;      // 1 byte
};

struct NetworkPacketOptimized {
    uint64_t timestamp;  // 8 bytes (largest first)
    uint32_t sourceIP;   // 4 bytes
    uint16_t sourcePort; // 2 bytes
    uint8_t  version;    // 1 byte
    uint8_t  flags;      // 1 byte
};

int main() {
    std::cout << "=== Naive Layout ===\n";
    std::cout << "sizeof:  " << sizeof(NetworkPacket) << " bytes\n";
    std::cout << "offsets: "
              << "version="   << offsetof(NetworkPacket, version)    << " "
              << "sourceIP="  << offsetof(NetworkPacket, sourceIP)   << " "
              << "sourcePort="<< offsetof(NetworkPacket, sourcePort) << " "
              << "timestamp=" << offsetof(NetworkPacket, timestamp)  << " "
              << "flags="     << offsetof(NetworkPacket, flags)      << "\n";

    std::cout << "\n=== Optimized Layout ===\n";
    std::cout << "sizeof:  " << sizeof(NetworkPacketOptimized) << " bytes\n";
    std::cout << "offsets: "
              << "timestamp=" << offsetof(NetworkPacketOptimized, timestamp)  << " "
              << "sourceIP="  << offsetof(NetworkPacketOptimized, sourceIP)   << " "
              << "sourcePort="<< offsetof(NetworkPacketOptimized, sourcePort) << " "
              << "version="   << offsetof(NetworkPacketOptimized, version)    << " "
              << "flags="     << offsetof(NetworkPacketOptimized, flags)      << "\n";

    // Naive:     32 bytes (padding inflated)
    // Optimized: 16 bytes (minimal padding)
    // Savings:   50% per packet!

    size_t packetCount = 10'000'000;
    std::cout << "\nWith " << packetCount << " packets:\n";
    std::cout << "Naive:     " << (sizeof(NetworkPacket) * packetCount) / (1024*1024) << " MB\n";
    std::cout << "Optimized: " << (sizeof(NetworkPacketOptimized) * packetCount) / (1024*1024) << " MB\n";

    return 0;
}

6.3 Stack Overflow — Đệ Quy Không Kiểm Soát

cpp
#include <iostream>

// ❌ Đệ quy không có điểm dừng hợp lý
int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// ✅ Phiên bản iterative — không stack overflow, O(n) time
int fibonacciSafe(int n) {
    if (n <= 1) return n;

    int prev2 = 0;
    int prev1 = 1;
    int current = 0;

    for (int i = 2; i <= n; ++i) {
        current = prev1 + prev2;
        prev2 = prev1;
        prev1 = current;
    }

    return current;
}

int main() {
    // fibonacci(40) → OK nhưng rất chậm (O(2^n) time)
    // fibonacci(100000) → Stack overflow do đệ quy quá sâu

    std::cout << "fib(10) recursive: " << fibonacci(10) << "\n";     // 55
    std::cout << "fib(10) iterative: " << fibonacciSafe(10) << "\n"; // 55
    std::cout << "fib(45) iterative: " << fibonacciSafe(45) << "\n"; // 1134903170

    return 0;
}

7. Spot The Bug 🐛

🐛 Tìm Lỗi: Memory Leak Ẩn

Đoạn code sau dùng để xử lý đơn hàng trong một server backend. Có bao nhiêu chỗ bị memory leak?

cpp
#include <iostream>
#include <string>
#include <stdexcept>

struct OrderData {
    int id;
    double amount;
    std::string customer;
};

void validateOrder(OrderData* order) {
    if (order->amount <= 0) {
        throw std::invalid_argument("Invalid amount");  // [1]
    }
    if (order->customer.empty()) {
        throw std::invalid_argument("Empty customer");   // [2]
    }
}

void processOrder(int id, double amount, const std::string& customer) {
    OrderData* order = new OrderData{id, amount, customer};  // [A]
    char* logBuffer = new char[256];                          // [B]

    snprintf(logBuffer, 256, "Processing order %d", id);
    std::cout << logBuffer << "\n";

    validateOrder(order);  // Có thể throw exception!

    // ... xử lý đơn hàng ...

    delete order;       // [C] — chỉ tới được nếu không throw
    delete[] logBuffer; // [D] — chỉ tới được nếu không throw
}

int main() {
    try {
        processOrder(1001, 150.0, "Alice");  // OK
        processOrder(1002, -50.0, "Bob");    // throw tại [1]
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << "\n";
    }
    return 0;
}
👉 Xem phân tích

2 memory leak khi validateOrder throw exception:

  1. order (dòng [A]): Nếu exception xảy ra tại [1] hoặc [2], dòng [C] không bao giờ được thực thi → order bị leak.

  2. logBuffer (dòng [B]): Tương tự, dòng [D] không được thực thi → logBuffer bị leak.

Cách sửa: Dùng RAII (sẽ học ở bài sau) hoặc smart pointer:

cpp
void processOrderFixed(int id, double amount, const std::string& customer) {
    auto order = std::make_unique<OrderData>(OrderData{id, amount, customer});
    auto logBuffer = std::make_unique<char[]>(256);

    snprintf(logBuffer.get(), 256, "Processing order %d", id);
    std::cout << logBuffer.get() << "\n";

    validateOrder(order.get());  // Nếu throw, unique_ptr tự delete

    // ... xử lý đơn hàng ...
    // Không cần delete — unique_ptr tự dọn khi ra khỏi scope
}

Đây là lý do Modern C++ khuyên không bao giờ dùng raw new/delete. Luôn dùng smart pointer hoặc container.


8. Kịch Bản Thực Tế: Thiết Kế Hệ Thống Game Server

🎮 Scenario: Entity Component System (ECS)

Bạn đang thiết kế memory layout cho một game server xử lý 10,000 entities đồng thời.

Yêu cầu:

  • Mỗi entity có Position (x, y, z — mỗi cái 8 bytes) và Health (4 bytes)
  • Server cần update tất cả positions mỗi tick (60 ticks/giây)
  • Latency mục tiêu: < 1ms per tick

Phương án A — Object-Oriented (mỗi entity là object riêng trên heap):

cpp
struct Entity {
    double x, y, z;   // 24 bytes
    int health;        // 4 bytes + 4 bytes padding = 32 bytes total
};

// 10,000 entities × new Entity → 10,000 heap allocations
std::vector<Entity*> entities;
for (int i = 0; i < 10000; ++i) {
    entities.push_back(new Entity{...});
}
// Update: truy cập ngẫu nhiên trên heap → cache miss liên tục

Phương án B — Data-Oriented (mảng liên tục trên heap qua vector):

cpp
// Tách dữ liệu theo component — Struct of Arrays (SoA)
std::vector<double> posX(10000);  // contiguous memory
std::vector<double> posY(10000);
std::vector<double> posZ(10000);
std::vector<int> health(10000);

// Update position: sequential memory access → cache-friendly
for (int i = 0; i < 10000; ++i) {
    posX[i] += velX[i] * dt;
    posY[i] += velY[i] * dt;
    posZ[i] += velZ[i] * dt;
}

Kết quả benchmark thực tế:

Phương ánCache missesThời gian update 10K entities
A (OOP, scattered heap)~8,000~2.5 ms
B (DOD, contiguous arrays)~50~0.08 ms

Phương án B nhanh hơn 30 lần nhờ CPU cache có thể prefetch dữ liệu liên tục.

Bài học: Cách bạn tổ chức bộ nhớ quan trọng hơn thuật toán trong nhiều trường hợp thực tế.


9. Tổng Kết — Quy Tắc Vàng Về Bộ Nhớ

Checklist Khi Viết Code C++

#Quy tắcGhi nhớ
1Ưu tiên stack cho biến cục bộ nhỏStack nhanh, tự dọn dẹp
2Dùng std::vector thay vì mảng C trên heapVector quản lý memory cho bạn
3Không bao giờ raw new — dùng smart pointermake_unique, make_shared
4Sắp xếp struct field từ lớn đến nhỏGiảm padding, tiết kiệm bộ nhớ
5Không cấp phát trong hot loopPre-allocate hoặc dùng pool
6Luôn hỏi: sống ở đâu, bao lâu, ai dọn?Tư duy ownership

Quick Decision Tree

Cần lưu dữ liệu?
├─ Kích thước nhỏ, biết trước? → STACK (biến cục bộ)
├─ Kích thước lớn hoặc chưa biết? → HEAP (std::vector, make_unique)
├─ Cần sống mãi mãi? → STATIC / GLOBAL (cẩn thận thread safety)
└─ Cần chia sẻ ownership? → HEAP + shared_ptr (học ở bài smart pointer)

10. Module 1 — Checkpoint

📍 Tiến Trình Của Bạn

Bạn đã hoàn thành Bài 1/3 của Module 1: Memory Mindset.

✅ Bài 1: Tư Duy Bộ Nhớ — Stack vs Heap ← Bạn đang ở đây ⬜ Bài 2: Pointers & References — Con trỏ và tham chiếu ⬜ Bài 3: RAII & Smart Pointers — Quản lý tài nguyên thông minh

Tiếp theo: Pointers & References →