Bài 30.124 phút đọc
Con trỏ void và cái giá của nó
Sau bài này bạn sẽ làm được
- Dùng con trỏ void làm kiểu chung cho mọi con trỏ dữ liệu
- Viết hàm hoán đổi tổng quát bằng byte
- Nêu bốn thứ mất đi khi xóa kiểu
- Nhận ra vấn đề thẳng hàng khi cấp phát vùng chung
Chương 29 hoãn quyết định gọi hàm nào. Chương này hoãn quyết định xử lý kiểu nào. Công cụ duy nhất C cho bạn là con trỏ không kiểu, và nó mạnh đúng bằng mức nó nguy hiểm.
#Con trỏ void là gì
Con trỏ void
Con trỏ tới một vùng nhớ mà trình biên dịch không biết chứa gì. Nó giữ được địa chỉ của bất kỳ đối tượng nào, và mọi con trỏ dữ liệu chuyển qua lại với nó được mà không mất thông tin.
void *p;
int x = 5;
p = &x; /* KHONG can ep kieu trong C */
printf("%d\n", *(int *)p); /* PHAI ep kieu truoc khi giai tham chieu */
double d = 1.5;
p = &d; /* cung con tro do, gio tro toi double */| Thao tác | Hợp lệ | Ghi chú |
|---|---|---|
| Gán con trỏ bất kỳ vào void * | Có | Không cần ép kiểu trong C, khác C++ |
| Gán void * vào con trỏ khác | Có | Cũng không cần ép, nhưng nên ép cho rõ |
| *p | Không | Không biết đọc bao nhiêu byte |
| p[0] | Không | Cùng lý do |
| p + 1 | Không chuẩn | GCC cho phép và coi như char *, nhưng đó là mở rộng |
| sizeof(void) | Không chuẩn | GCC trả 1, chuẩn thì cấm |
| p == NULL | Có | So sánh bình thường |
| memcpy(p, q, n) | Có | Mọi hàm nhận void * đều dùng được |
#Hoán đổi tổng quát
Một hàm cho mỗi kiểu
void hoan_doi_int(int *a, int *b) { int t = *a; *a = *b; *b = t; }
void hoan_doi_double(double *a, double *b) { double t = *a; *a = *b; *b = t; }
void hoan_doi_sv(SV *a, SV *b) { SV t = *a; *a = *b; *b = t; }
/* Mot ham cho moi kieu, va ban khong bao gio viet du. */Một hàm, làm việc trên byte
#include <stddef.h>
void hoan_doi(void *a, void *b, size_t co) {
unsigned char *pa = a, *pb = b;
for (size_t i = 0; i < co; ++i) {
unsigned char t = pa[i];
pa[i] = pb[i];
pb[i] = t;
}
}
int x = 1, y = 2; hoan_doi(&x, &y, sizeof x);
double u = 1.5, v = 2.5; hoan_doi(&u, &v, sizeof u);
SV a = {"An", 20}, b = {"Binh", 21}; hoan_doi(&a, &b, sizeof a);terminal
gcc -std=c11 -Wall -Wextra -o hoan-doi hoan-doi.c && ./hoan-doi
int: 2 1 double: 2.5 1.5 struct: Binh 21 | An 20
#Bốn thứ bạn mất
| Mất gì | Cụ thể | Hậu quả |
|---|---|---|
| Kiểm kiểu | Trình biên dịch không so được kiểu nữa | Truyền nhầm kiểu vẫn dịch sạch, sai lúc chạy |
| Kích thước | Bạn phải tự truyền sizeof | Truyền sai kích thước là đọc ghi ngoài vùng |
| Tối ưu | Trình biên dịch không biết bố cục nên không vector hóa | Chậm hơn bản có kiểu vài lần |
| Khả năng đọc | Đọc mã không biết vùng đó chứa gì | Người bảo trì phải tra tài liệu |
#Vấn đề thẳng hàng
Thẳng hàng
Yêu cầu rằng địa chỉ của một đối tượng phải chia hết cho một số nào đó. Trên x86 truy cập lệch hàng chỉ chậm hơn, còn trên nhiều kiến trúc khác thì nó gây lỗi phần cứng.
terminal
./g1 --thang-hang
alignof(char)=1 int=4 double=8 max_align_t=16 sizeof(max_align_t) = 32
Bốn quy tắc khi làm việc với vùng nhớ thô
- Dùng
unsigned char *để chạy qua byte. Đó là kiểu duy nhất chuẩn cho phép nhìn mọi đối tượng như dãy byte. - Ép sang
char *trước khi làm số học. Số học trênvoid *là mở rộng của GCC. - Làm tròn lên trước mỗi lần cắt vùng. Và dùng
alignofchứ đừng đoán con số. - Bật
-fsanitize=undefinedkhi kiểm thử. Nó bắt được lỗi lệch hàng mà x86 vẫn chạy được.
Tự làm thử
- Cài
hoan_doitổng quát và dùng nó choint,doublevà một struct. - Gọi nó với hai kiểu khác nhau và xác nhận trình biên dịch không cảnh báo gì.
- Viết macro
HOAN_DOItự tínhsizeof, rồi thêm_Static_assertkiểm hai kiểu giống nhau. - Hoán đổi một biến với chính nó và xác nhận giá trị không đổi.
- Cắt một mảng
unsigned charthành một chuỗi 5 byte rồi mộtdouble, chạy dưới-fsanitize=undefined. - In
alignofcủa năm kiểu khác nhau trên máy bạn.
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
void *giữ được địa chỉ của mọi đối tượng dữ liệu, nhưng không giải tham chiếu và không làm số học được.- Hoán đổi tổng quát chạy trên
unsigned char, kiểu duy nhất chuẩn cho phép nhìn mọi đối tượng như dãy byte. - Xóa kiểu là mất kiểm kiểu, mất kích thước tự động, mất tối ưu, và mất khả năng đọc.
- Đẩy
void *xuống tầng cài đặt, giữ giao diện công khai có kiểu. mallocluôn trả về địa chỉ thẳng hàng, nhưng khi bạn tự cắt một vùng lớn thì phải làm tròn lên bằngalignof.