Bài 14.822 phút đọc
realloc
Sau bài này bạn sẽ làm được
- Dùng realloc đúng mẫu, không bao giờ gán thẳng vào con trỏ cũ
- Hiểu ba hành vi đặc biệt của realloc
- Cài mảng động tự nới rộng theo cấp số nhân
- Giải thích vì sao nhân đôi cho chi phí trung bình hằng số
Hàm realloc giải quyết bài toán bạn gặp liên tục: cấp phát rồi mới biết cần nhiều hơn. Nó cũng là hàm dễ dùng sai nhất trong bốn hàm cấp phát, vì một cách viết trông rất tự nhiên lại gây rò rỉ bộ nhớ.
#Cách dùng
#include <stdlib.h>
void *realloc(void *p, size_t co_moi);| Điểm | Chi tiết |
|---|---|
| Nội dung cũ | Được giữ nguyên, tới mức nhỏ hơn của kích thước cũ và mới |
| Phần mở rộng | Không được xóa, chứa rác |
| Địa chỉ trả về | Có thể giống, có thể khác hoàn toàn |
| Khi trả về địa chỉ khác | Vùng cũ đã được giải phóng, đừng dùng lại |
| Khi thất bại | Trả về NULL, và vùng cũ VẪN NGUYÊN VẸN |
co-ban.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t n = 5;
int *a = malloc(n * sizeof *a);
if (a == NULL) return 1;
for (size_t i = 0; i < n; ++i)
a[i] = (int)i;
/* Nới rộng lên 10 phần tử */
size_t n_moi = 10;
int *tam = realloc(a, n_moi * sizeof *tam);
if (tam == NULL) {
fprintf(stderr, "Khong noi rong duoc\n");
free(a); /* a vẫn hợp lệ, phải tự giải phóng */
return 1;
}
a = tam; /* chỉ gán khi đã chắc chắn thành công */
for (size_t i = n; i < n_moi; ++i)
a[i] = (int)i; /* phần mới chứa rác, phải tự điền */
for (size_t i = 0; i < n_moi; ++i)
printf("%d ", a[i]);
printf("\n");
free(a);
return 0;
}terminal
./co-ban
0 1 2 3 4 5 6 7 8 9
terminal
# Chạy nhiều lần, đôi khi địa chỉ giữ nguyên, đôi khi đổi
./xem-dia-chi
truoc : 0x55d3f52b62a0 sau : 0x55d3f52b62a0 <- noi rong tai cho truoc : 0x55d3f52b62a0 sau : 0x55d3f52b76e0 <- da chuyen di cho khac
#Cái bẫy mất con trỏ
Gán thẳng vào con trỏ cũ
int *a = malloc(100 * sizeof *a);
a = realloc(a, 200 * sizeof *a); /* SAI */
if (a == NULL) {
/* Vùng nhớ cũ vẫn tồn tại nhưng KHÔNG CÒN CON TRỎ NÀO tới nó.
Đây chính là định nghĩa của rò rỉ bộ nhớ. */
return 1;
}Qua biến tạm
int *a = malloc(100 * sizeof *a);
if (a == NULL) return 1;
int *tam = realloc(a, 200 * sizeof *tam);
if (tam == NULL) {
free(a); /* a vẫn hợp lệ, giải phóng được */
return 1;
}
a = tam; /* chỉ gán khi chắc chắn thành công */#Ba hành vi đặc biệt
| Cách gọi | Hành vi | Tương đương với |
|---|---|---|
| realloc(NULL, co) | Cấp phát mới hoàn toàn | malloc(co) |
| realloc(p, 0) | Tùy cài đặt: giải phóng và trả NULL, hoặc trả về khối nhỏ nhất | Không có gì tương đương chắc chắn |
| realloc(p, co_nho_hon) | Thu nhỏ, giữ phần đầu, thường trả về đúng p | Không có |
Hành vi 1: realloc với NULL
int *a = NULL;
size_t suc_chua = 0;
/* Không cần trường hợp đặc biệt cho lần đầu tiên */
for (size_t i = 0; i < 100; ++i) {
if (i >= suc_chua) {
suc_chua = suc_chua ? suc_chua * 2 : 4;
int *tam = realloc(a, suc_chua * sizeof *tam);
if (tam == NULL) { free(a); return 1; }
a = tam;
}
a[i] = (int)i;
}
free(a);Vì realloc(NULL, co) hoạt động y hệt malloc(co), vòng lặp trên không cần một nhánh riêng cho lần cấp phát đầu tiên. Đây là mẫu rất gọn cho mảng động.
Hành vi 2: realloc với kích thước 0
Hành vi 3: thu nhỏ
char *bo_dem = malloc(4096);
/* Đọc dữ liệu, hóa ra chỉ dùng 100 byte */
size_t da_dung = doc_du_lieu(bo_dem, 4096);
char *tam = realloc(bo_dem, da_dung + 1);
if (tam != NULL) bo_dem = tam; /* nếu thất bại thì giữ khối lớn, vẫn dùng được */#Mảng động tự lớn
mang-dong.c
#include <stdint.h>
#include <stdlib.h>
typedef struct {
int *du_lieu;
size_t n; /* số phần tử đang dùng */
size_t suc_chua; /* số phần tử chứa được */
} MangDong;
/* Khởi tạo trạng thái rỗng, không cấp phát gì. */
void md_khoi_tao(MangDong *m)
{
m->du_lieu = NULL;
m->n = 0;
m->suc_chua = 0;
}
/* Bảo đảm chứa được ít nhất can phần tử. Trả về 0 nếu ổn. */
static int md_bao_dam(MangDong *m, size_t can)
{
if (can <= m->suc_chua) return 0;
size_t moi = m->suc_chua ? m->suc_chua : 4;
while (moi < can) {
if (moi > SIZE_MAX / 2) return -1; /* tránh tràn */
moi *= 2;
}
if (moi > SIZE_MAX / sizeof *m->du_lieu) return -1;
int *tam = realloc(m->du_lieu, moi * sizeof *tam);
if (tam == NULL) return -1; /* du_lieu cũ vẫn nguyên */
m->du_lieu = tam;
m->suc_chua = moi;
return 0;
}
/* Thêm một phần tử vào cuối. Trả về 0 nếu ổn. */
int md_them(MangDong *m, int gia_tri)
{
if (md_bao_dam(m, m->n + 1) != 0) return -1;
m->du_lieu[m->n++] = gia_tri;
return 0;
}
/* Giải phóng và đưa về trạng thái rỗng, gọi nhiều lần vẫn an toàn. */
void md_huy(MangDong *m)
{
free(m->du_lieu);
md_khoi_tao(m);
}dung.c
#include <stdio.h>
int main(void)
{
MangDong m;
md_khoi_tao(&m);
for (int i = 0; i < 1000; ++i)
if (md_them(&m, i * i) != 0) {
fprintf(stderr, "Het bo nho tai i = %d\n", i);
md_huy(&m);
return 1;
}
printf("n = %zu, suc chua = %zu\n", m.n, m.suc_chua);
printf("phan tu cuoi = %d\n", m.du_lieu[m.n - 1]);
md_huy(&m);
return 0;
}terminal
gcc -std=c17 -Wall -g -fsanitize=address mang-dong.c dung.c -o t && ./t
n = 1000, suc chua = 1024 phan tu cuoi = 998001
valgrind --leak-check=full ./t
All heap blocks were freed -- no leaks are possible
#Vì sao nhân đôi chứ không cộng thêm
Cộng thêm một mỗi lần
/* Mỗi lần thêm một phần tử là một lần realloc.
Thêm n phần tử: n lần realloc, và tổng số byte chép là 1+2+...+n = n²/2. */
int *tam = realloc(a, (n + 1) * sizeof *tam);Nhân đôi
/* Chỉ realloc khi đầy, và mỗi lần nhân đôi.
Thêm n phần tử: khoảng log n lần realloc, tổng số byte chép nhỏ hơn 2n. */
if (n >= suc_chua) {
suc_chua = suc_chua ? suc_chua * 2 : 4;
int *tam = realloc(a, suc_chua * sizeof *tam);
...
}| Chiến lược | Số lần realloc với n = 1 triệu | Tổng phần tử phải chép | Độ phức tạp mỗi lần thêm |
|---|---|---|---|
| Cộng thêm 1 | 1 000 000 | khoảng 500 tỷ | O(n) |
| Cộng thêm 100 | 10 000 | khoảng 5 tỷ | O(n) |
| Nhân đôi | khoảng 18 | dưới 2 triệu | O(1) trung bình |
| Nhân 1.5 | khoảng 35 | dưới 3 triệu | O(1) trung bình |
terminal
# Thêm một triệu phần tử bằng hai chiến lược
./do-chien-luoc
cong them 1 : 41.283 s nhan doi : 0.004 s ti le : hon 10 000 lan
Vì sao một số thư viện chọn hệ số 1.5
Thu nhỏ khi mảng vơi đi
/* Nếu chỉ còn dùng một phần tư sức chứa thì thu về một nửa.
Ngưỡng một phần tư chứ không phải một nửa, để tránh trường hợp
thêm rồi xóa liên tục quanh ngưỡng làm realloc chạy mãi. */
int md_xoa_cuoi(MangDong *m)
{
if (m->n == 0) return -1;
--m->n;
if (m->suc_chua > 8 && m->n < m->suc_chua / 4) {
size_t moi = m->suc_chua / 2;
int *tam = realloc(m->du_lieu, moi * sizeof *tam);
if (tam != NULL) { /* thu nhỏ thất bại thì bỏ qua */
m->du_lieu = tam;
m->suc_chua = moi;
}
}
return 0;
}Tự làm thử
- Cấp phát mảng, nới rộng bằng
realloc, in địa chỉ trước và sau, chạy nhiều lần để thấy cả hai trường hợp. - Viết
a = realloc(a, ...)rồi ép nó thất bại bằng cách xin một khối khổng lồ, chạy dưới valgrind để thấy rò rỉ. - Kiểm chứng
realloc(NULL, co)hoạt động giốngmallocbằng một chương trình nhỏ. - Cài đủ cấu trúc
MangDongtrong bài, thêm một triệu phần tử và chạy dưới valgrind. - Lưu con trỏ vào phần tử đầu rồi thêm nhiều phần tử, chạy dưới
-fsanitize=addressvà đọc thông báo. - Cài cả hai chiến lược cộng thêm một và nhân đôi, đo thời gian thêm một trăm nghìn phần tử.
- Thêm hàm xóa cuối có thu nhỏ, thử thêm và xóa liên tục quanh ngưỡng để quan sát hiện tượng trễ.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Không bao giờ viết
p = realloc(p, ...). Luôn qua biến tạm, vì khi thất bại bạn sẽ mất con trỏ tới vùng cũ. - Khi
reallocthất bại, vùng nhớ cũ vẫn nguyên vẹn và bạn vẫn phải giải phóng nó. realloc(NULL, co)hoạt động nhưmalloc, nên mảng động không cần trường hợp đặc biệt cho lần cấp phát đầu tiên.realloc(p, 0)là hành vi không xác định từ C23. Hãy xử lý riêng bằngfree.- Nhân đôi sức chứa cho chi phí trung bình hằng số mỗi lần thêm, trong khi cộng thêm một cho chi phí bậc hai.
- Với mảng động, hãy lưu chỉ số chứ đừng lưu con trỏ vào bên trong, vì mọi con trỏ hết hạn sau mỗi lần nới rộng.