Các vùng nhớ trong một chương trình C++
Một chương trình C++ khi chạy sẽ sử dụng nhiều vùng nhớ khác nhau, được phân tách rõ ràng về mục đích sử dụng, cách cấp phát, thời gian tồn tại, khả năng mở rộng và giới hạn vật lý hoặc logic. Việc hiểu rõ các vùng nhớ là điều quan trọng để tránh lỗi phổ biến như tràn stack, leak heap, hoặc lỗi phân mảnh bộ nhớ.
Các vùng nhớ (Text, Data, BSS, Stack, Heap) được sắp xếp trong không gian địa chỉ ảo:
[Text Segment] <- Mã lệnh (read-only)
[Data Segment] <- Biến global/static (khởi tạo)
[BSS Segment] <- Biến global/static (chưa khởi tạo/0)
[Heap] <- Cấp phát động (mở rộng lên)
...
[Stack] <- Ngăn xếp (mở rộng xuống)
1. Text Segment (Mã lệnh chương trình)
Mục đích:
- Chứa mã máy (machine code) – các lệnh do trình biên dịch tạo ra.
Đặc điểm:
- Là vùng chỉ đọc (read-only).
- Không thể mở rộng.
- Không chứa dữ liệu người dùng hay biến runtime.
- Thường được hệ điều hành ánh xạ (memory-mapped) từ file
.exe
hoặc.dll
.
Giới hạn:
- Có kích thước cố định tại thời điểm load chương trình.
- Giới hạn bởi kích thước của mã chương trình hoặc giới hạn vùng địa chỉ thực thi của hệ điều hành.
Ví dụ:
int sum(int a, int b) {
return a + b;
}
Hàm sum
sau khi biên dịch sẽ nằm trong text segment.
2. Data Segment (Biến global/static đã khởi tạo)
Mục đích:
- Lưu trữ các biến global hoặc static đã được khởi tạo với giá trị khác 0.
Đặc điểm:
- Tồn tại suốt vòng đời của chương trình.
- Được hệ điều hành cấp phát khi chương trình load.
- Không tự mở rộng được.
- Data Segment thường được chia thành:
- Read-only Data: Lưu trữ các hằng số như
const int x = 10;
hoặc chuỗi ký tự hằng (const char* str = "hello";
). - Read-write Data: Lưu trữ các biến global/static có thể thay đổi giá trị.
- Read-only Data: Lưu trữ các hằng số như
Giới hạn:
- Kích thước tổng Data + BSS bị giới hạn bởi:
- Hệ điều hành (ví dụ: Linux 32-bit giới hạn ~1–2 GB cho toàn bộ tiến trình).
- Kích thước file thực thi.
- Layout bộ nhớ (bản đồ vùng địa chỉ của tiến trình).
Ví dụ:
int a = 10; // biến toàn cục
static int b = 20; // static trong hàm
const int x = 10; // read-only Data Segment
int y = 20; // read-write Data Segment
3. BSS Segment (Biến global/static khởi tạo bằng 0 hoặc chưa khởi tạo)
Mục đích:
- Chứa các biến toàn cục hoặc biến tĩnh chưa được khởi tạo hoặc được khởi tạo bằng 0.
Đặc điểm:
- Tồn tại suốt vòng đời chương trình.
- Không lưu dữ liệu thực tế trong file nhị phân, chỉ giữ thông tin kích thước cần cấp phát và được zero hóa khi khởi chạy.
- Không thể mở rộng.
Giới hạn:
- Tương tự như Data Segment, nhưng thường được nhóm chung khi tính tổng bộ nhớ tĩnh.
- Nếu khai báo mảng global cực lớn (ví dụ:
int arr[1'000'000'000];
) thì có thể bị lỗi load-time allocation failure (tùy hệ điều hành).
Ví dụ:
int x; // biến toàn cục chưa khởi tạo
static int y = 0; // khởi tạo bằng 0
4. Stack Segment (Ngăn xếp)
Mục đích:
- Lưu trữ:
- Biến cục bộ
- Tham số hàm
- Địa chỉ trả về (return address)
- Thông tin khung ngăn xếp (stack frame)
Đặc điểm:
- Cấp phát tự động khi hàm được gọi và giải phóng khi kết thúc.
- Không mở rộng tự động (trên hầu hết OS).
- Sắp xếp theo cơ chế Last In First Out (LIFO).
Giới hạn:
- Phụ thuộc hệ điều hành và cấu hình:
- Windows (mặc định): khoảng 1MB/thread (có thể thay đổi bằng
/F
linker flag). - Linux (mặc định): thường 8MB/thread.
- Với các mảng cục bộ lớn như
int a[1'000'000'000];
sẽ gây crash (stack overflow). - Điều chỉnh kích thước stack (main thread):
- Với MSVC (Windows):
- Dùng
/F<size>
trong lệnh biên dịch (cờ này sẽ đẩy lệnh cho trình liên kết thực hiện)cl /F<size> source.cpp
# Ví dụ:
cl /F8388608 main.cpp # Thiết lập stack của main thread là 8 MB. - Dùng
/STACK:<size>
trực tiếp trong lệnh liên kếtlink main.obj /STACK:8388608 # Thiết lập stack của main thread là 8 MB.
- Dùng
- Với GCC/Clang (Windows)
# Dùng tham số linker
-Wl,--stack,<bytes> - Với Linux/MacOS
# Dùng ulimit -s để tăng stack limit trước khi chạy chương trình.
ulimit -s 16384 # 16MB
./a.out
- Với MSVC (Windows):
- Điều chỉnh kích thước stack (sub thread):
- Với POSIX, sử dụng sử dụng
pthread_attr_setstacksize
:#include <pthread.h>
#include <iostream>
void* threadFunc(void*) {
char arr[4 * 1024 * 1024]; // 4MB stack
std::cout << "Thread ran\n";
return nullptr;
}
int main() {
pthread_t t;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 8 * 1024 * 1024); // 8MB
pthread_create(&t, &attr, threadFunc, nullptr);
pthread_join(t, nullptr);
pthread_attr_destroy(&attr);
} - Với mối trường Windows:
HANDLE hThread = CreateThread(
nullptr,
8 * 1024 * 1024, // stack size = 8MB
MyThreadFunc,
nullptr,
0,
nullptr
);
- Với POSIX, sử dụng sử dụng
- Windows (mặc định): khoảng 1MB/thread (có thể thay đổi bằng
Ví dụ:
void func() {
int local = 42; // nằm trên stack
int arr[1000]; // mảng cục bộ trên stack
}
Lưu ý:
- Không phù hợp cho dữ liệu lớn hoặc cấu trúc đệ quy quá sâu.
- Không nên cấp phát mảng lớn trên stack.
5. Heap Segment (Free Store – bộ nhớ cấp phát động)
Mục đích:
- Dành cho bộ nhớ được cấp phát bằng tay hoặc qua smart pointer
Đặc điểm:
- Cấp phát bằng
new
,malloc
; giải phóng bằngdelete
,free
. - Có thể mở rộng gần như không giới hạn (tùy RAM vật lý và hệ điều hành).
- Được quản lý bởi heap allocator (tùy OS hoặc runtime library).
Giới hạn:
- Hạn chế bởi:
- Tổng lượng RAM vật lý.
- Giới hạn ảo của process (ví dụ 2GB trên Windows 32-bit nếu không bật LARGEADDRESSAWARE).
- Fragmentation nếu cấp phát và giải phóng rải rác.
Cơ chế mở rộng:
- Trên Windows: dùng
HeapAlloc()
hoặcVirtualAlloc()
để yêu cầu hệ thống mở thêm vùng ảo. - Trên Linux: sử dụng
brk()
hoặcmmap()
.
Rủi ro:
- Memory Leak: nếu không giải phóng.
- Dangling Pointer: trỏ vào vùng đã bị
delete
. - Fragmentation: giảm hiệu năng và tăng thất thoát bộ nhớ.
Ví dụ:
int* p = new int[1000000]; // cấp phát trên heap
delete[] p;
6. Memory-Mapped Regions (DLLs, shared libraries, file mapping)
Mục đích:
- Các thư viện động (
.dll
,.so
), hoặc file được ánh xạ vào bộ nhớ.
Đặc điểm:
- Tự động ánh xạ bởi hệ điều hành khi liên kết động.
- Có thể chia sẻ giữa các tiến trình (nếu không ghi).
- Kích thước linh động, phụ thuộc file.
7. Ví dụ:
Giả định với mảng cực lớn: int arr[1'000'000'000];
Khai báo trong hàm
void func() {
int arr[1'000'000'000]; // ~4 GB
}
- Lỗi: Gây stack overflow ngay lập tức (vì vượt giới hạn stack).
Khai báo global
int arr[1'000'000'000]; // toàn cục ~4 GB
Tùy hệ điều hành:
- Trên hệ thống 64-bit, có thể thành công nếu đủ RAM/virtual memory.
- Trên 32-bit, khả năng cao lỗi do vùng data + BSS bị giới hạn < 2GB.
Dùng heap
int arr[1'000'000'000]; // toàn cục ~4 GB
- Thành công trên hệ thống 64-bit (nếu còn đủ RAM).
- Best practice trong trường hợp cần mảng cực lớn.
8. Khuyến nghị:
- Không khai báo mảng lớn trên stack.
- Dùng heap để xử lý dữ liệu lớn, kết hợp
std::vector
, smart pointer. - Luôn kiểm soát vùng nhớ cấp phát, dùng RAII, smart pointer như
std::unique_ptr
,std::shared_ptr
. - Tránh dùng biến toàn cục tràn lan, ảnh hưởng khả năng tái sử dụng và unit test.
- Kiểm tra rò rỉ bằng công cụ như:
- Windows: CRT Debug Heap, Visual Studio Leak Detection
- Linux: Valgrind, ASan, Massif