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 */| Biểu thức | Kiểu | Giá trị | Đọc là |
|---|---|---|---|
| pp | int ** | 0x7ffd18 | địa chỉ của p |
| *pp | int * | 0x7ffd10 | chính là p |
| **pp | int | 42 | chính là x |
| &pp | int *** | 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 x | x là int | 0 |
| int *p | p là con trỏ tới int | 1 |
| int **pp | pp là con trỏ tới con trỏ tới int | 2 |
| int ***ppp | ba tầng | 3 |
| char **argv | argv là con trỏ tới con trỏ tới char | 2 |
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 đích | Cần mấy tầng |
|---|---|
| Hàm sửa giá trị int của người gọi | Một, int * |
| Hàm sửa biến con trỏ của người gọi | Hai, int ** |
| Hàm nhận mảng int | Một, int * |
| Hàm nhận mảng con trỏ, ví dụ mảng chuỗi | Hai, 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ử
- Khai báo ba tầng như trong sơ đồ, in đủ bốn biểu thức trong bảng.
- Sửa giá trị của biến gốc qua
**pp, xác nhận biến đổi thật. - Cài hàm cấp phát nhận
int *rồiint **, chứng minh chỉ cách thứ hai hoạt động. - Viết macro
GIAI_PHONGvà dùng nó, kiểm tra con trỏ đã thànhNULL. - Cài
tao_ma_tranvàhuy_ma_tranđầy đủ, chạy dưới valgrind để xác nhận không rò rỉ. - 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.
- Thử truyền
int m[3][4]vào hàm nhậnint **, đọ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 **pplà 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ý domainnhậnchar **argv. int **mhoàn toàn khácint 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ế.