Menu

Bộ nhớ động trong C++: giải thích new và delete

Cách cấp phát bộ nhớ lúc chạy bằng new, giải phóng bằng delete, và tránh rò rỉ bộ nhớ, con trỏ treo, double free khi tự quản lý heap.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Vì sao cần xin bộ nhớ lúc chạy

Đến giờ mọi biến bạn tạo đều sống trên stack: kích thước biết trước lúc biên dịch và tự động bị hủy khi phạm vi kết thúc. Cách đó nhanh và an toàn, nhưng không xử lý được trường hợp bạn chỉ biết cần bao nhiêu bộ nhớ khi chương trình đang chạy: một buffer có kích thước theo dữ liệu người dùng nhập, một cấu trúc phải sống lâu hơn hàm đã tạo ra nó, hay một đồ thị mà hình dạng không biết trước.

Với những trường hợp đó, C++ cho phép bạn cấp phát từ heap (còn gọi là free store) bằng new, và trả lại bằng delete. Biểu thức new dành ra một khối nhớ, chạy hàm khởi tạo và trả về con trỏ tới khối đó, dựa ngay trên kiến thức về con trỏ bạn đã thấy ở trang trước.

Bản thân biến p sống trên stack, nó chỉ là một con trỏ. Còn số int mà nó trỏ tới thì sống trên heap và tồn tại cho đến khi bạn delete nó, bất kể bao nhiêu phạm vi đến rồi đi.

Stack và heap

Sự khác biệt này chính là lý do new tồn tại, nên đáng để làm rõ bằng ví dụ cụ thể.

void demo() {
    int a = 10;            // on the stack - gone when demo() returns
    int* b = new int(10);  // 'b' on the stack, the int it points to on the heap
}                          // 'a' destroyed; the heap int LEAKS - never deleted

Những khác biệt chính:

  • Stack: vòng đời tự động, rất nhanh, kích thước giới hạn (thường vài MB), được giải phóng giúp bạn khi ra khỏi phạm vi.
  • Heap: vòng đời do bạn quản lý, chậm hơn một chút, dung lượng lớn, và chỉ được giải phóng khi bạn gọi delete.

Đây là sự đánh đổi giữa linh hoạt và trách nhiệm: bộ nhớ heap sống đúng bao lâu bạn muốn, nhưng bạn trở thành người phải nhớ giải phóng nó.

Cấp phát mảng với new[]

Khi cần một khối nhớ có độ dài quyết định lúc chạy, hãy dùng dạng mảng new T[n]. Nó trả về con trỏ tới phần tử đầu tiên, và bạn giải phóng bằng delete[] tương ứng.

Quy tắc này nghiêm ngặt và dễ sai: bộ nhớ từ new được giải phóng bằng delete, còn bộ nhớ từ new[] được giải phóng bằng delete[]. Trộn lẫn hai thứ, như delete arr với vùng nhớ cấp phát bằng new[], là undefined behavior, kể cả khi trên máy bạn nó có vẻ vẫn chạy.

Ba bug kinh điển

Tự quản lý bộ nhớ có một nhóm nhỏ lỗi chiếm phần lớn các bug liên quan tới heap. Hãy học cách nhận ra cả ba.

1. Rò rỉ bộ nhớ: bạn không bao giờ gọi delete. Khối nhớ bị giữ mãi mãi. Một lần thì vô hại, nhưng nằm trong vòng lặp thì chết người.

void leaky() {
    int* p = new int(5);
    // ... no delete ...
}   // p is gone; the heap int is now unreachable AND unfreed

2. Con trỏ treo (dangling pointer): bạn dùng vùng nhớ sau khi đã giải phóng. Con trỏ vẫn giữ địa chỉ cũ, nhưng vùng nhớ đó không còn là của bạn nữa.

3. Double free: bạn delete cùng một khối nhớ hai lần. Việc này làm hỏng dữ liệu quản lý của heap và thường làm chương trình sập.

int* p = new int(1);
delete p;
delete p;   // double-free - undefined behavior, often a crash

Gán con trỏ về nullptr sau khi xóa sẽ vô hiệu hóa cả lỗi dùng con trỏ treo lẫn double free: giải tham chiếu nullptr làm chương trình sập ngay (dễ gỡ lỗi), còn delete nullptr được quy định rõ là không làm gì và an toàn.

Một chu trình cấp phát, sử dụng, giải phóng thực tế

Ghép tất cả lại, đây là hình dạng của việc tự quản lý bộ nhớ đúng cách: cấp phát, sử dụng, giải phóng đúng một lần, và không đụng tới con trỏ sau đó.

Để ý delete u làm hai việc với một kiểu lớp: đầu tiên nó chạy hàm hủy của đối tượng, rồi mới trả lại vùng nhớ thô. Thứ tự đó quan trọng khi đối tượng của bạn sở hữu tài nguyên riêng.

Một điểm dễ nhầm tinh tế: nếu có ngoại lệ bị ném ra giữa new và delete, lệnh delete không bao giờ chạy và bạn bị rò rỉ. Bọc mọi lần cấp phát trong try/catch để xử lý chuyện đó vừa tẻ nhạt vừa dễ sai, và đó chính là vấn đề trang tiếp theo giải quyết.

Tiếp theo: smart pointer

Giờ bạn đã thấy cái giá đầy đủ của việc tự quản lý bộ nhớ: mỗi new là một lời hứa sẽ delete về sau, và chỉ một lần quên, giải phóng hai lần hay giải phóng quá sớm cũng là undefined behavior. C++ hiện đại gần như không bao giờ hứa điều đó bằng tay. Trang tiếp theo giới thiệu smart pointer, gồm std::unique_ptr và std::shared_ptr: những đối tượng sở hữu một vùng nhớ trên heap và tự động gọi delete giúp bạn khi ra khỏi phạm vi, biến cả ba bug kinh điển thành việc trình biên dịch và RAII lo thay bạn.

Câu hỏi thường gặp

new và delete trong C++ khác nhau thế nào?

new cấp phát bộ nhớ trên heap lúc chạy và trả về một con trỏ tới vùng nhớ đó; delete giải phóng vùng nhớ đã được cấp phát bằng new. Mỗi new phải đi kèm đúng một delete, nếu không bạn sẽ bị rò rỉ bộ nhớ. Với mảng, dùng new[] cùng delete[].

Chuyện gì xảy ra nếu quên gọi delete trong C++?

Bạn bị rò rỉ bộ nhớ (memory leak): khối nhớ trên heap vẫn bị giữ suốt thời gian chương trình chạy dù không còn gì trỏ tới nó. Một lần rò rỉ thường vô hại, nhưng rò rỉ trong vòng lặp hay trong một dịch vụ chạy lâu sẽ tăng dần cho tới khi chương trình hết bộ nhớ và sập.

Có nên dùng trực tiếp new và delete trong C++ hiện đại không?

Hiếm khi. Hãy ưu tiên container như std::vector hoặc smart pointer (std::unique_ptr, std::shared_ptr), những thứ tự giải phóng bộ nhớ. new/delete thô đáng để hiểu vì smart pointer bọc chúng bên trong, nhưng trong code hằng ngày chúng là nguồn gây rò rỉ bộ nhớ và con trỏ treo.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu