Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 13.120 phút đọc

Con trỏ tới con trỏ

Sau bài này bạn sẽ làm được

  • Vẽ được sơ đồ hai tầng gián tiếp
  • Viết hàm sửa được con trỏ của người gọi
  • Đọc và viết khai báo có nhiều dấu sao
  • Nhận ra khi nào một tầng gián tiếp là đủ

Con trỏ tới con trỏ nghe đáng sợ nhưng thật ra chỉ là áp dụng lại đúng một quy tắc bạn đã biết: muốn hàm sửa được thứ gì thì truyền địa chỉ của thứ đó. Nếu thứ đó tình cờ là một con trỏ, bạn được con trỏ tới con trỏ.

#Hai tầng gián tiếp

int    x = 42;
int   *p = &x;      /* p giữ địa chỉ của x */
int  **pp = &p;     /* pp giữ địa chỉ của p */
Ba ô nhớ riêng biệt. Mỗi dấu sao khi dùng đi thêm một mũi tên trên sơ đồ.
Biểu thứcKiểuGiá trịĐọc là
ppint **0x7ffd18địa chỉ của p
*ppint *0x7ffd10chính là p
**ppint42chính là x
&ppint ***0x7ffd20địa chỉ của pp
hai-tang.c
#include <stdio.h>

int main(void)
{
    int    x = 42;
    int   *p = &x;
    int  **pp = &p;

    printf("x    = %d\n", x);
    printf("*p   = %d\n", *p);
    printf("**pp = %d\n", **pp);

    printf("p    == &x : %d\n", p == &x);       /* 1 */
    printf("*pp  == p  : %d\n", *pp == p);      /* 1 */
    printf("**pp == x  : %d\n", **pp == x);     /* 1 */

    **pp = 100;                                  /* sửa x qua hai tầng */
    printf("x = %d\n", x);                      /* 100 */

    return 0;
}

#Ba trường hợp cần dùng

Trường hợp 1: hàm sửa con trỏ của người gọi

Một tầng, hàm không sửa được
void cap_phat(int *p, size_t n)
{
    p = malloc(n * sizeof *p);      /* chỉ đổi bản sao */
}

int *a = NULL;

cap_phat(a, 10);
/* a vẫn là NULL */
Hai tầng
int cap_phat(int **pp, size_t n)
{
    if (pp == NULL) return -1;

    int *tam = malloc(n * sizeof *tam);

    if (tam == NULL) return -1;

    *pp = tam;

    return 0;
}

int *a = NULL;

if (cap_phat(&a, 10) == 0) {
    a[0] = 1;
    free(a);
}

Trường hợp 2: giải phóng an toàn

void giai_phong(void **pp)
{
    if (pp == NULL || *pp == NULL) return;

    free(*pp);
    *pp = NULL;
}

int *a = malloc(40);

giai_phong((void **)&a);
/* a giờ là NULL, không phải con trỏ treo */

Trường hợp 3: mảng con trỏ truyền vào hàm

/* Mảng con trỏ phân rã thành con trỏ tới con trỏ, đúng như mảng int
   phân rã thành int sao. */
void in_danh_sach(char **ten, size_t n)
{
    for (size_t i = 0; i < n; ++i)
        printf("%s\n", ten[i]);
}

char *ten[] = { "An", "Binh", "Cuong" };

in_danh_sach(ten, 3);

Đây cũng là lý do main có dạng int main(int argc, char **argv). Bài 13.3 sẽ nói kỹ về mảng con trỏ và đối số dòng lệnh.

#Đọc khai báo nhiều dấu sao

Khai báoĐọc làSố tầng
int xx là int0
int *pp là con trỏ tới int1
int **pppp là con trỏ tới con trỏ tới int2
int ***pppba tầng3
char **argvargv là con trỏ tới con trỏ tới char2

Bao nhiêu tầng là quá nhiều

#Mảng hai chiều cấp phát động

Đây là ứng dụng quan trọng nhất của con trỏ tới con trỏ, và nó khác hoàn toàn với mảng hai chiều tĩnh ở Chương 10.

hai-chieu.c
#include <stdlib.h>

/* Cấp phát ma trận hang x cot. Trả về NULL nếu thất bại. */
int **tao_ma_tran(size_t hang, size_t cot)
{
    int **m = malloc(hang * sizeof *m);      /* mảng con trỏ hàng */

    if (m == NULL) return NULL;

    for (size_t i = 0; i < hang; ++i) {
        m[i] = malloc(cot * sizeof *m[i]);   /* mỗi hàng một vùng riêng */

        if (m[i] == NULL) {
            for (size_t j = 0; j < i; ++j)   /* dọn phần đã cấp */
                free(m[j]);

            free(m);

            return NULL;
        }
    }

    return m;
}

void huy_ma_tran(int **m, size_t hang)
{
    if (m == NULL) return;

    for (size_t i = 0; i < hang; ++i)
        free(m[i]);

    free(m);
}

int **m = tao_ma_tran(3, 4);

if (m != NULL) {
    m[1][2] = 42;
    huy_ma_tran(m, 3);
}

Cách tốt hơn: một lần cấp phát

phang.c
/* Cấp một khối phẳng rồi tự tính chỉ số. Chỉ một malloc, một free,
   và dữ liệu liên tục nên thân thiện với bộ nhớ đệm. */
int *m = malloc(hang * cot * sizeof *m);

if (m != NULL) {
    m[1 * cot + 2] = 42;
    free(m);
}

/* Hoặc cấp một lần rồi dựng mảng con trỏ trỏ vào đó, được cả hai */
int **tao_nhanh(size_t hang, size_t cot)
{
    int **m   = malloc(hang * sizeof *m);
    int  *khoi = malloc(hang * cot * sizeof *khoi);

    if (m == NULL || khoi == NULL) {
        free(m);
        free(khoi);

        return NULL;
    }

    for (size_t i = 0; i < hang; ++i)
        m[i] = khoi + i * cot;

    return m;
}

/* Hủy: chỉ cần free(m[0]) rồi free(m) */

#Khi nào một tầng là đủ

Mục đíchCần mấy tầng
Hàm sửa giá trị int của người gọiMột, int *
Hàm sửa biến con trỏ của người gọiHai, int **
Hàm nhận mảng intMột, int *
Hàm nhận mảng con trỏ, ví dụ mảng chuỗiHai, char **
Hàm chỉ đọc dữ liệu qua con trỏMột, const int *
Dùng hai tầng khi không cần
/* Hàm chỉ đọc dữ liệu, không cần đổi con trỏ */
void in_mang(int **pp, size_t n)
{
    for (size_t i = 0; i < n; ++i)
        printf("%d ", (*pp)[i]);
}

int  a[5];
int *p = a;

in_mang(&p, 5);      /* rườm rà không cần thiết */
Một tầng là đủ
void in_mang(const int *p, size_t n)
{
    for (size_t i = 0; i < n; ++i)
        printf("%d ", p[i]);
}

int a[5];

in_mang(a, 5);       /* gọn và rõ */

Tự làm thử

  1. Khai báo ba tầng như trong sơ đồ, in đủ bốn biểu thức trong bảng.
  2. Sửa giá trị của biến gốc qua **pp, xác nhận biến đổi thật.
  3. Cài hàm cấp phát nhận int * rồi int **, chứng minh chỉ cách thứ hai hoạt động.
  4. Viết macro GIAI_PHONG và dùng nó, kiểm tra con trỏ đã thành NULL.
  5. Cài tao_ma_tranhuy_ma_tran đầy đủ, chạy dưới valgrind để xác nhận không rò rỉ.
  6. Cài cả ba cách tạo ma trận động, đo thời gian cộng hai ma trận 1000 nhân 1000 với từng cách.
  7. Thử truyền int m[3][4] vào hàm nhận int **, đọc cảnh báo, rồi ép kiểu và quan sát chương trình sập.

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

  • int **pp là con trỏ tới con trỏ. Mỗi dấu sao khi dùng đi thêm một tầng.
  • Dùng hai tầng khi hàm phải đổi biến con trỏ của người gọi, ví dụ hàm cấp phát hoặc hàm giải phóng an toàn.
  • Mảng con trỏ phân rã thành T ** khi truyền vào hàm, đó là lý do main nhận char **argv.
  • int **m hoàn toàn khác int m[3][4]: bố cục rời rạc, nhiều lần cấp phát, và không truyền lẫn cho nhau được.
  • Ba tầng trở lên gần như luôn là dấu hiệu nên dùng struct thay thế.