Bộ nhớ động: mới, xóa & Mối nguy hiểm của chúng
Tìm hiểu cách phân bổ động hoạt động với tính năng mới và xóa, hiểu sự nguy hiểm của rò rỉ bộ nhớ, trống kép và không sử dụng sau này, đồng thời khám phá lý do C++ hiện đại không chứa dữ liệu mới/xóa thô.
Phân bổ động với tính năng mới và xóa
ngăn xếp nhanh nhưng bị giới hạn về kích thước và phạm vi. Khi bạn cần các đối tượng tồn tại lâu hơn phạm vi tạo của chúng hoặc có kích thước không xác định tại thời điểm biên dịch, bạn phân bổ trên heap (cửa hàng miễn phí).
new T(args)phân bổ bộ nhớ cho mộtT, gọi hàm tạo của nó và trả vềT*.delete pgọi hàm hủy và giải phóng bộ nhớ. Đối với mảng:new T[n]vàdelete[] p. newkhông khớp vớidelete[]hoặcnew[]vớideletelà hành vi không xác định — trình biên dịch sẽ không phát hiện được điều này.
mới, xóa và các biến thể mảng của chúng
Đây là API phân bổ thô. Nghiên cứu nó để bạn nhận ra nó trong mã cũ, nhưng không bao giờ viết mã này bằng mã mới — thay vào đó hãy sử dụng con trỏ thông minh.
#include <iostream>
#include <cstring>
struct Widget {
int id;
Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};
int main() {
// Single object
Widget* w = new Widget(1);
std::cout << "Using widget " << w->id << '\n';
delete w; // destructor called, memory freed
// Array
Widget* arr = new Widget[3]{{10}, {20}, {30}};
for (int i = 0; i < 3; ++i)
std::cout << arr[i].id << ' ';
std::cout << '\n';
delete[] arr; // MUST use delete[] for new[]
// UNDEFINED BEHAVIOR — don't do these:
// delete arr; // wrong: used delete instead of delete[]
// delete[] w; // wrong: used delete[] instead of delete
}Vị trí mới
Vị trí mới xây dựng một đối tượng tại một địa chỉ bộ nhớ cụ thể mà không cần phân bổ. Nó được sử dụng bởi các bộ cấp phát, nhóm bộ nhớ và các vùng chứa nhưstd::vectortrong nội bộ. Bạn phải gọi hàm hủy một cách rõ ràng - KHÔNG được sử dụngdeletetrên các đối tượng có vị trí mới.
#include <iostream>
#include <new> // required for placement new
#include <cstddef> // std::byte
#include <memory> // std::align
struct Sensor {
int id;
double reading;
Sensor(int i, double r) : id(i), reading(r) {
std::cout << "Sensor " << id << " init\n";
}
~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};
int main() {
// Pre-allocate raw storage (aligned)
alignas(Sensor) std::byte buffer[sizeof(Sensor)];
// Construct in pre-allocated memory
Sensor* s = new (buffer) Sensor(42, 3.14);
std::cout << "Reading: " << s->reading << '\n';
// Must destroy explicitly — do NOT use delete
s->~Sensor(); // explicit destructor call
// delete s; // WRONG — memory was not allocated by new
}Ba lỗi trí nhớ chết người
new/deletethô khiến bạn gặp phải ba loại lỗi:
1. Rò rỉ bộ nhớ - phân bổ bằngnewnhưng không bao giờ gọidelete. Bộ nhớ không bao giờ được trả lại cho HĐH (cho đến khi thoát khỏi quá trình). Trong một máy chủ hoạt động lâu dài, điều này cuối cùng sẽ tiêu tốn hết RAM có sẵn.
2. Double Free — gọideletetrên cùng một con trỏ hai lần.deletethứ hai làm hỏng cấu trúc dữ liệu nội bộ của bộ cấp phát heap, thường dẫn đến sự cố sau này ở mã không liên quan. Điều này cực kỳ khó gỡ lỗi.
3. Sử dụng sau khi miễn phí — truy cập bộ nhớ saudelete. Bộ nhớ có thể đã được sử dụng lại cho mục đích phân bổ khác, vì vậy bạn có thể đang đọc hoặc ghi dữ liệu của đối tượng khác. Đây là một lỗ hổng bảo mật (lỗi lớp CVE).
Hãy xem xét:f(new A(), new B()). Trình biên dịch có thể đánh giá các đối số theo bất kỳ thứ tự nào và có thể xen kẽ chúng:
1. Cấp phát bộ nhớ cho A
2. Cấp phát bộ nhớ cho B
3. Xây dựng A
4. Xây dựng B
Nếu bước 4 không thực hiện được, A sẽ bị rò rỉ - không ai gọideletetrên đó. Ngay cảf(std::shared_ptr(new A()), std::shared_ptr(new B()))cũng gặp vấn đề này trước C++17 (đảm bảo rằng mỗi đối số được đánh giá đầy đủ trước đối số tiếp theo). Cách khắc phục: luôn sử dụngstd::make_uniquehoặcstd::make_shared — chúng thực hiện phân bổ và xây dựng dưới dạng một hoạt động duy nhất, an toàn ngoại lệ.
Tổng quan về bộ phân bổ tùy chỉnh
new/deletemặc định sử dụng bộ cấp phát toàn cầu (thường làmalloc/free), có mục đích chung nhưng không tối ưu cho tất cả các mẫu. Bộ phân bổ tùy chỉnh cung cấp:
- Bộ cấp phát đấu trường/tuyến tính — chạm vào con trỏ, giải phóng mọi thứ cùng một lúc. Tuyệt vời cho dữ liệu trò chơi trên mỗi khung hình.
- Bộ phân bổ nhóm — phân bổ trước các khối có kích thước cố định. O(1) cấp phát/miễn phí, không bị phân mảnh.
- Bộ phân bổ ngăn xếp — Phân bổ LIFO, cực kỳ nhanh.
Các thùng chứa C++ chấp nhận các tham số mẫu cấp phát:std::vector. Không gian tênstd::pmr(tài nguyên bộ nhớ đa hình) trong C++17 cung cấp khung cấp phát đa hình thời gian chạy để tránh vấn đề tham số mẫu.
Trong C++ hiện đại (C++14 trở lên), mã ứng dụng được viết tốt phải chứa zero rawnewvàdelete:
- Sở hữu duy nhất: sử dụngstd::make_unique
- Sở hữu chung: sử dụngstd::make_shared
- Mảng động: sử dụngstd::vector
- Chuỗi: sử dụngstd::string
- Đối tượng tùy chọn: sử dụngstd::optional
new/deletethô có thể xuất hiện trong mã thư viện (bộ cấp phát, vùng chứa tùy chỉnh), nhưng mã ứng dụng sẽ không bao giờ cần đến chúng. Nếu bạn thấynewtrong quá trình đánh giá mã, hãy hỏi tại sao — hầu như luôn có giải pháp thay thế an toàn hơn.
new/delete[]hoặcnew[]/deletekhông khớp là hành vi không xác định- Rò rỉ bộ nhớ, double-free và use-after-free là ba lỗi bộ nhớ chết người
f(new A(), new B())có thể bị rò rỉ nếu hàm tạo ném (trước C++17 ngay cả với trình bao bọc Shared_ptr)- Vị trí các cấu trúc mới tại một địa chỉ nhất định - hủy thủ công, không bao giờ sử dụng
delete - Mã ứng dụng C++ hiện đại không được có
new/deletethô - sử dụng con trỏ và vùng chứa thông minh
Câu đố — Kiểm tra kiến thức
(15 XP)1. Điều gì xảy ra nếu bạn sử dụng`delete`trên bộ nhớ được phân bổ bằng`new[]`?
2. Tại sao`f(new A(), new B())`có thể rò rỉ bộ nhớ?
3. Bạn nên hủy một đối tượng được tạo bằng vị trí mới như thế nào?