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:
- Allocation: Giảm stack pointer xuống bằng kích thước biến → xong
- Deallocation: Tăng stack pointer khi function return → xong
- Không bookkeeping: Không free list, không fragmentation tracking, không metadata header
- 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:
- Gọi memory allocator (ví dụ
malloc→mmap/sbrk) - Allocator tìm trong free list block có kích thước phù hợp
- Nếu không có block nào phù hợp, nó yêu cầu OS cấp thêm memory
- Ghi metadata header (kích thước block, flags) trước data của bạn
- 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ơn | Phải tìm free list, update metadata |
| Deallocation chậm hơn | Phải gộp free block, update list |
| Fragmentation | Alloc/dealloc lặp đi lặp lại tạo khoảng trống (xem bên dưới) |
| Cache miss | Heap object nằm rải rác trong memory |
| Thread contention | Global heap cần locking trong chương trình multithread |
| Memory leak | Quê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 ở đây2. 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 leak3. 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
| Duration | Keyword/Vị trí | Ở đâu | Lifetime | Ví dụ |
|---|---|---|---|---|
| Automatic | Biến local | Stack | Scope exit | int x = 10; |
| Dynamic | new / make_unique | Heap | Đến khi delete | new int(42) |
| Static | static, globals | Data segment | Suốt đời chương trình | static int n; |
| Thread | thread_local | Thread-local | Suốt đời thread | thread_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 ở đâyKhi 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 cacheCâ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ống | Dùng cái này | Stack hay Heap? |
|---|---|---|
| Primitive và struct nhỏ | Khai báo trực tiếp | Stack |
| Mảng động | std::vector | Heap (managed) |
| Chuỗi động | std::string | Heap (managed) |
| Sở hữu độc quyền | std::unique_ptr | Heap (managed) |
| Sở hữu chung | std::shared_ptr | Heap (managed) |
| Collection kích thước cố định | std::array | Stack |
| Giá trị tùy chọn | std::optional | Stack |
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
| Feature | Stack | Heap |
|---|---|---|
| Tốc độ | Rất nhanh (dịch pointer) | Chậm hơn (allocator tìm + metadata) |
| Kích thước | Giới hạn (thường 1–8 MB) | Giới hạn bởi RAM có sẵn |
| Lifetime | Tự động (theo scope) | Thủ công (bạn kiểm soát) |
| Thread safety | Mỗi thread có stack riêng | Shared — cần đồng bộ |
| Fragmentation | Không bao giờ | Có thể fragment theo thời gian |
| Cache performance | Xuất sắc (liên tục) | Thay đổi (có thể rải rác) |
| Allocation | Tự động bởi compiler | Thủ công (new, make_unique, container) |
| Deallocation | Tự động khi ra scope | Thủ công (delete) hoặc qua RAII |
| Use case | Giá trị nhỏ, ngắn hạn | Data 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:
- Stack = nhanh, tự động, giới hạn. Dùng cho data nhỏ, local, kích thước cố định.
- Heap = linh hoạt, thủ công, tốn kém. Dùng cho data lớn, động, hoặc sống lâu.
- 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.
- Modern C++ = bạn hiếm khi gọi
new/delete. Dùngstd::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 khifoo()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 khidelete)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.