Bỏ qua điều hướng, tới nội dung chính
Học C
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ácHợ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ácCóCũng không cần ép, nhưng nên ép cho rõ
*pKhôngKhông biết đọc bao nhiêu byte
p[0]KhôngCùng lý do
p + 1Không chuẩnGCC cho phép và coi như char *, nhưng đó là mở rộng
sizeof(void)Không chuẩnGCC trả 1, chuẩn thì cấm
p == NULLCó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ểuTrình biên dịch không so được kiểu nữaTruyền nhầm kiểu vẫn dịch sạch, sai lúc chạy
Kích thướcBạn phải tự truyền sizeofTruyền sai kích thước là đọc ghi ngoài vùng
Tối ưuTrình biên dịch không biết bố cục nên không vector hóaChậ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ên void * 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 alignof chứ đừng đoán con số.
  • Bật -fsanitize=undefined khi kiểm thử. Nó bắt được lỗi lệch hàng mà x86 vẫn chạy được.

Tự làm thử

  1. Cài hoan_doi tổng quát và dùng nó cho int, double và một struct.
  2. 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ì.
  3. Viết macro HOAN_DOI tự tính sizeof, rồi thêm _Static_assert kiểm hai kiểu giống nhau.
  4. Hoán đổi một biến với chính nó và xác nhận giá trị không đổi.
  5. Cắt một mảng unsigned char thành một chuỗi 5 byte rồi một double, chạy dưới -fsanitize=undefined.
  6. In alignof củ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.
  • malloc luô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ằng alignof.