Nói đến con trỏ không thể không nhắc tới cấp phát bộ nhớ động và giải phóng bộ nhớ cho biến con trỏ trong ngôn ngữ C. Trong bài viết này, Lập trình không khó sẽ làm rõ vấn đề cấp phát bộ nhớ động sử dụng malloc()
, calloc()
, free()
và realloc()
trong C. Đây là các hàm giúp các bạn cấp phát và giải phóng bộ nhớ khi làm việc với con trỏ trong ngôn ngữ C.
Tại sao cần cấp phát bộ nhớ động?
Như bạn đã biết, mảng là một tập hợp của các phần tử nằm liên tiếp nhau trên bộ nhớ và có cùng kiểu dữ liệu. Khi khai báo mảng, bạn phải chỉ định rõ kích thước tối đa (số lượng phần tử tối đa). Và sau khi khai báo, bạn không thể thay đổi kích thước của mảng – Cấp phát tĩnh.
Đôi khi kích thước của mảng bạn khai báo có thể không đủ sài. Để giải quyết vấn đề này, bạn có thể cấp phát thêm bộ nhớ theo cách thủ công trong thời gian chạy chương trình. Đó cũng chính là khái niệm cấp phát động trong C.
Bảng dưới đây so sánh giúp bạn sự khác biệt giữa cấp phát bộ nhớ động và tĩnh.
Cấp phát bộ nhớ tĩnh | Cấp phát bộ nhớ động |
Bộ nhớ được cấp phát trước khi chạy chương trình (trong quá trình biên dịch) | Bộ nhớ được cấp phát trong quá trình chạy chương trình. |
Không thể cấp phát hay phân bổ lại bộ nhớ trong khi chạy chương trình | Cho phép quản lý, phân bổ hay giải phóng bộ nhớ trong khi chạy chương trình |
Vùng nhớ được cấp phát và tồn tại cho đến khi kết thúc chương trình | Chỉ cấp phát vùng nhớ khi cần sử dụng tới |
Chương trình chạy nhanh hơn so với cấp phát động | Chương trình chạy chậm hơn so với cấp phát tĩnh |
Tốn nhiều không gian bộ nhớ hơn | Tiết kiệm được không gian bộ nhớ sử dụng |
Ưu điểm chính của việc sử dụng cấp phát động là giúp ta tiết kiệm được không gian bộ nhớ mà chương trình sử dụng. Bởi vì chúng ta sẽ chỉ cấp phát khi cần dùng và có thể giải phóng vùng nhớ đó ngay sau khi sử dụng xong.
Nhược điểm chính của cấp phát động là bạn phải tự quản lý vùng nhớ mà bạn cấp phát. Nếu bạn cứ cấp phát mà quên giải phóng bộ nhớ thì chương trình của bạn sẽ tiêu thụ hết tài nguyên của máy tính dẫn đến tình trạng tràn bộ nhớ (memory leak).
Cấp phát bộ nhớ động trong C
Để cấp phát vùng nhớ động cho biến con trỏ trong ngôn ngữ C, bạn có thể sử dụng hàm malloc()
hoặc hàm calloc()
. Sử dụng hàm free()
để giải phóng bộ nhớ đã cấp phát khi không cần sử dụng, sử dụng realloc()
để thay đổi (phân bổ lại) kích thước bộ nhớ đã cấp phát trong khi chạy chương trình.
Sử dụng hàm malloc()
Từ malloc là đại diện cho cụm từ memory allocation (dịch: cấp phát bộ nhớ).
Hàm malloc()
thực hiện cấp phát bộ nhớ bằng cách chỉ định số byte cần cấp phát. Hàm này trả về con trỏ kiểu void
cho phép chúng ta có thể ép kiểu về bất cứ kiểu dữ liệu nào.
Cú pháp của hàm malloc()
:
ptr = (castType*) malloc(size);
Ví dụ:
ptr = (int*) malloc(100 * sizeof(int));
Ví dụ trên thực hiện cấp phát cho việc lưu trữ 100 số nguyên. Giả sử sizeof int
là 4, khi đó lệnh dưới đây thực hiện cấp phát 400 bytes. Khi đó, con trỏ ptr
sẽ có giá trị là địa chỉ của byte dữ liệu đầu tiên trong khối bộ nhớ vừa cấp phát.
Trong trường hợp không thể cấp phát bộ nhớ, nó sẽ trả về một con trỏ NULL
.
Sử dụng hàm calloc()
Từ calloc
đại diện cho cụm từ contiguous allocation (dịch: cấp phát liên tục).
Hàm malloc()
khi cấp phát bộ nhớ thì vùng nhớ cấp phát đó không được khởi tạo giá trị ban đầu. Trong khi đó, hàm calloc()
thực hiện cấp phát bộ nhớ và khởi tạo tất cả các ô nhớ có giá trị bằng 0.
Hàm calloc()
nhận vào 2 tham số là số ô nhớ muốn khởi tạo và kích thước của 1 ô nhớ.
Cú pháp của hàm calloc():
ptr = (castType*)calloc(n, size);
Ví dụ:
ptr = (int*) calloc(100, sizeof(int));
Trong ví dụ trên, hàm calloc()
thực hiện cấp phát 100 ô nhớ liên tiếp và mỗi ô nhớ có kích thước là số byte của kiểu int
. Hàm này cũng trả về con trỏ chứa giá trị là địa chỉ của byte đầu tiên trong khối bộ nhớ vừa cấp phát.
Sử dụng hàm free()
Việc cấp phát bộ nhớ động trong C dù sử dụng malloc()
hay calloc()
thì chúng cũng đều không thể tự giải phóng bộ nhớ. Bạn cần sử dụng hàm free()
để giải phóng vùng nhớ.
Cú pháp:
free(ptr); // ptr là con trỏ
Lệnh này sẽ giải phóng vùng nhớ mà con trỏ ptr
đã được cấp phát. Giải phóng ở đây có nghĩa là trả lại vùng nhớ đó cho hệ điều hành và hệ điều hành có thể sử dụng vùng nhớ đó vào việc khác nếu cần.
Nếu bạn không giải phóng nó thì nó sẽ tồn tại cho tới khi chương trình kết thúc. Điều này sẽ rất nguy hiểm nếu chương trình của bạn liên tục cấp phát các vùng nhớ mới và sẽ gây ra hiện tượng tràn bộ nhớ mà mình đã nhắc tới ở trên. Thử code này xem sao (bảo đảm là máy bạn sẽ bị treo và chỉ còn cách ấn nút nguồn thôi, bạn có thể để cấp phát size nhỏ hơn và theo dõi thay đổi qua Task Manager):
#include <stdio.h> #include <stdlib.h> int main(){ for(;;){ printf("nCap phat %d bytes!", 1000000 * sizeof (int)); int *ptr = (int*) malloc (1000000 * sizeof (int)); } }
Ví dụ sử dụng malloc() và free()
Trong ví dụ dưới đây, chúng ta sẽ sử dụng hàm malloc()
để cấp phát động n * sizeof int
byte và sử dụng xong sẽ dùng free()
để giải phóng.
#include <stdio.h> // Thư viện này cần để cấp phát bộ nhớ động #include <stdlib.h> int main() { int n, i, *ptr, sum = 0; printf("Nhap so luong phan tu: "); scanf("%d", &n); ptr = (int *)malloc(n * sizeof(int)); // Nếu không thể cấp phát, // hàm malloc sẽ trả về con trỏ NULL if (ptr == NULL) { printf("Co loi! khong the cap phat bo nho."); exit(0); } printf("Nhap cac gia tri: "); for (i = 0; i < n; ++i) { scanf("%d", ptr + i); sum += *(ptr + i); } printf("Tong = %d", sum); // Giải phóng vùng nhớ cho con trỏ free(ptr); return 0; }
Ví dụ sử dụng calloc() và free()
Trong ví dụ này, chúng ta sẽ dùng calloc()
để cấp phát n ô nhớ liên tiếp và mỗi ô nhớ có kích thước là sizeof int
. Lưu ý là hàm calloc()
sẽ chậm hơn malloc()
một chút do nó phải thêm bước khởi tạo các ô nhớ có giá trị bằng 0. Do đó, tùy thuộc bạn cần hiệu năng hay cần khởi tạo giá trị ban đầu mà sử dụng hàm cấp phát thích hợp.
#include <stdio.h> // Thư viện này cần để cấp phát bộ nhớ động #include <stdlib.h> int main() { int n, i, *ptr, sum = 0; printf("Nhap so luong phan tu: "); scanf("%d", &n); ptr = (int *)calloc(n, sizeof(int)); // Nếu không thể cấp phát, // hàm calloc sẽ trả về con trỏ NULL if (ptr == NULL) { printf("Co loi! khong the cap phat bo nho."); exit(0); } printf("Nhap cac gia tri: "); for (i = 0; i < n; ++i) { scanf("%d", ptr + i); sum += *(ptr + i); } printf("Tong = %d", sum); // Giải phóng vùng nhớ cho con trỏ free(ptr); return 0; }
Sử dụng hàm realloc()
Nếu việc cấp phát bộ nhớ động không đủ hoặc cần nhiều hơn mức đã cấp phát, bạn có thể thay đổi kích thước của bộ nhớ đã được cấp phát trước đó bằng cách sử dụng hàm realloc()
.
Cú pháp của realloc()
:
ptr = realloc(ptr, n);
Hàm này thực hiện cấp phát vùng nhớ mới cho con trỏ ptr
. Vùng nhớ mới đó sẽ có kích thước mới là n
bytes.
Hàm này cũng trả về con trỏ chứa giá trị là địa chỉ của byte đầu tiên trong vùng nhớ mới. Hàm này sẽ cố gắng mở rộng số ô nhớ ra phía sau nếu có thể để giữ nguyên giá trị của con trỏ ban đầu. Trong trường hợp phải đổi sang một vùng nhớ khác, hàm realloc()
cũng sẽ mang theo giá trị đã có ở vùng nhớ cũ sang vùng nhớ mới và giải phóng luôn vùng nhớ cũ (đọc thêm tài liệu số 2). Trong trường hợp không thể, nó sẽ trả về con trỏ NULL
giống như malloc()
và calloc()
.
Ví dụ sử dụng hàm realloc()
Trong ví dụ dưới đây, ta sẽ sử dụng hàm realloc()
để tái phân bổ lại bộ nhớ. Như trong ví dụ dưới đây thì việc cấp phát không phải di chuyển sang vùng nhớ khác mà chỉ mở rộng ra phía sau.
#include <stdio.h> #include <stdlib.h> int main() { int *ptr, i , n1, n2; printf("Nhap so luong phan tu: "); scanf("%d", &n1); ptr = (int*) malloc(n1 * sizeof(int)); printf("Dia chi cua vung nho vua cap phat: %u", ptr); printf("nNhap lai so luong phan tu: "); scanf("%d", &n2); // phân bổ lại vùng nhớ ptr = (int*) realloc(ptr, n2 * sizeof(int)); printf("Dia chi cua vung nho duoc cap phat lai: %u", ptr); // giải phóng free(ptr); return 0; }
Kết quả chạy:
Nhap so luong phan tu: 2 Dia chi cua vung nho vua cap phat: 1993360 Nhap lai so luong phan tu: 100 Dia chi cua vung nho duoc cap phat lai: 1993360
Cấp phát động trong C++
Mục này không phải kiến thức của C, nhưng mình cũng sẽ ghi lại để các bạn học luôn hoặc tìm hiểu, phân biệt và sử dụng trong C++ khi cần.
Để cấp phát động trong C++ bạn sử dụng toán tử new
và giải phóng bộ nhớ sử dụng delete
. Ví dụ:
#include <iostream> #include <cstring> using namespace std; int main() { int num; cout << "Nhap so luong sinh vien: "; cin >> num; float* ptr; // Cấp phát `num` ô nhớ kiểu float ptr = new float[num]; cout << "Nhap diem GPA." << endl; for (int i = 0; i < num; ++i) { cout << "Sinh vien " << i + 1 << ": "; cin >> *(ptr + i); } cout << "nHien thi diem GPA cua sinh vien." << endl; for (int i = 0; i < num; ++i) { cout << "Sinh vien " << i + 1 << " :" << *(ptr + i) << endl; } // giải phóng bộ nhớ của `ptr` delete [] ptr; return 0; }
Sau đây mình sẽ hướng dẫn bạn cách sử dụng cấp phát động vào mảng 1 chiều.
Cấp phát động mảng 1 chiều
Dưới đây là ví dụ sử dụng cấp phát động cho mảng 1 chiều để các bạn tham khảo và nắm được ứng dụng của cấp phát bộ nhớ động.
#include <stdio.h> #include <stdlib.h> void NhapMang(int *arr, int n) { for (int i = 0; i < n; i++) { printf("arr[%d] = ", i); // Do giá trị con trỏ là địa chỉ rồi. Nên bạn sẽ không thấy dấu & quen thuộc nữa scanf("%d", (arr + i)); } } void XuatMang(int *arr, int n) { for (int i = 0; i < n; i++) { printf("arr[%d] = %dn", i, *(arr + i)); } } void ThemPhanTu(int *a, int &n, int val, int pos) { // Phân bổ lại bộ nhớ đã cấp phát cho con trỏ. // Ta cần thêm 1 ô nhớ cho nó => dùng realloc() a = (int *)realloc(a, (n + 1) * sizeof(int)); // Neu pos <= 0 => Them vao dau if (pos < 0) { pos = 0; } // Neu pos >= n => Them vao cuoi else if (pos > n) { pos = n; } // Dich chuyen mang de tao o trong truoc khi them. for (int i = n; i > pos; i--) { *(a + i) = *(a + i - 1); } // Chen val tai pos *(a + pos) = val; // Tang so luong phan tu sau khi chen. ++n; } void XoaPhanTu(int *a, int &n, int pos) { // Mang rong, khong the xoa. if (n <= 0) { return; } // Neu pos <= 0 => Xoa dau if (pos < 0) { pos = 0; } // Neu pos >= n => Xoa cuoi else if (pos >= n) { pos = n - 1; } // Dich chuyen mang for (int i = pos; i < n - 1; i++) { a[i] = a[i + 1]; } // Cấp phát lại vùng nhớ, giờ ta chỉ cần n - 1 ô nhớ a = (int *)realloc(a, (n - 1) * sizeof(int)); // Giam so luong phan tu sau khi xoa. --n; } int main() { int *arr; int n; do { printf("Nhap so luong n = "); scanf("%d", &n); } while (n < 1); // cấp phát đủ sài arr = (int *)malloc(n * sizeof(int)); // arr = (int*) calloc(n, sizeof(int)); if (arr == NULL) { printf("Khong the cap phat!"); exit(0); } NhapMang(arr, n); printf("nMang vua nhap la:n"); XuatMang(arr, n); printf("n=======THEM PHAN TU======n"); int val, pos; printf("nNhap so can them: "); scanf("%d", &val); printf("nNhap vi tri muon chen: "); scanf("%d", &pos); ThemPhanTu(arr, n, val, pos); printf("nMang sau khi them:n"); XuatMang(arr, n); printf("n=======XOA PHAN TU======n"); printf("nNhap vi tri muon xoa: "); scanf("%d", &pos); XoaPhanTu(arr, n, pos); printf("nMang sau khi xoa:n"); XuatMang(arr, n); // giải phóng free(arr); }
Kết quả chạy chương trình:
Nhap so luong n = 3 arr[0] = 1 arr[1] = 2 arr[2] = 3 Mang vua nhap la: arr[0] = 1 arr[1] = 2 arr[2] = 3 =======THEM PHAN TU====== Nhap so can them: 4 Nhap vi tri muon chen: 4 Mang sau khi them: arr[0] = 1 arr[1] = 2 arr[2] = 3 arr[3] = 4 =======XOA PHAN TU====== Nhap vi tri muon xoa: 4 Mang sau khi xoa: arr[0] = 1 arr[1] = 2 arr[2] = 3
Đỗ Phương Nam viết
Em có một câu hỏi là nếu như trong hàm main() mà ta thực hiện cấp phát bộ nhớ động bằng hàm malloc() hoặc calloc() mà không có câu lệnh free() để giải phóng vùng nhớ. Thì khi gặp lệnh return 0; thoát khỏi hàm thì vùng nhớ đó có tự giải phóng không ạ. Mặc dù câu lệnh free() là quan trọng nhưng nếu nó tự giải phóng khi chương trình kết thúc thì không cần dùng free() thì có sao không ạ ?