Con trỏ
Sau bài này bạn sẽ làm được
- Khai báo và khởi tạo con trỏ đúng cách
- Phân biệt con trỏ rỗng, con trỏ hoang và con trỏ treo
- Giải thích vì sao mọi con trỏ đều rộng như nhau nhưng kiểu vẫn quan trọng
- Luôn khởi tạo con trỏ ngay khi khai báo
Con trỏ là một biến bình thường. Nó có địa chỉ, có kích thước, có giá trị. Điểm khác biệt duy nhất là giá trị của nó tình cờ là địa chỉ của một vùng nhớ khác. Hiểu con trỏ như một biến bình thường sẽ gỡ được phần lớn khó khăn.
#Khai báo con trỏ
int *p; /* p là con trỏ tới int */
double *q; /* q là con trỏ tới double */
char *s; /* s là con trỏ tới char */Khởi tạo
int n = 42;
int *p1 = &n; /* trỏ tới một biến có sẵn */
int *p2 = NULL; /* con trỏ rỗng, chưa trỏ đi đâu */
int *p3; /* NGUY HIỂM: chứa rác, không biết trỏ đi đâu */
int *p4 = malloc(sizeof *p4); /* trỏ tới vùng vừa cấp, Chương 14 */
int a[5];
int *p5 = a; /* trỏ tới phần tử đầu của mảng, do phân rã */
int *p6 = &a[2]; /* trỏ tới phần tử thứ ba */Mọi con trỏ đều rộng như nhau
#include <stdio.h>
int main(void)
{
printf("%zu %zu %zu %zu\n",
sizeof(char *), sizeof(int *),
sizeof(double *), sizeof(void *));
return 0;
}8 8 8 8 # trên máy 64 bit
4 4 4 4 # trên máy 32 bit
Con trỏ chứa một địa chỉ, và mọi địa chỉ đều rộng như nhau. Vậy nếu kích thước giống nhau thì kiểu của con trỏ để làm gì. Đó là câu hỏi của mục sau.
#Sơ đồ và ba biểu thức
| Biểu thức | Đọc là | Kiểu | Giá trị trong sơ đồ |
|---|---|---|---|
| x | giá trị của x | int | 42 |
| &x | địa chỉ của x | int * | 0x7ffd3a10 |
| p | giá trị của p | int * | 0x7ffd3a10 |
| &p | địa chỉ của p | int ** | 0x7ffd3a18 |
| *p | giá trị tại chỗ p trỏ | int | 42 |
#include <stdio.h>
int main(void)
{
int x = 42;
int *p = &x;
printf("x = %d\n", x);
printf("&x = %p\n", (void *)&x);
printf("p = %p\n", (void *)p);
printf("&p = %p\n", (void *)&p);
printf("*p = %d\n", *p);
printf("p == &x : %d\n", p == &x); /* 1 */
printf("*p == x : %d\n", *p == x); /* 1 */
return 0;
}#Vì sao kiểu con trỏ quan trọng
Mọi con trỏ đều rộng 8 byte, nhưng kiểu vẫn quyết định ba chuyện, và cả ba đều quan trọng.
| Kiểu quyết định | Ví dụ với int * và char * |
|---|---|
| Đọc bao nhiêu byte khi giải tham chiếu | int * đọc 4 byte, char * đọc 1 byte |
| Bước nhảy của số học con trỏ | p + 1 với int * nhảy 4 byte, với char * nhảy 1 byte |
| Diễn giải các byte đó thành giá trị gì | Cùng bốn byte, int * thấy số nguyên, float * thấy số thực |
#include <stdio.h>
int main(void)
{
int n = 1000000;
int *pi = &n;
char *pc = (char *)&n; /* cùng địa chỉ, kiểu khác */
printf("pi == pc : %d\n", (void *)pi == (void *)pc); /* 1, cùng địa chỉ */
printf("*pi = %d\n", *pi); /* 1000000, đọc 4 byte */
printf("*pc = %d\n", *pc); /* 64, chỉ đọc 1 byte đầu */
printf("pi + 1 cach pi %td byte\n",
(char *)(pi + 1) - (char *)pi); /* 4 */
printf("pc + 1 cach pc %td byte\n",
(pc + 1) - pc); /* 1 */
return 0;
}pi == pc : 1 *pi = 1000000 *pc = 64 pi + 1 cach pi 4 byte pc + 1 cach pc 1 byte
#Ba loại con trỏ nguy hiểm
| Loại | Nghĩa là | Phát hiện thế nào |
|---|---|---|
| Con trỏ rỗng | Bằng NULL, cố tình không trỏ đi đâu | Kiểm tra được bằng if, giải tham chiếu thì sập ngay |
| Con trỏ hoang | Chưa khởi tạo, chứa rác | Không kiểm tra được, đây là loại tệ nhất |
| Con trỏ treo | Từng hợp lệ, nhưng vùng nhớ đã bị thu hồi | Không kiểm tra được, cần công cụ dò lỗi |
Con trỏ rỗng
int *p = NULL;
if (p != NULL)
printf("%d\n", *p); /* an toàn: chỉ dùng khi khác NULL */
printf("%d\n", *p); /* sập ngay, có thông báo rõ ràng */Đây là loại an toàn nhất trong ba loại, vì nó kiểm tra được và vì giải tham chiếu nó gần như luôn làm chương trình chết ngay tại chỗ gây lỗi. Hệ điều hành cố tình không ánh xạ trang nhớ ở địa chỉ 0 đúng để tạo ra hành vi này.
Con trỏ hoang
void f(void)
{
int *p; /* chứa rác từ lời gọi hàm trước */
*p = 42; /* ghi vào một địa chỉ ngẫu nhiên */
}Con trỏ treo
/* Cách 1: trả về địa chỉ biến cục bộ */
int *sai(void)
{
int x = 42;
return &x; /* x chết khi hàm kết thúc */
}
/* Cách 2: dùng sau khi giải phóng */
int *p = malloc(sizeof *p);
free(p);
*p = 42; /* p vẫn giữ địa chỉ cũ nhưng vùng đó đã bị thu hồi */
/* Cách 3: con trỏ tới phần tử của mảng đã ra khỏi phạm vi */
int *q;
{
int a[5];
q = a;
}
*q = 1; /* a không còn tồn tại */ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
WRITE of size 4
#0 in main treo.c:13
0x602000000010 is located 0 bytes inside of 4-byte region
freed by thread T0 here:
#0 in free
#1 in main treo.c:12#Bốn quy tắc bắt buộc
int *p; /* 1. không khởi tạo */
*p = 42; /* 2. dùng mà không kiểm tra */
int *q = malloc(100);
*q = 1; /* 3. không kiểm tra kết quả malloc */
free(q);
printf("%d\n", *q); /* 4. dùng sau khi giải phóng */int *p = NULL; /* 1. luôn khởi tạo */
if (p != NULL) *p = 42; /* 2. luôn kiểm tra trước khi giải tham chiếu */
int *q = malloc(100);
if (q == NULL) return -1; /* 3. luôn kiểm tra kết quả cấp phát */
*q = 1;
free(q);
q = NULL; /* 4. đặt NULL ngay sau khi giải phóng */Tự làm thử
- Khai báo một biến và một con trỏ tới nó, in đủ năm biểu thức trong bảng, đối chiếu với sơ đồ.
- In
sizeofcủa con trỏ tới bốn kiểu khác nhau, xác nhận chúng bằng nhau. - Trỏ cả
int *vàchar *vào cùng một biến, in giá trị qua cả hai và giải thích chênh lệch. - Viết ba chương trình tạo ra ba loại con trỏ nguy hiểm, chạy dưới
-fsanitize=addressvà so sánh thông báo. - Khai báo
int *a, b;rồi thử gánb = &n. Đọc thông báo lỗi và giải thích. - Viết hàm trả về địa chỉ biến cục bộ, gọi nó rồi gọi một hàm khác, in giá trị và quan sát nó bị đè.
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
- Con trỏ là biến bình thường, chỉ khác ở chỗ giá trị của nó là một địa chỉ.
- Dấu sao trong khai báo gắn với biến chứ không gắn với kiểu, nên
int *a, b;chỉ cóalà con trỏ. - Mọi con trỏ đều rộng như nhau, nhưng kiểu quyết định đọc mấy byte, nhảy mấy byte và diễn giải thế nào.
- Ba loại nguy hiểm là con trỏ rỗng, con trỏ hoang và con trỏ treo. Con trỏ rỗng là loại an toàn nhất vì kiểm tra được.
- Bốn quy tắc: luôn khởi tạo, luôn kiểm tra trước khi giải tham chiếu, luôn kiểm tra kết quả cấp phát, và đặt
NULLngay sau khi giải phóng.