Để hiểu rõ cách C++ hoạt động với bộ nhớ, việc nắm bắt cấu trúc phân bố bộ nhớ là rất quan trọng. Hệ điều hành quản lý bộ nhớ của một chương trình đang chạy bằng cách chia thành nhiều vùng riêng biệt, mỗi vùng có mục đích và đặc tính khác nhau:
- Vùng Mã (Code Segment): Lưu trữ mã thực thi của chương trình và các hằng số chỉ đọc (ví dụ: chuỗi ký tự hằng). Đây là vùng bộ nhớ chỉ đọc.
- Vùng Dữ liệu (Data Segment / Static Segment): Chứa các biến toàn cục (global variables) và biến tĩnh (static variables), cả biến toàn cục tĩnh lẫn biến cục bộ tĩnh. Vùng này được cấp phát khi chương trình khởi động.
- Vùng Ngăn xếp (Stack Segment): Được sử dụng cho các biến cục bộ không tĩnh, tham số hàm, giá trị trả về của hàm và quản lý ngữ cảnh hàm. Vùng stack phát triển theo chiều giảm địa chỉ. Việc cấp phát và giải phóng trên stack diễn ra tự động.
- Vùng Bộ nhớ Động (Heap Segment): Vùng này dành cho việc cấp phát bộ nhớ động trong quá trình chạy chương trình. Lập trình viên chịu trách nhiệm cấp phát và giải phóng bộ nhớ trên heap. Vùng heap phát triển theo chiều tăng địa chỉ.
- Vùng Ánh xạ Bộ nhớ (Memory-mapped Segment): Dùng cho I/O hiệu quả hoặc chia sẻ bộ nhớ giữa các tiến trình.
Việc phân chia bộ nhớ thành các vùng như vậy giúp hệ điều hành quản lý tài nguyên hiệu quả hơn và áp đặt các quy tắc bảo mật khác nhau cho từng loại dữ liệu. Trong số các vùng này, vùng heap là nơi lập trình viên có quyền kiểm soát trực tiếp nhất để cấp phát và giải phóng bộ nhớ theo nhu cầu ứng dụng.
Ví dụ Minh Họa Vị Trí Biến Trong Bộ Nhớ
Hãy xem xét một ví dụ minh họa để hiểu rõ hơn về vị trí các biến trong các vùng bộ nhớ:
#include <iostream>
#include <cstring> // Cho strlen
// Biến toàn cục (Global variable) - Vùng Dữ liệu
int globalCounter = 100;
// Biến toàn cục tĩnh (Static global variable) - Vùng Dữ liệu
static int staticGlobalFlag = 200;
void demonstrateMemoryAreas() {
// Biến cục bộ (Local variable) - Vùng Ngăn xếp
int localValue = 30;
// Biến cục bộ tĩnh (Static local variable) - Vùng Dữ liệu
static int staticLocalCount = 40;
// Chuỗi ký tự hằng (String literal) - Vùng Mã
const char* message = "Hello Memory!";
// Mảng ký tự cục bộ (Local character array) - Vùng Ngăn xếp
char buffer[] = "BufferData";
// Cấp phát động một số nguyên (Dynamic int) - Vùng Heap
int* dynamicNumber = new int(500);
std::cout << "Dia chi globalCounter (&globalCounter): " << &globalCounter << " (Vung du lieu)" << std::endl;
std::cout << "Dia chi staticGlobalFlag (&staticGlobalFlag): " << &staticGlobalFlag << " (Vung du lieu)" << std::endl;
std::cout << "Dia chi localValue (&localValue): " << &localValue << " (Vung ngan xep)" << std::endl;
std::cout << "Dia chi staticLocalCount (&staticLocalCount): " << &staticLocalCount << " (Vung du lieu)" << std::endl;
std::cout << "Dia chi chuoi message (&message): " << &message << " (Vung ngan xep - con tro)" << std::endl;
std::cout << "Dia chi du lieu message (*message): " << (void*)message << " (Vung ma - du lieu chuoi)" << std::endl;
std::cout << "Dia chi buffer (&buffer): " << &buffer << " (Vung ngan xep)" << std::endl;
std::cout << "Dia chi con tro dynamicNumber (&dynamicNumber): " << &dynamicNumber << " (Vung ngan xep - con tro)" << std::endl;
std::cout << "Dia chi du lieu dynamicNumber (*dynamicNumber): " << dynamicNumber << " (Vung heap - du lieu so nguyen)" << std::endl;
// Giải phóng bộ nhớ động
delete dynamicNumber;
}
int main() {
demonstrateMemoryAreas();
return 0;
}
So sánh sizeof và strlen
Hai toán tử/hàm này thường gây nhầm lẫn khi làm việc với chuỗi ký tự trong C/C++:
sizeof:- Là một toán tử (operator), không phải hàm.
- Xác định kích thước bộ nhớ (tính bằng byte) mà một kiểu dữ liệu hoặc một biến chiếm giữ.
- Giá trị của
sizeofđược xác định tại thời điểm biên dịch. - Có thể áp dụng cho bất kỳ kiểu dữ liệu, biến, mảng hoặc cấu trúc nào.
- Đối với con trỏ,
sizeoftrả về kích thước của con trỏ đó (ví dụ: 4 hoặc 8 byte tùy hệ thống), chứ không phải kích thước của dữ liệu mà nó trỏ tới.
strlen:- Là một hàm, nằm trong thư viện
<cstring>(hoặc<string.h>). - Xác định độ dài của một chuỗi ký tự kết thúc bằng ký tự null (
\0). - Giá trị của
strlenđược xác định tại thời điểm chạy bằng cách duyệt qua chuỗi cho đến khi gặp\0. - Chỉ áp dụng cho chuỗi ký tự kiểu C (
char*hoặcchar[]) và không bao gồm ký tự\0trong độ dài trả về.
- Là một hàm, nằm trong thư viện
Ví dụ:
#include <iostream>
#include <cstring> // Cho strlen
void demonstrateSizeofStrlen() {
char fixedArray[] = "Cpp"; // 'C', 'p', 'p', '\0'
const char* ptrToStringLiteral = "Programming"; // Con trỏ trỏ đến chuỗi hằng
char* dynamicBuffer = new char[20];
strcpy(dynamicBuffer, "Dynamic"); // 'D', 'y', 'n', 'a', 'm', 'i', 'c', '\0'
std::cout << "fixedArray: sizeof = " << sizeof(fixedArray) << ", strlen = " << strlen(fixedArray) << std::endl;
// sizeof(fixedArray) tra ve 4 (3 ky tu + \0)
// strlen(fixedArray) tra ve 3 (so ky tu thuc te truoc \0)
std::cout << "ptrToStringLiteral: sizeof = " << sizeof(ptrToStringLiteral) << ", strlen = " << strlen(ptrToStringLiteral) << std::endl;
// sizeof(ptrToStringLiteral) tra ve kich thuoc cua con tro (4 hoac 8 byte)
// strlen(ptrToStringLiteral) tra ve 11 (do dai chuoi)
std::cout << "dynamicBuffer: sizeof = " << sizeof(dynamicBuffer) << ", strlen = " << strlen(dynamicBuffer) << std::endl;
// sizeof(dynamicBuffer) tra ve kich thuoc cua con tro
// strlen(dynamicBuffer) tra ve 7 (do dai chuoi "Dynamic")
delete[] dynamicBuffer;
}
Quản lý Bộ nhớ Động Trong C
Ngôn ngữ C cung cấp các hàm thư viện chuẩn để cấp phát và giải phóng bộ nhớ động trên vùng heap:
malloc(size_t size): Cấp phát một khối bộ nhớ có kích thướcsizebyte. Nội dung của khối bộ nhớ này không được khởi tạo (chứa giá trị "rác"). Trả về một con trỏvoid*đến khối bộ nhớ được cấp phát, hoặcNULLnếu thất bại.calloc(size_t num, size_t size): Cấp phát một mảng gồmnumphần tử, mỗi phần tử có kích thướcsizebyte. Tất cả các bit trong khối bộ nhớ được cấp phát sẽ được khởi tạo thành 0. Trả vềvoid*hoặcNULL.realloc(void* ptr, size_t new_size): Thay đổi kích thước của khối bộ nhớ đã được cấp phát trước đó bởimalloc,callochoặcrealloc.- Nếu
new_sizelớn hơn kích thước hiện tại, khối bộ nhớ có thể được mở rộng tại chỗ hoặc cấp phát một vị trí mới lớn hơn, di chuyển dữ liệu cũ sang và giải phóng khối cũ. - Nếu
new_sizenhỏ hơn, khối bộ nhớ được cắt bớt. - Nếu
ptrlàNULL,reallochoạt động giống nhưmalloc. - Nếu
new_sizelà 0,reallochoạt động giống nhưfree(giải phóng bộ nhớ và trả vềNULL). - Nếu thất bại, nó trả về
NULL, và con trỏ gốcptrvẫn còn hợp lệ và không bị giải phóng.
- Nếu
free(void* ptr): Giải phóng khối bộ nhớ màptrđang trỏ tới. Con trỏptrphải là con trỏ được trả về bởi một trong các hàm cấp phát (malloc,calloc,realloc). Sau khifree,ptrtrở thành "con trỏ treo" (dangling pointer) và nên được gán vềNULLđể tránh lỗi.
Ví dụ về quản lý bộ nhớ kiểu C:
#include <cstdlib> // Cho malloc, free, calloc, realloc
#include <iostream>
void cMemoryExample() {
std::cout << "--- Minh hoa quan ly bo nho kieu C ---" << std::endl;
// Sử dụng malloc
int* arr1 = (int*)malloc(sizeof(int) * 3); // Cấp phát cho 3 số nguyên
if (arr1 == nullptr) {
std::cerr << "Loi cap phat bo nho voi malloc!" << std::endl;
return;
}
std::cout << "malloc: cac phan tu (chua khoi tao): ";
for (int i = 0; i < 3; ++i) {
std::cout << arr1[i] << " "; // Co the chua gia tri rac
}
std::cout << std::endl;
arr1[0] = 10; arr1[1] = 20; arr1[2] = 30;
free(arr1); // Giải phóng
// Sử dụng calloc
int* arr2 = (int*)calloc(4, sizeof(int)); // Cấp phát cho 4 số nguyên, khởi tạo bằng 0
if (arr2 == nullptr) {
std::cerr << "Loi cap phat bo nho voi calloc!" << std::endl;
return;
}
std::cout << "calloc: cac phan tu (khoi tao bang 0): ";
for (int i = 0; i < 4; ++i) {
std::cout << arr2[i] << " ";
}
std::cout << std::endl;
// Sử dụng realloc để mở rộng arr2
int* arr3 = (int*)realloc(arr2, sizeof(int) * 6); // Thay đổi kích thước thành 6 số nguyên
if (arr3 == nullptr) {
std::cerr << "Loi cap phat bo nho voi realloc!" << std::endl;
free(arr2); // Nếu realloc thất bại, arr2 vẫn hợp lệ và cần được giải phóng.
return;
}
std::cout << "realloc: cac phan tu sau khi mo rong (6 phan tu): ";
// 4 phan tu dau van la 0, 2 phan tu moi chua gia tri rac
for (int i = 0; i < 6; ++i) {
std::cout << arr3[i] << " ";
}
std::cout << std::endl;
free(arr3); // Giải phóng arr3 (con trỏ mới trả về từ realloc)
}
Quản lý Bộ nhớ Động Trong C++: Toán tử new và delete
Mặc dù các hàm quản lý bộ nhớ của C vẫn có thể sử dụng trong C++, C++ cung cấp cặp toán tử new và delete để quản lý bộ nhớ động. Các toán tử này được thiết kế để hoạt động tốt hơn với các kiểu dữ liệu của C++, đặc biệt là các đối tượng được định nghĩa bởi người dùng.
new và delete với các kiểu dữ liệu cơ bản
Để cấp phát bộ nhớ cho một biến hoặc một mảng các biến kiểu cơ bản, chúng ta sử dụng new và delete như sau:
void cppBuiltInTypesMemory() {
std::cout << "\n--- new/delete voi kieu du lieu co ban ---" << std::endl;
// Cấp phát một số nguyên đơn lẻ
int* singleInt = new int; // Không khởi tạo (giá trị rác)
std::cout << "Gia tri singleInt (chua khoi tao): " << *singleInt << std::endl;
*singleInt = 10;
std::cout << "Gia tri singleInt (sau khi gan): " << *singleInt << std::endl;
delete singleInt; // Giải phóng
// Cấp phát và khởi tạo một số nguyên
int* initializedInt = new int(123); // Khởi tạo với giá trị 123
std::cout << "Gia tri initializedInt (khoi tao): " << *initializedInt << std::endl;
delete initializedInt; // Giải phóng
// Cấp phát một mảng 5 số thực
double* doubleArray = new double[5]; // Mảng 5 phần tử, không khởi tạo
for (int i = 0; i < 5; ++i) {
doubleArray[i] = i * 1.5;
}
std::cout << "doubleArray: ";
for (int i = 0; i < 5; ++i) std::cout << doubleArray[i] << " ";
std::cout << std::endl;
delete[] doubleArray; // Giải phóng mảng
// Cấp phát và khởi tạo một mảng 4 số nguyên (khởi tạo một phần)
int* partialInitArray = new int[4]{1, 2}; // Hai phần tử đầu là 1, 2; các phần tử còn lại là 0
std::cout << "partialInitArray: ";
for (int i = 0; i < 4; ++i) std::cout << partialInitArray[i] << " ";
std::cout << std::endl;
delete[] partialInitArray;
}
Lưu ý quan trọng: Khi cấp phát bộ nhớ cho một đối tượng đơn lẻ, sử dụng new và delete. Khi cấp phát bộ nhớ cho một mảng đối tượng, phải sử dụng new[] và delete[]. Việc không khớp có thể dẫn đến hành vi không xác định hoặc rò rỉ bộ nhớ, đặc biệt với các kiểu dữ liệu do người dùng định nghĩa.
new và delete với các kiểu dữ liệu do người dùng định nghĩa
Ưu điểm lớn nhất của new và delete là khả năng tự động gọi hàm tạo (constructor) và hàm hủy (destructor) của đối tượng. Điều này không xảy ra với malloc và free.
#include <iostream>
class MyObject {
public:
int value;
MyObject(int v = 0) : value(v) {
std::cout << "MyObject (ID: " << value << ") Constructor called." << std::endl;
}
~MyObject() {
std::cout << "MyObject (ID: " << value << ") Destructor called." << std::endl;
}
void showValue() const {
std::cout << "MyObject's value: " << value << std::endl;
}
};
void cppCustomTypesMemory() {
std::cout << "\n--- new/delete voi kieu du lieu nguoi dung dinh nghia ---" << std::endl;
// Cấp phát một đối tượng MyObject đơn lẻ
MyObject* objPtr1 = new MyObject(); // Gọi hàm tạo MyObject::MyObject(0)
objPtr1->showValue();
delete objPtr1; // Gọi hàm hủy MyObject::~MyObject()
MyObject* objPtr2 = new MyObject(55); // Gọi hàm tạo MyObject::MyObject(55)
objPtr2->showValue();
delete objPtr2; // Gọi hàm hủy MyObject::~MyObject()
// Cấp phát một mảng MyObject
MyObject* objArray = new MyObject[3]; // Gọi hàm tạo mặc định cho mỗi đối tượng
objArray[0].value = 100;
objArray[1].value = 101;
objArray[2].value = 102;
std::cout << "MyObject array values: ";
for (int i = 0; i < 3; ++i) {
objArray[i].showValue();
}
delete[] objArray; // Gọi hàm hủy cho mỗi đối tượng trong mảng
}
Hàm operator new và operator delete
new và delete là các toán tử trong C++, nhưng ở phía dưới, chúng gọi các hàm toàn cục có tên là operator new và operator delete.
- Toán tử
newgọioperator newđể cấp phát bộ nhớ thô. Sau đó, nó gọi hàm tạo để khởi tạo đối tượng trong vùng bộ nhớ đó. - Toán tử
deletegọi hàm hủy của đối tượng. Sau đó, nó gọioperator deleteđể giải phóng bộ nhớ thô.
Các hàm operator new và operator delete mặc định của hệ thống thường được triển khai bằng cách gọi các hàm malloc và free tương ứng của C. Tuy nhiên, operator new có thêm logic xử lý lỗi (chẳng hạn như ném ngoại lệ std::bad_alloc nếu cấp phát thất bại) thay vì trả về NULL như malloc.
Nguyên lý hoạt động của new và delete
Với các kiểu dữ liệu cơ bản:
Khi cấp phát bộ nhớ cho các kiểu dữ liệu cơ bản, new/delete và new[]/delete[] có cơ chế tương tự malloc/free. Sự khác biệt chính là new ném ngoại lệ khi cấp phát thất bại, trong khi malloc trả về NULL.
Với các kiểu dữ liệu do người dùng định nghĩa:
Nguyên lý của new T (cấp phát một đối tượng):
- Gọi hàm
operator new(sizeof(T))để cấp phát một khối bộ nhớ đủ lớn cho đối tượngT. - Trên khối bộ nhớ vừa cấp phát, gọi hàm tạo của lớp
Tđể khởi tạo đối tượng.
Nguyên lý của delete T (giải phóng một đối tượng):
- Gọi hàm hủy của lớp
Tđể thực hiện các công việc dọn dẹp tài nguyên bên trong đối tượng. - Gọi hàm
operator delete(ptr)để giải phóng khối bộ nhớ đã được cấp phát.
Nguyên lý của new T[N] (cấp phát một mảng đối tượng):
- Gọi hàm
operator new[](sizeof(T) * N + extra_bytes). Hàmoperator new[]này thực tế sẽ gọioperator newđể cấp phát bộ nhớ. Một phần bộ nhớ phụ (extra_bytes) thường được cấp phát để lưu trữ số lượng đối tượngNtrong mảng. Điều này cần thiết đểdelete[]biết phải gọi bao nhiêu lần hàm hủy. - Trên vùng bộ nhớ đã cấp phát, lặp lại
Nlần để gọi hàm tạo mặc định cho từng đối tượng trong mảng.
Nguyên lý của delete[] T (giải phóng một mảng đối tượng):
- Sử dụng thông tin số lượng đối tượng đã được lưu trữ (từ
extra_bytes) để lặp lạiNlần và gọi hàm hủy cho từng đối tượng trong mảng. - Gọi hàm
operator delete[](ptr)(thực tế gọioperator delete) để giải phóng toàn bộ khối bộ nhớ đã cấp phát, bao gồm cả phầnextra_bytes.
Ví dụ về cơ chế ẩn của new[] với kiểu người dùng định nghĩa:
Khi cấp phát một mảng đối tượng như new MyObject[N], hệ thống thường cấp phát thêm một số byte ở đầu khối bộ nhớ để lưu trữ số lượng đối tượng N. Điều này cho phép delete[] biết được cần gọi bao nhiêu hàm hủy và giải phóng toàn bộ khối bộ nhớ đúng cách.
#include <iostream>
class ManagedArrayObject {
public:
int id;
ManagedArrayObject(int val = 0) : id(val) {
std::cout << "ManagedArrayObject #" << id << " Constructor called." << std::endl;
}
~ManagedArrayObject() {
std::cout << "ManagedArrayObject #" << id << " Destructor called." << std::endl;
}
};
void demonstrateArrayMemory() {
std::cout << "\n--- Chi tiet bo nho mang doi tuong voi new[] ---" << std::endl;
// Cấp phát một mảng 3 đối tượng ManagedArrayObject
ManagedArrayObject* objects = new ManagedArrayObject[3];
// Hệ thống cấp phát không chỉ 3 * sizeof(ManagedArrayObject)
// mà còn thêm một phần nhỏ để lưu trữ số lượng '3'.
objects[0].id = 1;
objects[1].id = 2;
objects[2].id = 3;
std::cout << "Values in array: " << objects[0].id << ", " << objects[1].id << ", " << objects[2].id << std::endl;
// Nếu dùng 'delete objects;' (KHÔNG KHỚP):
// Chỉ hàm hủy của objects[0] được gọi.
// Bộ nhớ được giải phóng với kích thước sai, dẫn đến rò rỉ hoặc lỗi runtime.
// delete objects;
// Sử dụng 'delete[] objects;' (ĐÚNG CÁCH):
// Hệ thống đọc số lượng đối tượng đã lưu, gọi hàm hủy cho tất cả 3 đối tượng,
// sau đó giải phóng toàn bộ khối bộ nhớ.
delete[] objects;
}
Nếu bạn sử dụng delete objects; thay vì delete[] objects; cho mảng các đối tượng tùy chỉnh, chỉ hàm hủy của đối tượng đầu tiên được gọi và bộ nhớ được giải phóng với kích thước không chính xác. Điều này gây ra rò rỉ bộ nhớ (do các hàm hủy còn lại không được gọi) và có thể dẫn đến lỗi nghiêm trọng hoặc treo chương trình.
Đối với mảng các kiểu dữ liệu cơ bản (ví dụ: int* p = new int[10]; delete p;), việc sử dụng delete thay vì delete[] có thể không gây ra lỗi ngay lập tức vì không có hàm hủy nào cần được gọi. Tuy nhiên, nó vẫn là một hành vi không xác định và không nên được thực hiện.
Biểu thức New Định vị (Placement New)
Biểu thức new định vị (placement new) là một tính năng đặc biệt của C++ cho phép bạn xây dựng một đối tượng trong một vùng bộ nhớ đã được cấp phát sẵn, thay vì yêu cầu operator new cấp phát bộ nhớ mới.
Cú pháp: new (địa_chỉ_đặt) Kiểu_dữ_liệu hoặc new (địa_chỉ_đặt) Kiểu_dữ_liệu(danh_sách_khởi_tạo)
Trong đó địa_chỉ_đặt phải là một con trỏ tới vùng bộ nhớ đã tồn tại.
Trường hợp sử dụng: Placement new thường được dùng trong các tình huống nâng cao như:
- Làm việc với các hệ thống quản lý bộ nhớ tùy chỉnh (memory pools) nơi bạn đã cấp phát một khối bộ nhớ lớn và muốn xây dựng các đối tượng nhỏ hơn trong đó.
- Tạo đối tượng trong vùng bộ nhớ chia sẻ (shared memory).
- Tối ưu hóa hiệu suất bằng cách tránh overhead của việc cấp phát bộ nhớ hệ thống.
Ví dụ:
#include <iostream>
#include <new> // Cần thiết cho placement new
#include <cstdlib> // Cho malloc, free
class Widget {
public:
int id;
Widget(int i = 0) : id(i) {
std::cout << "Widget #" << id << " Constructor called." << std::endl;
}
~Widget() {
std::cout << "Widget #" << id << " Destructor called." << std::endl;
}
};
void demonstratePlacementNew() {
std::cout << "\n--- Minh hoa Placement New ---" << std::endl;
// 1. Cấp phát bộ nhớ thô (raw memory) bằng malloc
void* buffer1 = malloc(sizeof(Widget));
if (!buffer1) {
std::cerr << "Failed to allocate buffer1." << std::endl;
return;
}
std::cout << "Da cap phat bo nho thô bang malloc tai dia chi: " << buffer1 << std::endl;
// 2. Sử dụng placement new để xây dựng đối tượng trong buffer1
Widget* w1 = new (buffer1) Widget(101); // Gọi constructor của Widget tại buffer1
w1->id = 102; // Có thể thay đổi giá trị của đối tượng đã được tạo
std::cout << "Widget w1.id: " << w1->id << std::endl;
// 3. Gọi hàm hủy một cách thủ công (quan trọng với placement new)
w1->~Widget();
// 4. Giải phóng bộ nhớ thô
free(buffer1);
std::cout << "Da giai phong buffer1." << std::endl;
// Ví dụ khác với operator new để cấp phát bộ nhớ thô
void* buffer2 = operator new(sizeof(Widget));
std::cout << "Da cap phat bo nho thô bang operator new tai dia chi: " << buffer2 << std::endl;
Widget* w2 = new (buffer2) Widget(201);
std::cout << "Widget w2.id: " << w2->id << std::endl;
w2->~Widget();
operator delete(buffer2);
std::cout << "Da giai phong buffer2." << std::endl;
}
Các Câu Hỏi Phỏng Vấn Thường Gặp
1. Sự khác biệt giữa malloc/free và new/delete là gì?
malloc/free và new/delete đều dùng để cấp phát và giải phóng bộ nhớ động trên vùng heap. Tuy nhiên, chúng có những khác biệt cơ bản:
- Tính chất:
malloc/freelà các hàm trong thư viện C chuẩn, trong khinew/deletelà các toán tử trong C++. - Kiểu dữ liệu:
malloctrả vềvoid*, yêu cầu ép kiểu tường minh sang kiểu con trỏ mong muốn.newtrả về con trỏ đúng kiểu dữ liệu, không yêu cầu ép kiểu.
- Khởi tạo:
mallocchỉ cấp phát bộ nhớ, không khởi tạo nội dung (chứa giá trị "rác").newcó thể khởi tạo đối tượng. Với kiểu cơ bản,new int(10)khởi tạo 10;new int[5]{1,2}khởi tạo 1,2,0,0,0. Với kiểu tùy chỉnh,newsẽ gọi hàm tạo (constructor).
- Xử lý mảng:
malloccấp phát khối bộ nhớ đơn lẻ. Để quản lý mảng, cần tính toán kích thước tổng.new[]vàdelete[]được thiết kế riêng để cấp phát và giải phóng mảng các đối tượng, đảm bảo gọi hàm tạo/hàm hủy cho từng phần tử.
- Hàm tạo/Hàm hủy:
malloc/freechỉ cấp phát/giải phóng bộ nhớ thô và không gọi hàm tạo hay hàm hủy của đối tượng.new/deletetự động gọi hàm tạo và hàm hủy của đối tượng, giúp quản lý tài nguyên nội bộ của đối tượng.
- Xử lý lỗi:
malloctrả vềNULLnếu cấp phát thất bại, yêu cầu kiểm traNULLthủ công.newném ngoại lệstd::bad_allocnếu cấp phát thất bại (mặc định), không yêu cầu kiểm traNULLnhưng cần cơ chế xử lý ngoại lệ.
2. Rò rỉ bộ nhớ (Memory Leak) là gì?
Rò rỉ bộ nhớ xảy ra khi một chương trình cấp phát bộ nhớ động nhưng sau đó mất khả năng truy cập hoặc giải phóng khối bộ nhớ đó khi nó không còn cần thiết. Điều này không có nghĩa là bộ nhớ biến mất về mặt vật lý, mà là ứng dụng không còn kiểm soát được vùng bộ nhớ đã cấp phát, dẫn đến việc lãng phí tài nguyên và không thể tái sử dụng.
Tác hại của rò rỉ bộ nhớ: Với các chương trình chạy trong thời gian dài (như hệ điều hành, dịch vụ backend), rò rỉ bộ nhớ có thể dẫn đến việc ứng dụng ngày càng sử dụng nhiều bộ nhớ hơn, phản hồi chậm hơn, và cuối cùng có thể treo hoặc sập do cạn kiệt tài nguyên hệ thống.
Phân loại rò rỉ bộ nhớ (thường gặp):
- Rò rỉ Heap (Heap leak): Loại phổ biến nhất, xảy ra khi bộ nhớ được cấp phát từ heap bằng
malloc/newkhông được giải phóng bằngfree/delete. - Rò rỉ tài nguyên hệ thống (System Resource Leak): Xảy ra khi các tài nguyên khác của hệ điều hành (như file handles, network sockets, database connections) không được giải phóng đúng cách sau khi sử dụng.
Cách phát hiện rò rỉ bộ nhớ:
- Sử dụng các công cụ phân tích tĩnh (static analysis tools) hoặc công cụ phân tích động (dynamic analysis tools) như Valgrind (Linux), Dr. Memory (Windows/Linux/macOS), AddressSanitizer (GCC/Clang).
- Trên Windows/Visual Studio, có thể sử dụng các hàm như
_CrtDumpMemoryLeaks()để kiểm tra rò rỉ cơ bản, cung cấp thông tin về số byte bị rò rỉ nhưng ít chi tiết về vị trí.