Trong bài này chúng ta sẽ học cấp phát động trong C++ với new và delete: bộ nhớ stack và heap khác nhau thế nào, cách cấp phát biến, mảng và đối tượng trên heap, và 4 lỗi kinh điển là rò rỉ bộ nhớ, con trỏ treo, delete hai lần, dùng nhầm delete với delete[]. Đây là phần nền để hiểu vì sao C++ hiện đại khuyên dùng smart pointer thay cho new/delete trực tiếp.
Bài 12 trong Serie Lập trình C++ từ A tới Z
Bộ nhớ stack và heap
Chương trình C++ dùng 2 vùng nhớ chính để lưu biến:
- Stack chứa biến cục bộ và tham số hàm. Vào hàm thì biến được tạo, ra khỏi hàm thì tự hủy. Nhanh, tự động, nhưng nhỏ và kích thước phải biết lúc biên dịch.
- Heap (còn gọi là free store) là vùng nhớ lớn mà chương trình xin cấp lúc chạy bằng
new. Vùng nhớ tồn tại cho tới khi chương trình tự trả lại bằngdelete.
Ví dụ đời thường: stack giống bàn làm việc, xong việc thì dọn sạch. Heap giống tủ đồ thuê ngoài: muốn dùng bao lâu cũng được, nhưng phải tự nhớ đi trả, quên trả thì tủ bị chiếm mãi.
Mọi biến trong các bài trước đều nằm trên stack. Vậy khi nào cần heap? Khi biến phải sống lâu hơn hàm tạo ra nó, khi kích thước chỉ biết lúc chạy, hoặc khi dữ liệu quá lớn so với stack.
Trả về địa chỉ biến cục bộ
Đây là lý do rõ nhất để cần heap:
#include <iostream>
int* taoSoLuongSai()
{
int soLuong = 5; // bien cuc bo nam tren stack
return &soLuong; // sai: bien bi huy khi ham ket thuc
}
int* taoSoLuongDung()
{
int* p = new int(5); // cap phat tren heap, ton tai toi khi delete
return p;
}
int main()
{
int* q = taoSoLuongDung();
std::cout << "Gia tri tren heap: " << *q << '\n';
delete q;
return 0;
}
taoSoLuongSai trả về địa chỉ của soLuong, nhưng soLuong nằm trên stack và bị hủy ngay khi hàm kết thúc. Người gọi nhận về một địa chỉ trỏ vào vùng nhớ đã bị thu hồi. g++ cảnh báo address of local variable returned. Còn taoSoLuongDung cấp phát trên heap bằng new, vùng nhớ vẫn còn sau khi hàm kết thúc.
Cấp phát động trong C++ với new và delete
new KieuDuLieu(giaTri)cấp phát một biến trên heap, trả về con trỏ tới nó.delete conTrotrả vùng nhớ đó lại cho hệ thống.
Mỗi new phải có đúng một delete tương ứng, không thiếu, không thừa. Chương trình ở trên làm đúng điều này: new int(5) trong hàm, delete q trong main.
Nếu đã học cấp phát động với malloc trong C, new/delete tương ứng với malloc/free. Khác biệt quan trọng: new gọi hàm khởi tạo và delete gọi hàm hủy của đối tượng, còn malloc/free chỉ cấp phát byte thô. Vì vậy trong C++ không dùng malloc cho đối tượng.
Cấp phát mảng động với new[] và delete[]
Mảng thô ở bài mảng và con trỏ trong C++ phải biết kích thước lúc biên dịch. Mảng trên heap thì kích thước quyết định lúc chạy:
#include <iostream>
int main()
{
int n;
std::cout << "So dau sach: ";
std::cin >> n;
int* soLuong = new int[n]; // mang co kich thuoc biet luc chay
for (int i = 0; i < n; i++)
soLuong[i] = (i + 1) * 10;
std::cout << "So luong:";
for (int i = 0; i < n; i++)
std::cout << ' ' << soLuong[i];
std::cout << '\n';
delete[] soLuong; // mang thi phai delete[]
soLuong = nullptr;
return 0;
}
Cấp phát bằng new[] thì phải trả bằng delete[], có cặp ngoặc vuông. Sau khi delete, gán con trỏ về nullptr để lỡ dùng lại thì chương trình crash ngay chứ không âm thầm đọc rác.
Trên thực tế, mảng có kích thước lúc chạy thì dùng std::vector như bài string và vector, nó làm đúng việc new[]/delete[] ở trên nhưng tự động. Ví dụ này chỉ để các bạn hiểu bên dưới vector làm gì.
Cấp phát đối tượng
new với class sẽ gọi hàm khởi tạo, delete sẽ gọi hàm hủy:
#include <iostream>
#include <string>
class Sach
{
public:
Sach(const std::string& ten) : ten_(ten) { std::cout << " Tao sach '" << ten_ << "'\n"; }
~Sach() { std::cout << " Huy sach '" << ten_ << "'\n"; }
const std::string& ten() const { return ten_; }
private:
std::string ten_;
};
int main()
{
std::cout << "new:\n";
Sach* s = new Sach("Lap trinh C++");
std::cout << "Dang dung: " << s->ten() << '\n';
std::cout << "delete:\n";
delete s;
std::cout << "Ket thuc main\n";
return 0;
}
Đối tượng tạo bằng new dùng qua con trỏ với toán tử ->. Hàm hủy chạy đúng lúc gọi delete, không phải lúc kết thúc khối lệnh như đối tượng trên stack. Đây cũng là cách các đối tượng đa hình ở bài tính đa hình trong C++ được tạo ra.
4 lỗi kinh điển khi dùng new và delete
1. Rò rỉ bộ nhớ (memory leak)
Quên delete thì vùng nhớ bị chiếm cho tới khi chương trình tắt:
#include <iostream>
void xuLyPhieuMuon()
{
int* boDem = new int[1000]; // 4000 byte
boDem[0] = 1;
// quen delete[] boDem;
}
int main()
{
for (int i = 0; i < 3; i++)
xuLyPhieuMuon();
std::cout << "Xu ly xong 3 phieu muon\n";
return 0;
}
Chạy bình thường thì không có dấu hiệu gì. Biên dịch với -fsanitize=address thì khi chương trình kết thúc, LeakSanitizer báo rò rỉ 12000 byte trong 3 lần cấp phát, chỉ đúng dòng 5 là chỗ new. Mỗi phiếu mượn mất 4000 byte, một server chạy liên tục xử lý triệu phiếu thì sẽ hết RAM.
2. Con trỏ treo (dangling pointer)
Dùng con trỏ sau khi đã delete:
#include <iostream>
int main()
{
int* soLuong = new int(5);
delete soLuong;
std::cout << "Sau delete: " << *soLuong << '\n'; // dung vung nho da tra
return 0;
}
Lần chạy thường in ra một số rác, không báo lỗi gì. AddressSanitizer bắt được và gọi tên đúng lỗi: heap-use-after-free, đọc vùng nhớ heap sau khi đã giải phóng.
3. delete hai lần
#include <iostream>
int main()
{
int* soLuong = new int(5);
delete soLuong;
delete soLuong; // xoa lan hai
std::cout << "Xong\n";
return 0;
}
Lần này chương trình bị hệ thống dừng ngay với thông báo double free detected. Trong chương trình lớn, lỗi này thường xảy ra khi 2 đối tượng cùng giữ một con trỏ và cả hai đều nghĩ mình phải delete.
4. new[] mà delete thường
int main()
{
int* ds = new int[10];
delete ds; // sai: mang phai dung delete[]
return 0;
}
Biên dịch và chạy bình thường đều không có dấu hiệu gì, nhưng đây là undefined behavior. Với mảng đối tượng, chỉ phần tử đầu được gọi hàm hủy. AddressSanitizer báo alloc-dealloc-mismatch.
Lưu ý: Cả 4 lỗi trên đều biên dịch được và nhiều khi chạy vẫn “có vẻ đúng”. Khi viết code có new/delete, hãy luôn chạy thử một lần với -g -fsanitize=address. Cờ này bắt được tất cả các lỗi trong bài, chỉ đúng dòng code gây lỗi.
Vì sao C++ hiện đại hạn chế new và delete?
Nhìn lại 4 lỗi trên, chúng có chung một nguyên nhân: lập trình viên phải tự nhớ ai sở hữu vùng nhớ và khi nào trả. Chương trình càng lớn, có càng nhiều nhánh if, return giữa chừng hay ngoại lệ, thì càng dễ quên.
C++ giải quyết bằng kỹ thuật RAII: gắn vùng nhớ với một đối tượng trên stack, đối tượng đó tự delete trong hàm hủy. Các bạn đã dùng RAII mà không biết:
std::stringvàstd::vectorcấp phát trên heap bên trong, và tự giải phóng khi bị hủy.std::unique_ptrvàstd::shared_ptr(smart pointer) là con trỏ tựdeleteđối tượng nó trỏ tới.
Quy tắc chung trong code C++ hiện đại: gần như không bao giờ viết new và delete trực tiếp. Cần mảng động thì dùng std::vector, cần một đối tượng trên heap thì dùng std::make_unique. Hiểu new/delete vẫn cần thiết để đọc code cũ, viết thư viện cấp thấp, và hiểu các công cụ kia làm gì bên dưới.
Bài tập
- Viết hàm
int* taoMang(int n)cấp phát mảng n phần tử trên heap, gán giá trị từ 1 tới n rồi trả về. Trongmain, in mảng và giải phóng đúng cách. - Viết lại bài 1 bằng
std::vector<int>. So sánh số dòng code và số chỗ có thể quên giải phóng. - Cố tình quên
delete[]trong bài 1, biên dịch với-fsanitize=addressvà đọc báo cáo. - Cho class
Sachcó hàm hủy in ra màn hình. Cấp phátnew Sach[3]rồi giải phóng bằngdelete(không có ngoặc vuông). Hàm hủy được gọi mấy lần?
Lời giải sẽ có trong bài tập tổng hợp của chương nhé.
Kết
Cấp phát động trong C++ với new và delete cho phép tạo dữ liệu sống lâu hơn hàm và có kích thước quyết định lúc chạy, nhưng đi kèm trách nhiệm tự giải phóng. 4 lỗi cần nhớ: quên delete gây rò rỉ, dùng sau delete là con trỏ treo, delete hai lần, và new[] phải đi với delete[]. Công cụ AddressSanitizer bắt được cả 4.
Muốn tra cứu chi tiết về new và delete, các bạn xem thêm tài liệu chuẩn trên cppreference.
Bài tiếp theo chúng ta sẽ học smart pointer unique_ptr và shared_ptr, cách C++ hiện đại loại bỏ gần hết các lỗi trên. Nếu thấy bài viết hữu ích, hãy chia sẻ cho bạn bè nhé.




![Cấp phát động trong C++ với new và delete: stack, heap và rò rỉ bộ nhớ 4 Cấp phát động trong C++: mảng có kích thước nhập lúc chạy cấp bằng new[] rồi giải phóng bằng delete[]](https://khuenguyencreator.com/wp-content/uploads/2026/10/ket-qua-new-mang-dong.png)

![Cấp phát động trong C++ với new và delete: stack, heap và rò rỉ bộ nhớ 6 LeakSanitizer phát hiện rò rỉ 12000 byte khi quên delete[] trong hàm](https://khuenguyencreator.com/wp-content/uploads/2026/10/loi-ro-ri-bo-nho-leak-sanitizer.png)


![Cấp phát động trong C++ với new và delete: stack, heap và rò rỉ bộ nhớ 9 Cấp phát bằng new[] mà giải phóng bằng delete, AddressSanitizer báo alloc-dealloc-mismatch](https://khuenguyencreator.com/wp-content/uploads/2026/10/loi-new-mang-delete-thuong.png)