Bỏ qua điều hướng, tới nội dung chính
Học C
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ểmChi 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ộngKhô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ácVùng cũ đã được giải phóng, đừng dùng lại
Khi thất bạiTrả 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ọiHành viTương đương với
realloc(NULL, co)Cấp phát mới hoàn toànmalloc(co)
realloc(p, 0)Tùy cài đặt: giải phóng và trả NULL, hoặc trả về khối nhỏ nhấtKhô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 pKhô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);

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

Mỗi khi đầy, sức chứa nhân đôi. Số ô đậm là phần đã dùng, số ô nhạt là chỗ dự phòng cho lần thêm sau.
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ượcSố lần realloc với n = 1 triệuTổng phần tử phải chépĐộ phức tạp mỗi lần thêm
Cộng thêm 11 000 000khoảng 500 tỷO(n)
Cộng thêm 10010 000khoảng 5 tỷO(n)
Nhân đôikhoảng 18dưới 2 triệuO(1) trung bình
Nhân 1.5khoảng 35dưới 3 triệuO(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ử

  1. 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.
  2. 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ỉ.
  3. Kiểm chứng realloc(NULL, co) hoạt động giống malloc bằng một chương trình nhỏ.
  4. Cài đủ cấu trúc MangDong trong bài, thêm một triệu phần tử và chạy dưới valgrind.
  5. Lưu con trỏ vào phần tử đầu rồi thêm nhiều phần tử, chạy dưới -fsanitize=address và đọc thông báo.
  6. 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ử.
  7. 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 realloc thấ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ằng free.
  • 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.