Giải tham chiếu
Sau bài này bạn sẽ làm được
- Đọc và ghi giá trị qua con trỏ
- Phân biệt dấu sao khi khai báo với dấu sao khi giải tham chiếu
- Nhận ra bốn tình huống giải tham chiếu không hợp lệ
- Dùng công cụ để bắt lỗi giải tham chiếu
Nếu toán tử lấy địa chỉ là hành động ghi số nhà ra giấy, thì giải tham chiếu là hành động đi tới ngôi nhà đó. Đây là toán tử nguy hiểm nhất trong C, vì nó là chỗ mọi lỗi con trỏ cuối cùng phát nổ.
#Cú pháp và ý nghĩa
* một ngôi đặt trước một con trỏ, cho ra đối tượng mà con trỏ đó đang trỏ tới. Kết quả dùng được cả để đọc lẫn để ghi.#include <stdio.h>
int main(void)
{
int x = 42;
int *p = &x;
printf("%d\n", *p); /* đọc: in ra 42 */
*p = 100; /* ghi: x giờ bằng 100 */
printf("%d\n", x); /* 100 */
*p += 5; /* đọc rồi ghi: x giờ bằng 105 */
printf("%d\n", x); /* 105 */
return 0;
}Điểm mấu chốt: *p và x là cùng một vùng nhớ. Không phải hai bản sao đồng bộ với nhau, mà là đúng một chỗ nhìn qua hai cái tên. Mọi thao tác qua *p đều là thao tác trên x.
| Viết | Nghĩa là | Kiểu |
|---|---|---|
| p | địa chỉ mà con trỏ đang giữ | int * |
| *p | giá trị tại địa chỉ đó | int |
| &(*p) | quay lại chính p | int * |
| *(&x) | quay lại chính x | int |
#Hai vai trò của dấu sao
Dấu * xuất hiện ở hai chỗ hoàn toàn khác nhau, và nhầm lẫn giữa chúng là nguyên nhân số một khiến người mới thấy con trỏ khó.
| Ở đâu | Nghĩa là | Ví dụ |
|---|---|---|
| Trong khai báo | Biến này là con trỏ | int *p; |
| Trong biểu thức | Đi tới chỗ con trỏ trỏ tới | *p = 42; |
| Giữa hai toán hạng | Phép nhân | a * b |
int *p = &x; /* KHAI BÁO: p là con trỏ. Không có phép giải tham chiếu nào. */
int y;
*p = 42; /* BIỂU THỨC: ghi vào chỗ p trỏ tới */
y = *p * 2; /* dấu sao đầu là giải tham chiếu, dấu sao sau là nhân */#Đọc và ghi qua con trỏ
int a[5] = { 10, 20, 30, 40, 50 };
int *p = a;
/* Đọc */
int x = *p; /* 10 */
int y = *(p + 2); /* 30 */
int z = p[2]; /* 30, cùng nghĩa với dòng trên */
/* Ghi */
*p = 99; /* a[0] thành 99 */
*(p + 1) = 88; /* a[1] thành 88 */
p[2] = 77; /* a[2] thành 77 */
/* Đọc rồi ghi */
(*p)++; /* a[0] thành 100 */
*p += 10; /* a[0] thành 110 */Mẫu duyệt bằng con trỏ
/* Cách kinh điển, gặp khắp nơi trong mã C */
for (int *p = a; p < a + n; ++p)
printf("%d ", *p);
/* Với chuỗi thì điều kiện là byte kết thúc */
for (const char *s = chuoi; *s != '\0'; ++s)
putchar(*s);
/* Cách viết cô đọng của thư viện chuẩn */
while ((*d++ = *s++) != '\0')
;Dòng cuối là thân của strcpy ở Bài 11.5. Giờ bạn đọc được nó: giải tham chiếu cả hai con trỏ, gán, tăng cả hai, rồi so kết quả gán với byte kết thúc.
#Bốn cách giải tham chiếu sai
| Trường hợp | Ví dụ | Triệu chứng |
|---|---|---|
| Con trỏ rỗng | int *p = NULL; *p = 1; | Sập ngay, thông báo rõ ràng |
| Con trỏ hoang | int *p; *p = 1; | Có thể sập, có thể phá dữ liệu chỗ khác |
| Con trỏ treo | free(p); *p = 1; | Thường không sập ngay, hỏng dữ liệu |
| Ra ngoài biên | int a[3]; int *p = a + 5; *p = 1; | Ghi đè biến khác |
#include <stdlib.h>
int main(void)
{
/* 1. Con trỏ rỗng */
int *a = NULL;
/* *a = 1; */
/* 2. Con trỏ hoang */
int *b;
/* *b = 1; */
/* 3. Con trỏ treo */
int *c = malloc(sizeof *c);
free(c);
/* *c = 1; */
/* 4. Ra ngoài biên */
int arr[3] = { 0 };
int *d = arr + 5;
/* *d = 1; */
return 0;
}1) SEGV on unknown address 0x000000000000 2) SEGV, dia chi ngau nhien 3) heap-use-after-free 4) stack-buffer-overflow
#Độ ưu tiên với toán tử khác
| Biểu thức | Nghĩa là | Vì sao |
|---|---|---|
| *p + 1 | (*p) + 1 | Toán tử một ngôi ưu tiên cao hơn phép cộng |
| *(p + 1) | phần tử kế tiếp | Ngoặc đổi thứ tự |
| *p++ | *(p++) | Hậu tố ưu tiên cao hơn dấu sao |
| (*p)++ | tăng giá trị | Ngoặc đổi thứ tự |
| *p.x | *(p.x) | Chấm ưu tiên cao hơn, thường là lỗi |
| (*p).x | trường x của struct p trỏ tới | Viết gọn là p->x |
| *p[0] | *(p[0]) | Ngoặc vuông ưu tiên cao hơn |
int y = *p + 1;
int z = *p++;
int w = *ps.x; /* lỗi biên dịch nếu ps là con trỏ tới struct */int y = (*p) + 1;
int z = *(p++);
int w = ps->x; /* dạng viết tắt của (*ps).x */Bảng độ ưu tiên rút gọn
| Mức | Toán tử | Kết hợp |
|---|---|---|
| Cao nhất | () [] . -> ++ hậu tố | Trái sang phải |
| * & ! ~ ++ tiền tố (kiểu) | Phải sang trái | |
| * / % | Trái sang phải | |
| Thấp hơn | + - | Trái sang phải |
Bài 4.6 đã lập bảng độ ưu tiên đầy đủ. Điều cần nhớ ở đây là toán tử giải tham chiếu nằm ở mức thứ hai, thấp hơn ngoặc vuông, dấu chấm và hậu tố. Đó là nguồn của gần như mọi bất ngờ trong bảng trên.
Tự làm thử
- Viết chương trình đổi giá trị một biến qua con trỏ, in giá trị trước và sau bằng cả tên biến lẫn qua con trỏ.
- Chạy thử đủ bốn dạng
(*p)++,*p++,*++p,++*pvà điền vào bảng ba cột như trong bài. - Duyệt một mảng bằng cả chỉ số lẫn con trỏ, xác nhận kết quả giống nhau.
- Bỏ chú thích từng dòng trong chương trình bốn lỗi, chạy dưới
-fsanitize=addressvà chép lại bốn thông báo khác nhau. - Viết struct hai trường, truy cập trường qua cả
(*p).xvàp->x, rồi thử*p.xvà đọc thông báo lỗi. - Tự cài
strcpybằng dòngwhile ((*d++ = *s++))và giải thích từng phần của biểu thức đó.
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
*pvà biến màptrỏ tới là cùng một vùng nhớ, không phải hai bản sao.- Dấu sao trong khai báo và dấu sao trong biểu thức là hai chuyện khác nhau. Trong
int *p = &x;không có phép giải tham chiếu nào. (*p)++tăng giá trị,*p++tăng con trỏ. Luôn dùng ngoặc khi trộn hai toán tử.- Bốn cách giải tham chiếu sai là con trỏ rỗng, hoang, treo và ra ngoài biên. Chỉ loại đầu kiểm tra được bằng if.
- Dùng
p->xthay cho(*p).x, vì*p.xđược hiểu là*(p.x)và gần như luôn là lỗi.