Back to blog

C++ Stack vs Heap Memory: Hiểu Sâu Về Bộ Nhớ

cppc++memory-managementstackheap
C++ Stack vs Heap Memory: Hiểu Sâu Về Bộ Nhớ

Mỗi biến trong C++ đều sống ở đâu đó trong memory. Hiểu được ở đâu — và tại sao — là thứ phân biệt developer viết code nhanh, đúng với người suốt ngày đi debug crash và memory leak bí ẩn.

Đa số ngôn ngữ giấu memory management sau garbage collector. C++ thì không. Nó cho bạn quyền kiểm soát trực tiếp object được tạo ở đâu, sống bao lâu, và bị hủy khi nào. Quyền năng đó chính là superpower của C++, nhưng nó đòi hỏi bạn phải hiểu rõ.

Guide này cover hai vùng memory chính — stack và heap — cách chúng hoạt động, khác nhau thế nào, và cách chọn giữa chúng.

Bạn Sẽ Học Được Gì

✅ Cách stack và heap được tổ chức trong process memory
✅ Object lifetime và storage duration (automatic, dynamic, static)
✅ Stack allocation: cách hoạt động, tại sao nhanh, và giới hạn
✅ Heap allocation: khi nào cần và chi phí của nó
✅ Stack overflow, heap fragmentation, và các pitfall khác
✅ Decision framework: stack vs heap cho mọi tình huống
✅ Cách modern C++ (smart pointers, containers) đơn giản hóa lựa chọn


Memory Layout của Một Chương Trình C++

Khi chương trình C++ chạy, hệ điều hành cấp cho nó một block virtual memory. Vùng memory này được chia thành nhiều segment:

Điểm chính:

  • Stack grows downward (từ địa chỉ cao xuống thấp)
  • Heap grows upward (từ địa chỉ thấp lên cao)
  • Chúng mọc hướng về nhau, với free space ở giữa
  • Text segment chứa code đã compile (read-only)
  • Data/BSS segments chứa biến global và static
#include <iostream>
 
int globalVar = 42;                    // Data segment (đã khởi tạo)
int uninitializedGlobal;               // BSS segment (zero-initialized)
 
int main() {
    int stackVar = 10;                 // Stack
    static int staticLocal = 20;       // Data segment (đã khởi tạo)
    int* heapVar = new int(30);        // Pointer trên stack, value trên heap
 
    std::cout << "Stack:  " << &stackVar << '\n';
    std::cout << "Heap:   " << heapVar << '\n';
    std::cout << "Global: " << &globalVar << '\n';
    std::cout << "Static: " << &staticLocal << '\n';
 
    delete heapVar;
    return 0;
}

Chạy thử là bạn sẽ thấy các địa chỉ phản ánh đúng layout: stack address cao, heap address thấp hơn, và globals còn thấp hơn nữa.


Stack: Nhanh, Tự Động, Giới Hạn

Stack là cấu trúc dữ liệu LIFO (Last In, First Out) được quản lý bởi CPU. Mỗi khi bạn gọi function, một stack frame mới được push lên. Khi function return, frame đó bị pop.

Cách Stack Allocation Hoạt Động

void bar(int y) {
    int local_b = y * 2;    // push lên stack
    // ... làm việc ...
}                            // local_b bị pop (destroyed)
 
void foo(int x) {
    int local_a = x + 1;    // push lên stack
    bar(local_a);            // frame mới push cho bar()
    // frame của bar đã pop khi tới đây
}                            // local_a bị pop (destroyed)
 
int main() {
    int n = 10;              // push lên stack
    foo(n);                  // frame mới push cho foo()
    return 0;                // n bị pop (destroyed)
}

Tại Sao Stack Allocation Nhanh

Stack allocation gần như miễn phí — chỉ là dịch chuyển pointer:

  1. Allocation: Giảm stack pointer xuống bằng kích thước biến → xong
  2. Deallocation: Tăng stack pointer khi function return → xong
  3. Không bookkeeping: Không free list, không fragmentation tracking, không metadata header
  4. Cache-friendly: Stack memory liên tục và được CPU cache tốt

So sánh với heap allocation — phải tìm block trống phù hợp, update metadata, và có thể gọi vào OS kernel.

#include <chrono>
#include <iostream>
 
void stackAllocation() {
    for (int i = 0; i < 1'000'000; i++) {
        int arr[100];           // Stack: chỉ dịch pointer
        arr[0] = i;             // Tránh optimization
    }
}
 
void heapAllocation() {
    for (int i = 0; i < 1'000'000; i++) {
        int* arr = new int[100]; // Heap: tìm block, update metadata
        arr[0] = i;
        delete[] arr;            // Heap: đánh dấu free, gộp block
    }
}
 
int main() {
    auto start = std::chrono::high_resolution_clock::now();
    stackAllocation();
    auto mid = std::chrono::high_resolution_clock::now();
    heapAllocation();
    auto end = std::chrono::high_resolution_clock::now();
 
    auto stackTime = std::chrono::duration_cast<std::chrono::microseconds>(mid - start);
    auto heapTime = std::chrono::duration_cast<std::chrono::microseconds>(end - mid);
 
    std::cout << "Stack: " << stackTime.count() << " μs\n";
    std::cout << "Heap:  " << heapTime.count() << " μs\n";
    std::cout << "Ratio: " << (double)heapTime.count() / stackTime.count() << "x slower\n";
 
    return 0;
}

Kết quả thường thấy: heap allocation chậm hơn 10–100 lần so với stack allocation.

Giới Hạn của Stack

Stack nhanh vì nó đơn giản. Nhưng sự đơn giản đó đi kèm ràng buộc:

1. Kích thước cố định (thường 1–8 MB)

void stackoverflow() {
    int hugeArray[1'000'000];  // 4 MB trên stack — có thể crash!
    hugeArray[0] = 42;
}

2. Kích thước phải biết lúc compile (theo chuẩn C++)

void process(int n) {
    // int arr[n];           // Variable-Length Arrays — KHÔNG phải standard C++!
    std::vector<int> arr(n); // Dùng heap qua std::vector thay thế
}

3. Object chết khi function return

int* createValue() {
    int local = 42;
    return &local;  // DANGLING POINTER! local bị destroy khi return
}

Stack Overflow

Khi bạn dùng nhiều stack space hơn mức có sẵn, bạn sẽ gặp stack overflow — thường là segfault (segmentation fault):

// Nguyên nhân kinh điển: recursion không giới hạn
void infinite(int n) {
    int buffer[1000];        // Mỗi lần gọi tốn ~4 KB
    std::cout << n << '\n';
    infinite(n + 1);         // Không bao giờ dừng → stack overflow
}
 
// Fix: luôn có base case
void factorial(int n) {
    if (n <= 1) return;      // Base case dừng recursion
    factorial(n - 1);
}

Nguyên nhân stack overflow thường gặp:

  • Recursion không giới hạn (thiếu base case)
  • Recursion quá sâu (có base case nhưng call chain quá dài)
  • Biến local quá lớn (mảng khổng lồ trên stack)

Cách kiểm tra/set stack size:
Linux: ulimit -s (hiện KB, mặc định thường 8192 = 8 MB)
macOS: ulimit -s (mặc định thường 8192 = 8 MB)
Windows: Mặc định 1 MB, cấu hình qua linker flag /STACK


Heap: Linh Hoạt, Thủ Công, Tốn Kém

Heap (hay free store trong thuật ngữ C++) là một pool memory lớn, không có cấu trúc. Bạn yêu cầu memory từ nó, và bạn chịu trách nhiệm trả lại.

Cách Heap Allocation Hoạt Động

#include <iostream>
 
int main() {
    // 1. Allocate: OS/allocator tìm block trống
    int* p = new int(42);
 
    // 2. Sử dụng: memory sống bao lâu tùy bạn
    std::cout << *p << '\n';
 
    // 3. Deallocate: bạn phải free thủ công
    delete p;
    p = nullptr;  // Good practice: tránh dangling pointer
 
    return 0;
}

Đằng sau, new làm nhiều việc:

  1. Gọi memory allocator (ví dụ malloc → mmap/sbrk)
  2. Allocator tìm trong free list block có kích thước phù hợp
  3. Nếu không có block nào phù hợp, nó yêu cầu OS cấp thêm memory
  4. Ghi metadata header (kích thước block, flags) trước data của bạn
  5. Trả về pointer tới vùng data dùng được

Tại Sao Cần Heap

Heap giải quyết ba vấn đề mà stack không thể:

1. Object sống lâu hơn function tạo ra nó

#include <memory>
#include <string>
 
std::unique_ptr<std::string> createGreeting(const std::string& name) {
    // String này sống trên heap — tồn tại sau khi function return
    return std::make_unique<std::string>("Hello, " + name + "!");
}
 
int main() {
    auto greeting = createGreeting("World");
    std::cout << *greeting << '\n';  // "Hello, World!"
    // greeting tự động delete khi main() kết thúc
    return 0;
}

2. Data lớn sẽ overflow stack

#include <vector>
 
void processLargeDataset() {
    // 100 triệu int = ~400 MB — quá lớn cho stack
    // std::vector allocate trên heap bên trong
    std::vector<int> data(100'000'000);
 
    for (int i = 0; i < data.size(); i++) {
        data[i] = i;
    }
    // destructor của vector free heap memory
}

3. Data có kích thước chỉ biết lúc runtime

#include <iostream>
#include <memory>
 
int main() {
    int n;
    std::cout << "Bao nhiêu phần tử? ";
    std::cin >> n;
 
    // Kích thước do user input quyết định — phải dùng heap
    auto arr = std::make_unique<int[]>(n);
 
    for (int i = 0; i < n; i++) {
        arr[i] = i * i;
    }
 
    return 0;
}

Chi Phí của Heap Allocation

Không có gì miễn phí. Sự linh hoạt của heap đi kèm chi phí:

Chi phíLý do
Allocation chậm hơnPhải tìm free list, update metadata
Deallocation chậm hơnPhải gộp free block, update list
FragmentationAlloc/dealloc lặp đi lặp lại tạo khoảng trống (xem bên dưới)
Cache missHeap object nằm rải rác trong memory
Thread contentionGlobal heap cần locking trong chương trình multithread
Memory leakQuên delete → memory không bao giờ được trả lại

Heap Fragmentation

Fragmentation xảy ra khi heap có đủ tổng free memory nhưng không đủ liên tục free memory:

Tổng free: 80 bytes. Nhưng bạn không thể allocate block 64 byte vì free space bị phân mảnh thành các chunk 32 + 16 + 32 byte.

#include <iostream>
#include <vector>
 
// Minh họa fragmentation
int main() {
    std::vector<int*> ptrs;
 
    // Allocate nhiều block nhỏ
    for (int i = 0; i < 1000; i++) {
        ptrs.push_back(new int[10]);
    }
 
    // Free mỗi block thứ hai → tạo lỗ hổng
    for (int i = 0; i < 1000; i += 2) {
        delete[] ptrs[i];
        ptrs[i] = nullptr;
    }
 
    // Giờ thử allocate block lớn
    // Dù ~50% memory trống, nó có thể bị phân mảnh
    int* large = new int[5000];  // Có thể phải yêu cầu OS cấp memory mới
 
    // Cleanup
    for (int i = 1; i < 1000; i += 2) {
        delete[] ptrs[i];
    }
    delete[] large;
 
    return 0;
}

Storage Duration: Bức Tranh Toàn Cảnh

C++ định nghĩa bốn storage duration quyết định object sống bao lâu:

1. Automatic Storage Duration (Stack)

Object được tạo bên trong function hoặc block. Bị destroy khi ra khỏi scope.

void example() {
    int x = 10;             // Tạo ở đây
    {
        int y = 20;          // Tạo ở đây
        std::string s = "hi"; // Tạo ở đây
    }                        // y và s bị destroy ở đây
 
    // y và s không tồn tại ở đây
}                            // x bị destroy ở đây

2. Dynamic Storage Duration (Heap)

Object được tạo bằng new (hoặc malloc). Sống cho đến khi bạn delete (hoặc free) thủ công.

void example() {
    int* p = new int(42);    // Tạo trên heap
    // ...
    delete p;                // Destroy ở đây — BẠN quyết định khi nào
}
// Nếu quên delete → memory leak

3. Static Storage Duration

Object sống suốt đời chương trình. Bao gồm biến global, static local, và static class member.

int globalCounter = 0;       // Static: sống đến khi chương trình exit
 
void count() {
    static int calls = 0;   // Static local: khởi tạo một lần, tồn tại qua các lần gọi
    calls++;
    globalCounter++;
    std::cout << "Lần gọi #" << calls << ", global: " << globalCounter << '\n';
}
 
int main() {
    count();  // Lần gọi #1, global: 1
    count();  // Lần gọi #2, global: 2
    count();  // Lần gọi #3, global: 3
    return 0;
}

4. Thread Storage Duration

Object sống suốt đời một thread. Tạo bằng thread_local.

#include <iostream>
#include <thread>
 
thread_local int threadCounter = 0;  // Mỗi thread có bản copy riêng
 
void work(const std::string& name) {
    for (int i = 0; i < 3; i++) {
        threadCounter++;
        std::cout << name << ": " << threadCounter << '\n';
    }
}
 
int main() {
    std::thread t1(work, "Thread A");
    std::thread t2(work, "Thread B");
    t1.join();
    t2.join();
    // Mỗi thread đếm 1, 2, 3 độc lập
    return 0;
}

Tổng Kết Storage Duration

DurationKeyword/Vị tríỞ đâuLifetimeVí dụ
AutomaticBiến localStackScope exitint x = 10;
Dynamicnew / make_uniqueHeapĐến khi deletenew int(42)
Staticstatic, globalsData segmentSuốt đời chương trìnhstatic int n;
Threadthread_localThread-localSuốt đời threadthread_local int n;

Stack vs Heap: Decision Framework

Đây là decision tree thực tế để chọn allocate ở đâu:

Khi Nào Dùng Stack

// ✅ Kiểu nhỏ, kích thước cố định
int count = 0;
double ratio = 3.14;
char grade = 'A';
 
// ✅ Struct và object nhỏ
struct Point { double x, y; };
Point origin{0.0, 0.0};
 
// ✅ std::array cho collection kích thước cố định
std::array<int, 10> scores{};
 
// ✅ Iterator và biến loop
for (auto it = vec.begin(); it != vec.end(); ++it) { /* ... */ }
 
// ✅ RAII lock guard
{
    std::lock_guard<std::mutex> lock(mtx);
    // ... critical section ...
}  // lock tự động release ở đây

Khi Nào Dùng Heap

// ✅ Collection với kích thước xác định lúc runtime
std::vector<int> data(n);          // heap bên trong
std::string name = getUserInput(); // heap bên trong
 
// ✅ Object lớn
auto matrix = std::make_unique<double[]>(1'000'000);
 
// ✅ Object sống lâu hơn scope
auto config = std::make_unique<Config>();
return config;  // ownership chuyển cho caller
 
// ✅ Object đa hình
std::unique_ptr<Shape> shape = std::make_unique<Circle>(5.0);
 
// ✅ Resource dùng chung
auto cache = std::make_shared<Cache>();
// Nhiều component giữ shared_ptr đến cùng cache

Câu Trả Lời của Modern C++

Thực tế, modern C++ làm lựa chọn đơn giản hơn. Bạn hiếm khi gọi new/delete trực tiếp:

Tình huốngDùng cái nàyStack hay Heap?
Primitive và struct nhỏKhai báo trực tiếpStack
Mảng độngstd::vectorHeap (managed)
Chuỗi độngstd::stringHeap (managed)
Sở hữu độc quyềnstd::unique_ptrHeap (managed)
Sở hữu chungstd::shared_ptrHeap (managed)
Collection kích thước cố địnhstd::arrayStack
Giá trị tùy chọnstd::optionalStack

Các container của standard library (vector, string, map, v.v.) handle heap allocation bên trong và tự động cleanup qua destructor. Bạn có được sự linh hoạt của heap với sự an toàn của stack.


RAII: Cầu Nối Giữa Stack và Heap

RAII (Resource Acquisition Is Initialization) là pattern làm cho C++ memory management trở nên thực tế. Ý tưởng: gắn lifetime của heap resource vào scope của stack object.

#include <fstream>
#include <memory>
#include <mutex>
 
void demonstrateRAII() {
    // 1. File handle: mở trong constructor, đóng trong destructor
    {
        std::ofstream file("output.txt");
        file << "Hello, RAII!\n";
    }  // file tự động đóng ở đây
 
    // 2. Smart pointer: memory free khi pointer ra khỏi scope
    {
        auto data = std::make_unique<int[]>(1000);
        data[0] = 42;
    }  // memory tự động free ở đây
 
    // 3. Lock guard: mutex unlock khi guard bị destroy
    std::mutex mtx;
    {
        std::lock_guard<std::mutex> lock(mtx);
        // ... critical section ...
    }  // mutex tự động unlock ở đây
}

Pattern:

  • Stack object (RAII wrapper) quản lý heap resource
  • Constructor acquire resource
  • Destructor release resource
  • Scope exit đảm bảo cleanup — kể cả khi exception bị throw

Rule of thumb: Mỗi heap allocation nên được sở hữu bởi một stack object. Nếu tuân theo rule này, bạn sẽ không bao giờ bị memory leak.

Để tìm hiểu sâu hơn về smart pointer và ownership pattern, xem C++ Pointers Complete Guide.


Ví Dụ Thực Tế

Ví Dụ 1: Calculator Đơn Giản Trên Stack

Mọi thứ trên stack — nhanh và đơn giản:

#include <iostream>
#include <array>
 
struct Calculator {
    // Tất cả member trên stack
    std::array<double, 100> memory{};
    int top = 0;
 
    void push(double value) {
        if (top < 100) {
            memory[top++] = value;
        }
    }
 
    double pop() {
        if (top > 0) {
            return memory[--top];
        }
        return 0.0;
    }
 
    double add() {
        double b = pop(), a = pop();
        double result = a + b;
        push(result);
        return result;
    }
 
    double multiply() {
        double b = pop(), a = pop();
        double result = a * b;
        push(result);
        return result;
    }
};
 
int main() {
    Calculator calc;  // Toàn bộ calculator sống trên stack
    calc.push(3.0);
    calc.push(4.0);
    std::cout << "3 + 4 = " << calc.add() << '\n';      // 7
 
    calc.push(5.0);
    std::cout << "7 * 5 = " << calc.multiply() << '\n';  // 35
 
    return 0;
}

Ví Dụ 2: Graph Động Trên Heap

Khi data structure grow động, heap là cần thiết:

#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
 
class Graph {
    struct Node {
        std::string name;
        std::vector<Node*> neighbors;
 
        explicit Node(std::string n) : name(std::move(n)) {}
    };
 
    // Các node được sở hữu bởi graph, lưu trên heap
    std::unordered_map<std::string, std::unique_ptr<Node>> nodes;
 
public:
    void addNode(const std::string& name) {
        nodes[name] = std::make_unique<Node>(name);
    }
 
    void addEdge(const std::string& from, const std::string& to) {
        if (nodes.count(from) && nodes.count(to)) {
            nodes[from]->neighbors.push_back(nodes[to].get());
        }
    }
 
    void printNeighbors(const std::string& name) const {
        if (auto it = nodes.find(name); it != nodes.end()) {
            std::cout << name << " -> ";
            for (const auto* neighbor : it->second->neighbors) {
                std::cout << neighbor->name << " ";
            }
            std::cout << '\n';
        }
    }
};
 
int main() {
    Graph g;  // Graph object trên stack, node trên heap
 
    g.addNode("A");
    g.addNode("B");
    g.addNode("C");
    g.addEdge("A", "B");
    g.addEdge("A", "C");
    g.addEdge("B", "C");
 
    g.printNeighbors("A");  // A -> B C
    g.printNeighbors("B");  // B -> C
 
    return 0;
    // Tất cả node tự động free khi Graph destructor chạy
}

Ví Dụ 3: Đo Performance Stack vs Heap

Benchmark thực tế so sánh các allocation strategy:

#include <chrono>
#include <iostream>
#include <memory>
#include <vector>
 
struct Particle {
    double x, y, z;
    double vx, vy, vz;
    double mass;
 
    void update(double dt) {
        x += vx * dt;
        y += vy * dt;
        z += vz * dt;
    }
};
 
// Strategy 1: Stack allocation với std::array
void simulateStack(int steps) {
    std::array<Particle, 1000> particles{};
    for (auto& p : particles) {
        p = {0.0, 0.0, 0.0, 1.0, 0.5, 0.2, 1.0};
    }
    for (int i = 0; i < steps; i++) {
        for (auto& p : particles) {
            p.update(0.01);
        }
    }
}
 
// Strategy 2: Heap allocation với std::vector
void simulateHeap(int steps) {
    std::vector<Particle> particles(1000);
    for (auto& p : particles) {
        p = {0.0, 0.0, 0.0, 1.0, 0.5, 0.2, 1.0};
    }
    for (int i = 0; i < steps; i++) {
        for (auto& p : particles) {
            p.update(0.01);
        }
    }
}
 
// Strategy 3: Heap với allocation riêng lẻ (worst case)
void simulateHeapScattered(int steps) {
    std::vector<std::unique_ptr<Particle>> particles;
    for (int i = 0; i < 1000; i++) {
        particles.push_back(std::make_unique<Particle>(
            Particle{0.0, 0.0, 0.0, 1.0, 0.5, 0.2, 1.0}
        ));
    }
    for (int i = 0; i < steps; i++) {
        for (auto& p : particles) {
            p->update(0.01);
        }
    }
}
 
int main() {
    const int steps = 10'000;
 
    auto measure = [](const std::string& name, auto func) {
        auto start = std::chrono::high_resolution_clock::now();
        func();
        auto end = std::chrono::high_resolution_clock::now();
        auto us = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        std::cout << name << ": " << us.count() << " μs\n";
    };
 
    measure("Stack (array)        ", [&]{ simulateStack(steps); });
    measure("Heap (vector)        ", [&]{ simulateHeap(steps); });
    measure("Heap (scattered ptrs)", [&]{ simulateHeapScattered(steps); });
 
    return 0;
}

Kết quả expected:

  • Stack (array): Nhanh nhất — data liên tục, cache-friendly
  • Heap (vector): Chậm hơn chút — một heap allocation, vẫn liên tục
  • Heap (scattered): Chậm nhất — mỗi particle allocate riêng, cache-unfriendly

Bài học rút ra: không chỉ stack vs heap — data layout mới quan trọng. Memory liên tục (dù stack hay heap) đánh bại heap allocation rải rác.


Pitfall Thường Gặp và Cách Tránh

1. Return Pointer Đến Biến Local

// ❌ SAI: dangling pointer
int* bad() {
    int x = 42;
    return &x;     // x bị destroy khi bad() return
}
 
// ✅ ĐÚNG: return by value (move semantics làm cho efficient)
int good() {
    int x = 42;
    return x;      // Copy/move — an toàn
}
 
// ✅ ĐÚNG: return smart pointer đến heap object
std::unique_ptr<int> alsoGood() {
    return std::make_unique<int>(42);  // Heap object, ownership được transfer
}

2. Stack Overflow Từ Mảng Local Quá Lớn

// ❌ NGUY HIỂM: 40 MB trên stack
void bad() {
    double matrix[10'000'000];
}
 
// ✅ AN TOÀN: dùng std::vector (heap-backed)
void good() {
    std::vector<double> matrix(10'000'000);
}

3. Memory Leak Từ Raw new

// ❌ LEAK: exception trước delete
void bad() {
    int* data = new int[1000];
    riskyOperation();     // Nếu throw, delete[] không bao giờ chạy
    delete[] data;
}
 
// ✅ AN TOÀN: smart pointer handle cleanup kể cả khi exception
void good() {
    auto data = std::make_unique<int[]>(1000);
    riskyOperation();     // Kể cả nếu throw, memory vẫn được free
}

4. Fragmentation Từ Allocation Nhỏ Liên Tục

// ❌ CHẬM: hàng nghìn tiny heap allocation
std::vector<std::unique_ptr<int>> bad;
for (int i = 0; i < 10'000; i++) {
    bad.push_back(std::make_unique<int>(i));
}
 
// ✅ TỐT HƠN: một allocation liên tục duy nhất
std::vector<int> good(10'000);
for (int i = 0; i < 10'000; i++) {
    good[i] = i;
}

Nâng Cao: Custom Allocator và Memory Pool

Cho application cần performance tối đa, bạn có thể tùy chỉnh cách heap memory được allocate:

Khái Niệm Arena/Pool Allocator

Arena allocator pre-allocate một block lớn và cấp phát chunk từ đó — allocation nhanh với zero fragmentation:

#include <cstddef>
#include <iostream>
#include <vector>
 
class Arena {
    std::vector<std::byte> buffer;
    std::size_t offset = 0;
 
public:
    explicit Arena(std::size_t size) : buffer(size) {}
 
    void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
        // Align offset
        std::size_t aligned = (offset + alignment - 1) & ~(alignment - 1);
 
        if (aligned + size > buffer.size()) {
            throw std::bad_alloc();
        }
 
        void* ptr = buffer.data() + aligned;
        offset = aligned + size;
        return ptr;
    }
 
    void reset() { offset = 0; }  // "Free" mọi thứ cùng lúc
 
    std::size_t used() const { return offset; }
    std::size_t capacity() const { return buffer.size(); }
};
 
int main() {
    Arena arena(1024);  // Arena 1 KB
 
    // Allocate từ arena — cực nhanh (chỉ dịch pointer)
    int* a = static_cast<int*>(arena.allocate(sizeof(int)));
    int* b = static_cast<int*>(arena.allocate(sizeof(int)));
    double* c = static_cast<double*>(arena.allocate(sizeof(double)));
 
    *a = 10;
    *b = 20;
    *c = 3.14;
 
    std::cout << *a << ", " << *b << ", " << *c << '\n';  // 10, 20, 3.14
    std::cout << "Used: " << arena.used() << " / " << arena.capacity() << " bytes\n";
 
    arena.reset();  // Mọi allocation free cùng lúc — O(1)
    std::cout << "Sau reset: " << arena.used() << " bytes used\n";
 
    return 0;
}

C++17 std::pmr (Polymorphic Memory Resources)

Standard library cung cấp framework cho custom allocator:

#include <iostream>
#include <memory_resource>
#include <vector>
 
int main() {
    // Stack-based buffer với monotonic allocator
    std::array<std::byte, 4096> buffer;
    std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
 
    // Vector allocate từ pool thay vì global heap
    std::pmr::vector<int> vec(&pool);
 
    for (int i = 0; i < 100; i++) {
        vec.push_back(i);
    }
 
    std::cout << "Vector size: " << vec.size() << '\n';
    std::cout << "Allocate từ stack-backed pool — zero heap allocation!\n";
 
    return 0;
}

Khi nào dùng custom allocator? Chỉ khi profiling cho thấy memory allocation là bottleneck. Đa số application, default allocator là đủ. Custom allocator tỏa sáng trong game engine, embedded system, và high-frequency trading.


Quick Reference: Stack vs Heap

FeatureStackHeap
Tốc độRất nhanh (dịch pointer)Chậm hơn (allocator tìm + metadata)
Kích thướcGiới hạn (thường 1–8 MB)Giới hạn bởi RAM có sẵn
LifetimeTự động (theo scope)Thủ công (bạn kiểm soát)
Thread safetyMỗi thread có stack riêngShared — cần đồng bộ
FragmentationKhông bao giờCó thể fragment theo thời gian
Cache performanceXuất sắc (liên tục)Thay đổi (có thể rải rác)
AllocationTự động bởi compilerThủ công (new, make_unique, container)
DeallocationTự động khi ra scopeThủ công (delete) hoặc qua RAII
Use caseGiá trị nhỏ, ngắn hạnData lớn, dài hạn, hoặc kích thước động

Kết Luận

Memory management trong C++ không đáng sợ như bạn nghĩ. Mental model rất đơn giản:

  1. Stack = nhanh, tự động, giới hạn. Dùng cho data nhỏ, local, kích thước cố định.
  2. Heap = linh hoạt, thủ công, tốn kém. Dùng cho data lớn, động, hoặc sống lâu.
  3. RAII = cầu nối. Smart pointer và container cho bạn sự linh hoạt của heap với sự an toàn của stack.
  4. Modern C++ = bạn hiếm khi gọi new/delete. Dùng std::vector, std::string, std::unique_ptr, và để standard library lo memory cho bạn.

Golden rule: ưu tiên stack. Dùng heap khi cần. Luôn dùng RAII để quản lý heap resource.


Bài Tập Thực Hành

Bài 1: Xác định storage duration

Storage duration của mỗi biến là gì?

int a = 10;
 
void foo() {
    int b = 20;
    static int c = 30;
    int* d = new int(40);
    thread_local int e = 50;
}

Đáp án:

  • a — Static (biến global, sống suốt đời chương trình)
  • b — Automatic (biến local, chết khi foo() return)
  • c — Static (static local, khởi tạo một lần, sống suốt đời chương trình)
  • d (pointer) — Automatic (bản thân pointer nằm trên stack)
  • *d (giá trị được trỏ tới) — Dynamic (trên heap, sống đến khi delete)
  • e — Thread (mỗi thread có bản copy riêng)
Bài 2: Sửa bug memory

Tìm và sửa tất cả bug liên quan đến memory:

int* createArray(int size) {
    int arr[size];           // Bug 1
    for (int i = 0; i < size; i++) {
        arr[i] = i * i;
    }
    return arr;              // Bug 2
}
 
void process() {
    int* data = new int[100];
    if (data[0] > 10) {
        return;              // Bug 3
    }
    delete data;             // Bug 4
}

Đáp án:

// Phiên bản đã sửa
std::vector<int> createArray(int size) {
    std::vector<int> arr(size);    // Fix 1: Dùng vector cho mảng kích thước runtime
    for (int i = 0; i < size; i++) {
        arr[i] = i * i;
    }
    return arr;                     // Fix 2: Return by value (không dangling pointer)
}
 
void process() {
    auto data = std::make_unique<int[]>(100);  // Fix 3 & 4: Smart pointer
    if (data[0] > 10) {
        return;  // Smart pointer tự cleanup
    }
    // Không cần delete thủ công
}
Bài 3: Tối ưu allocation strategy

Code này tạo 10,000 particle. Bạn sẽ tối ưu memory usage thế nào?

struct Particle {
    double x, y, z;
    double vx, vy, vz;
};
 
void simulate() {
    std::vector<std::unique_ptr<Particle>> particles;
    for (int i = 0; i < 10'000; i++) {
        particles.push_back(std::make_unique<Particle>());
    }
    // ... simulation loop ...
}

Đáp án:

void simulate() {
    // Lưu particle liên tục — một allocation thay vì 10,000
    std::vector<Particle> particles(10'000);
    // ... simulation loop ...
}

Tại sao: vector<unique_ptr<Particle>> tạo 10,000 heap allocation riêng lẻ, rải rác khắp memory. vector<Particle> tạo một allocation liên tục duy nhất, tốt hơn đáng kể cho cache performance.


Bài Viết Liên Quan

Series: Modern C++ Learning Roadmap
Previous: C++ Pointers: The Complete Guide
Related: Phase 1: C++ Fundamentals

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.