Bài 13.718 phút đọc
Con trỏ void
Sau bài này bạn sẽ làm được
- Dùng con trỏ void để viết hàm chạy với mọi kiểu
- Biết vì sao không giải tham chiếu con trỏ void được
- Cài hàm hoán đổi tổng quát
- Nhận ra rủi ro mất kiểm tra kiểu
Kiểu void * giữ được địa chỉ nhưng cố tình vứt bỏ thông tin kiểu. Nghe như một bước lùi, nhưng đó chính là công cụ duy nhất của C để viết hàm chạy được với mọi kiểu dữ liệu.
#Con trỏ không kiểu
Con trỏ void
Con trỏ chỉ mang địa chỉ, không mang thông tin về kiểu của đối tượng ở địa chỉ đó. Nó chuyển tự do sang và về từ mọi con trỏ đối tượng khác.
void *p;
int n = 42;
double d = 3.14;
char s[] = "abc";
p = &n; /* hợp lệ */
p = &d; /* hợp lệ */
p = s; /* hợp lệ */| Đặc điểm | void * |
|---|---|
| Giữ được địa chỉ | Có, mọi con trỏ đối tượng |
| Giải tham chiếu được | Không, vì không biết đọc mấy byte |
| Số học con trỏ | Không theo chuẩn, GCC coi như char sao |
| sizeof | 8 trên máy sáu tư bit, như mọi con trỏ |
| Giữ được con trỏ hàm | Không bảo đảm theo chuẩn |
#Chuyển đổi tự động
chuyen-doi.c
int n = 42;
int *pi = &n;
void *pv = pi; /* int sao sang void sao: tự động, không cần ép */
int *pi2 = pv; /* void sao sang int sao: cũng tự động trong C */
printf("%d\n", *pi2); /* 42 */Vòng chuyển đổi luôn an toàn
int n = 42;
int *p1 = &n;
void *pv = p1;
int *p2 = pv;
/* Chuẩn bảo đảm p1 == p2 */#Ba điều không làm được
Không giải tham chiếu
void *p = &n;
*p; /* error: invalid use of void expression */
*(int *)p; /* đúng: ép về kiểu cụ thể trước */Lý do rất trực tiếp: trình biên dịch không biết đọc bao nhiêu byte và diễn giải chúng ra sao. Bạn phải nói cho nó biết bằng phép ép kiểu.
Không làm số học con trỏ
void *p = base;
p + 1; /* không chuẩn: GCC cho phép và coi như char sao */
(char *)p + 1; /* đúng và di động */
(char *)p + i * co; /* mẫu chuẩn để đi tới phần tử thứ i */Không giữ con trỏ hàm một cách bảo đảm
int (*f)(int) = ham_nao_do;
void *p = (void *)f; /* không bảo đảm theo chuẩn C */
int (*g)(int) = (int (*)(int))p;#Hàm hoán đổi tổng quát
Một hàm cho mỗi kiểu
void hoan_doi_int(int *a, int *b);
void hoan_doi_double(double *a, double *b);
void hoan_doi_char(char *a, char *b);
void hoan_doi_sv(SinhVien *a, SinhVien *b);
/* Thêm kiểu mới phải thêm hàm mới. */Một hàm cho mọi kiểu
void hoan_doi(void *a, void *b, size_t co);
int x = 1, y = 2;
double p = 1.5, q = 2.5;
hoan_doi(&x, &y, sizeof x);
hoan_doi(&p, &q, sizeof p);hoan-doi.c
#include <string.h>
/* Cách 1: đơn giản, dùng bộ đệm tạm cố định. */
void hoan_doi(void *a, void *b, size_t co)
{
if (a == NULL || b == NULL || a == b || co == 0) return;
unsigned char *x = a;
unsigned char *y = b;
for (size_t i = 0; i < co; ++i) {
unsigned char t = x[i];
x[i] = y[i];
y[i] = t;
}
}
/* Cách 2: nhanh hơn với dữ liệu lớn, dùng bộ đệm theo khối. */
void hoan_doi_nhanh(void *a, void *b, size_t co)
{
if (a == NULL || b == NULL || a == b) return;
unsigned char tam[64];
unsigned char *x = a;
unsigned char *y = b;
while (co > 0) {
size_t n = co < sizeof tam ? co : sizeof tam;
memcpy(tam, x, n);
memcpy(x, y, n);
memcpy(y, tam, n);
x += n;
y += n;
co -= n;
}
}terminal
# Hoán đổi hai struct 4 KB, một triệu lần
./do-hoan-doi
tung byte : 2.914 s theo khoi : 0.183 s ti le : 15.9 lan
Hàm đảo mảng tổng quát
dao.c
void dao_nguoc(void *base, size_t n, size_t co)
{
if (base == NULL || n < 2 || co == 0) return;
unsigned char *p = base;
for (size_t i = 0; i + 1 < n - i; ++i)
hoan_doi(p + i * co, p + (n - 1 - i) * co, co);
}
int a[5] = { 1, 2, 3, 4, 5 };
dao_nguoc(a, 5, sizeof a[0]); /* 5 4 3 2 1 */
char *s[3] = { "a", "b", "c" };
dao_nguoc(s, 3, sizeof s[0]); /* c b a */#Rủi ro mất kiểm tra kiểu
Ba cách giảm rủi ro
giam-rui-ro.c
/* Cách 1: macro bọc, tự tính sizeof từ chính mảng */
#define DAO_NGUOC(mang, n) dao_nguoc((mang), (n), sizeof (mang)[0])
int a[5];
DAO_NGUOC(a, 5); /* không thể sai sizeof nữa */
/* Cách 2: dùng _Generic của C11 để chọn hàm theo kiểu */
#define SO_SANH(a, b) _Generic((a), \
int: so_sanh_int, \
double: so_sanh_double, \
char *: so_sanh_chuoi \
)(&(a), &(b))
/* Cách 3: bọc bằng hàm inline có kiểu cụ thể */
static inline void dao_int(int *a, size_t n)
{
dao_nguoc(a, n, sizeof *a);
}| Cách | Ưu | Nhược |
|---|---|---|
| Macro bọc | Đơn giản, tự lấy sizeof đúng | Không kiểm tra kiểu, đối số có thể bị đánh giá nhiều lần |
| _Generic của C11 | Chọn hàm theo kiểu thật, kiểm tra được | Cú pháp rườm rà, phải liệt kê trước mọi kiểu |
| Hàm bọc có kiểu | Kiểm tra kiểu đầy đủ, dễ đọc, dễ gỡ lỗi | Phải viết một hàm cho mỗi kiểu |
Tự làm thử
- Gán địa chỉ của ba biến kiểu khác nhau vào cùng một
void *, rồi ép về đúng kiểu và in giá trị. - Thử giải tham chiếu
void *trực tiếp, chép lại thông báo lỗi. - Cài
hoan_doitổng quát theo cả hai cách, đo thời gian hoán đổi hai struct 4 kilobyte một triệu lần. - Cài
dao_nguoctổng quát, dùng nó với mảngint, mảngdoublevà mảng con trỏ chuỗi. - Cố tình truyền sai kích thước phần tử, chạy dưới
-fsanitize=addressvà đọc thông báo. - Viết macro
DAO_NGUOCtự tínhsizeof, chứng minh nó chặn được lỗi trên. - Dùng
_Genericđể viết một macro in giá trị đúng định dạng cho ba kiểu khác nhau.
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 mọi địa chỉ nhưng vứt bỏ thông tin kiểu, nên không giải tham chiếu và không làm số học con trỏ được.- Trong C, chuyển giữa
void *và con trỏ khác là tự động. Ép kiểu kết quả củamalloclà thừa. - Mẫu đi tới phần tử thứ i là
(char *)base + i * co, vìsizeof(char)luôn bằng 1. - Hàm tổng quát luôn nhận bộ ba: con trỏ dữ liệu, số phần tử, kích thước một phần tử.
- Cái giá là mất hoàn toàn kiểm tra kiểu. Giảm rủi ro bằng cách bọc lô gic tổng quát trong các hàm
static inlinecó kiểu cụ thể.