Bỏ qua để đến nội dung

Cấp phát bộ nhớ động với new/delete

Đến giờ, mọi biến bạn khai báo đều được cấp phát tự động và giải phóng khi ra khỏi phạm vi (scope) - đây gọi là bộ nhớ stack. Nhưng đôi khi bạn cần bộ nhớ tồn tại lâu hơn, hoặc kích thước chỉ biết được lúc chạy chương trình - đó là lúc dùng bộ nhớ heap (cấp phát động).

Stack Heap
Cấp phát Tự động, khi khai báo biến Thủ công, bằng new
Giải phóng Tự động, khi ra khỏi scope Thủ công, bằng delete
Tốc độ Rất nhanh Chậm hơn stack
Kích thước Giới hạn (thường vài MB) Lớn hơn nhiều (giới hạn bởi RAM)
Thời gian sống Kết thúc khi hàm/khối lệnh kết thúc Tồn tại đến khi bị delete
#include <iostream>
int main() {
int* ptr = new int; // Cấp phát một vùng nhớ int trên heap
*ptr = 42; // Gán giá trị
std::cout << *ptr << std::endl; // 42
delete ptr; // BẮT BUỘC: giải phóng vùng nhớ khi không dùng nữa
ptr = nullptr; // Thói quen tốt: tránh dangling pointer sau khi delete
return 0;
}

Có thể khởi tạo giá trị ngay khi cấp phát:

int* ptr = new int(42); // Cấp phát và khởi tạo giá trị 42 cùng lúc

Nếu bạn new mà quên delete, vùng nhớ đó sẽ không bao giờ được giải phóng cho đến khi chương trình kết thúc - đây gọi là rò rỉ bộ nhớ (memory leak):

void leaky_function() {
int* ptr = new int(42);
// Quên delete ptr! Vùng nhớ này bị "mất dấu" mãi mãi khi hàm kết thúc
} // ptr (biến con trỏ) bị hủy, nhưng vùng nhớ nó trỏ đến VẪN CÒN bị chiếm dụng

Nếu hàm này được gọi hàng triệu lần, chương trình sẽ dần dần chiếm hết bộ nhớ RAM và có thể bị hệ điều hành buộc dừng (crash). Mọi lần new phải có đúng một lần delete tương ứng.

Khi cần cấp phát một mảng có kích thước chỉ biết được lúc chạy chương trình:

#include <iostream>
int main() {
int size;
std::cout << "Nhap so luong phan tu: ";
std::cin >> size;
int* arr = new int[size]; // Cấp phát mảng động với kích thước "size"
for (int i = 0; i < size; i++) {
arr[i] = i * 10;
}
for (int i = 0; i < size; i++) {
std::cout << arr[i] << " ";
}
delete[] arr; // Dùng delete[] (có ngoặc vuông) cho mảng, KHÔNG dùng delete thường
return 0;
}

⚠️ Rất quan trọng: mảng cấp phát bằng new[] phải được giải phóng bằng delete[], không phải delete. Dùng sai sẽ gây ra hành vi không xác định (undefined behavior).

Gọi delete hai lần trên cùng một con trỏ cũng là lỗi nghiêm trọng:

int* ptr = new int(42);
delete ptr;
// delete ptr; // NGUY HIỂM! Giải phóng vùng nhớ đã được giải phóng trước đó

Gán nullptr cho con trỏ ngay sau khi delete là một thói quen tốt để tránh lỗi này, vì delete nullptr; là an toàn (không làm gì cả).

Trong C++ hiện đại, việc dùng trực tiếp new/delete ngày càng ít gặp, vì:

  • std::vector đã tự quản lý mảng động cho bạn
  • Smart pointers (std::unique_ptr, std::shared_ptr - sẽ học ở bài riêng) tự động gọi delete đúng lúc, giúp tránh rò rỉ bộ nhớ

Tuy nhiên, hiểu rõ new/delete là nền tảng bắt buộc để hiểu cách các công cụ hiện đại đó hoạt động bên dưới.

  • new cấp phát bộ nhớ trên heap, tồn tại cho đến khi bị delete một cách tường minh
  • Mỗi new phải có đúng một delete tương ứng - quên delete gây rò rỉ bộ nhớ (memory leak)
  • Mảng cấp phát bằng new[] phải giải phóng bằng delete[]
  • Gán nullptr sau khi delete để tránh lỗi giải phóng hai lần (double free)