Bài 52.328 phút đọc
Kiểu hiệu dụng
Sau bài này bạn sẽ làm được
- Định nghĩa kiểu hiệu dụng và cách nó hình thành
- Phát biểu quy tắc strict aliasing ở tầng chuẩn
- Nêu năm kiểu luôn được phép truy cập một đối tượng
- Viết type punning đúng bằng ba cách
Bài 33.6 đã cho thấy vi phạm strict aliasing làm chương trình cho ra hai kết quả khác nhau. Bài này nói về cơ chế đằng sau nó ở tầng chuẩn: kiểu hiệu dụng, và vì sao bộ nhớ từ malloc hoạt động khác biến khai báo.
#Kiểu hiệu dụng là gì
Kiểu hiệu dụng (effective type)
Kiểu thật sự của một đối tượng trong bộ nhớ, dùng để xác định phép truy cập nào là hợp lệ. Với đối tượng có khai báo thì đó là kiểu khai báo. Với bộ nhớ cấp phát động thì nó chưa có, và được xác định bởi lần ghi đầu tiên.
Ba trường hợp
/* 1. Doi tuong CO KHAI BAO: kieu hieu dung la kieu khai bao,
va no KHONG BAO GIO doi */
int x; /* kieu hieu dung = int, vinh vien */
float *p = (float *)&x;
*p = 1.0f; /* UB: ghi float vao doi tuong kieu int */
/* 2. Bo nho CAP PHAT: chua co kieu hieu dung */
void *m = malloc(sizeof(int)); /* chua co kieu */
*(int *)m = 5; /* kieu hieu dung gio la int */
float f = *(float *)m; /* UB: doc int qua float * */
/* 3. Ghi lai bang kieu khac: kieu hieu dung DOI */
*(float *)m = 1.0f; /* kieu hieu dung gio la float */
float g = *(float *)m; /* HOP LE */
int i = *(int *)m; /* UB */#Bộ nhớ từ malloc chưa có kiểu
Vì sao điều này quan trọng
/* Neu bo nho tu malloc CO kieu co dinh, thi khong the viet duoc
mot bo cap phat, mot be nho, hay bat cu thu gi tai su dung bo nho.
Chuan giai quyet bang cach noi: bo nho tu malloc co kieu hieu dung
duoc XAC DINH LAI moi khi ban ghi vao no bang mot kieu khac.
Nen doan nay hoan toan hop le: */
void *m = malloc(1024);
*(int *)m = 5; /* kieu hieu dung: int */
int a = *(int *)m; /* hop le */
*(double *)m = 3.14; /* kieu hieu dung: gio la double */
double b = *(double *)m; /* hop le */
SinhVien *sv = m;
*sv = (SinhVien){ .ma = 1 }; /* kieu hieu dung: gio la SinhVien */
free(m);
/* Ba lan dung lai cung mot vung nho voi ba kieu khac nhau.
Do la ca diem cua "chua co kieu". */#Năm kiểu luôn được phép
| Truy cập đối tượng kiểu int qua | Hợp lệ | Vì sao |
|---|---|---|
| int * | Có | Cùng kiểu |
| const int * hoặc volatile int * | Có | Chỉ thêm từ khóa |
| unsigned int * | Có | Bản không dấu tương ứng |
| signed int * | Có | Cùng kiểu, signed int là int |
| char * hoặc unsigned char * | Có | Ngoại lệ kiểu ký tự |
| struct chứa int * | Có | Kiểu gộp chứa kiểu đó |
| long * | Không | Kiểu khác, kể cả khi cùng độ rộng |
| float * | Không | Kiểu khác |
| int32_t * | Tùy | Hợp lệ nếu int32_t là typedef của int |
#Ba cách type punning đúng
Ép con trỏ
float f = 1.0f;
uint32_t bits = *(uint32_t *)&f; /* VI PHAM */
/* Bai 33.6 da do: o -O2, ham tra ve 1 trong khi o nho chua
1073741824. Hai gia tri khac nhau cho cung mot dia chi. */Ba cách đúng
/* 1. memcpy: dung o CA C lan C++, va khong ton them lenh nao */
float f = 1.0f;
uint32_t bits;
memcpy(&bits, &f, sizeof bits);
/* 2. union: hop le trong C, UB trong C++ */
union { float f; uint32_t u; } t = { .f = 1.0f };
uint32_t bits = t.u;
/* 3. unsigned char *: luon hop le, nhung phai tu ghep byte */
unsigned char *b = (unsigned char *)&f;
/* b[0] toi b[3] la bon byte cua f */terminal
# Ba hàm, ba cách, cùng một lệnh hợp ngữ
gcc -std=c11 -O2 -S -masm=intel -o - alias.c
doc_bit_sai:
movd eax, xmm0
ret
doc_bit_memcpy:
movd eax, xmm0
ret
doc_bit_union:
movd eax, xmm0
retMacro tiện dụng
#include <string.h>
/* C11: kiem tra kich thuoc luc dich */
#define DOI_BIT(T, x) __extension__({ \
_Static_assert(sizeof(T) == sizeof(x), "kich thuoc khong khop"); \
T _kq; \
memcpy(&_kq, &(x), sizeof _kq); \
_kq; \
})
float f = 1.0f;
uint32_t bits = DOI_BIT(uint32_t, f);
/* Macro nay dung bieu thuc khoi cua GNU, nen chi chay tren
GCC va Clang. Ban chuan phai la mot ham: */
static inline uint32_t float_sang_bit(float f) {
uint32_t u;
memcpy(&u, &f, sizeof u);
return u;
}
static inline float bit_sang_float(uint32_t u) {
float f;
memcpy(&f, &u, sizeof f);
return f;
}
/* Va tu GCC 11 hoac Clang 14: */
uint32_t bits = __builtin_bit_cast(uint32_t, f); /* ro nghia nhat */#Ba tình huống thật
Một: đọc định dạng nhị phân
unsigned char bo_dem[1024];
fread(bo_dem, 1, sizeof bo_dem, f);
/* SAI: ca ve trung bi danh lan ve can chinh */
TieuDe *td = (TieuDe *)bo_dem;
/* DUNG */
TieuDe td;
memcpy(&td, bo_dem, sizeof td);
/* Nhung chu y: cach nay van phu thuoc vao BO CUC struct cua may ban.
Bai 53.4 se noi ve cach tuan tu hoa tung truong, la cach duy nhat
thuc su kha chuyen. */Hai: hàm băm trên dữ liệu bất kỳ
/* Ham bam nhan mot doi tuong bat ky va doc byte cua no */
uint64_t bam(const void *du_lieu, size_t n) {
const unsigned char *p = du_lieu; /* HOP LE, ngoai le char */
uint64_t h = 14695981039346656037ULL;
for (size_t i = 0; i < n; ++i) {
h ^= p[i];
h *= 1099511628211ULL;
}
return h;
}
/* Doc qua unsigned char * la HOP LE voi moi doi tuong.
Do la ly do ham bam kieu nay viet duoc bang C chuan.
NHUNG: dung bam mot struct co PHAN DEM, vi byte dem
khong co gia tri xac dinh. Bai 53.1. */Ba: bể nhớ và bộ cấp phát
Bể nhớ hợp lệ về kiểu hiệu dụng
#include <stdlib.h>
#include <stddef.h>
typedef struct {
unsigned char *bo_nho; /* tu malloc, nen CHUA co kieu */
size_t co;
size_t da_dung;
} Be;
int be_tao(Be *b, size_t co) {
b->bo_nho = malloc(co); /* quan trong: malloc, khong phai mang tinh */
if (b->bo_nho == NULL) return -1;
b->co = co;
b->da_dung = 0;
return 0;
}
void *be_cap(Be *b, size_t n, size_t can_chinh) {
size_t vi_tri = (b->da_dung + can_chinh - 1) / can_chinh * can_chinh;
if (vi_tri + n > b->co) return NULL;
b->da_dung = vi_tri + n;
return b->bo_nho + vi_tri;
}
/* Dung: */
SinhVien *sv = be_cap(&b, sizeof *sv, _Alignof(SinhVien));
if (sv != NULL) *sv = (SinhVien){ .ma = 1 }; /* dat kieu hieu dung */
/* Vi bo nho den tu malloc, no chua co kieu hieu dung, nen lan ghi
nay hop le. Neu "bo_nho" la mot mang tinh thi day la vung xam. */Tự làm thử
- Viết chương trình ghi
intrồi đọcfloattừ cùng vùngmallocvà chạy ở-O0và-O2. - Chứng minh ghi lại bằng kiểu khác thì đổi được kiểu hiệu dụng.
- Kiểm tra
int64_ttrên máy bạn làlonghaylong long. - Viết ba hàm đọc bit của
floatvà so hợp ngữ. - Viết một bể nhớ dựa trên
mallocvà giải thích vì sao nó hợp lệ. - Đổi bể nhớ đó sang mảng tĩnh và chỉ ra chỗ trở thành vùng xám.
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
- Đối tượng có khai báo thì kiểu hiệu dụng là kiểu khai báo và không bao giờ đổi.
- Bộ nhớ từ
mallocchưa có kiểu hiệu dụng, và lần ghi gần nhất xác định nó. - Năm loại kiểu được phép truy cập, và ngoại lệ
charlà thứ làmmemcpyvàfreadviết được bằng C. - Ngoại lệ
charchỉ một chiều: đọc mọi thứ quaunsigned char *thì được, ghiintvào mảngcharthì không. memcpy, union vàunsigned char *là ba cách đúng, và cả ba không tốn thêm lệnh nào.