Bài 17.118 phút đọc
Union cơ bản
Sau bài này bạn sẽ làm được
- Khai báo union và tính kích thước của nó
- So sánh union với struct trên bốn tiêu chí
- Biết quy tắc chỉ được đọc thành viên vừa ghi
- Hiểu ngoại lệ về tiền tố chung của chuẩn C
Union trông giống struct nhưng làm điều ngược lại: thay vì mỗi trường có chỗ riêng, mọi trường dùng chung một vùng nhớ. Đây là công cụ sắc nhưng nguy hiểm, và bài này nói rõ ranh giới an toàn của nó.
#Mọi thành viên dùng chung một chỗ
union.c
#include <stdio.h>
union Data {
int i;
float f;
char c;
};
int main(void)
{
union Data d;
d.i = 42;
printf("%d\n", d.i); /* 42 */
d.f = 3.14f; /* GHI ĐÈ lên cùng vùng nhớ */
printf("%.2f\n", d.f); /* 3.14 */
printf("%d\n", d.i); /* 1078523331, không còn là 42 */
printf("%zu\n", sizeof(union Data)); /* 4 */
return 0;
}terminal
./union
42 3.14 1078523331 4
Union
Một kiểu mà mọi thành viên bắt đầu ở cùng địa chỉ. Kích thước của nó bằng thành viên lớn nhất, và tại một thời điểm chỉ có một thành viên chứa giá trị có nghĩa.
#Kích thước và căn chỉnh
kich-thuoc.c
#include <stdio.h>
union A { char c; int i; }; /* 4 */
union B { char c; double d; }; /* 8 */
union C { char s[10]; int i; }; /* 12, không phải 10 */
union D { char s[16]; double d; }; /* 16 */
int main(void)
{
printf("%zu %zu %zu %zu\n",
sizeof(union A), sizeof(union B),
sizeof(union C), sizeof(union D));
printf("can chinh: %zu %zu %zu %zu\n",
_Alignof(union A), _Alignof(union B),
_Alignof(union C), _Alignof(union D));
return 0;
}terminal
./kich-thuoc
4 8 12 16 can chinh: 4 8 4 8
| Quy tắc | Nội dung |
|---|---|
| Kích thước tối thiểu | Bằng thành viên lớn nhất |
| Căn chỉnh | Bằng căn chỉnh lớn nhất trong các thành viên |
| Kích thước cuối cùng | Làm tròn lên bội của căn chỉnh, nên có thể có đệm ở cuối |
| Địa chỉ mọi thành viên | Giống nhau, bằng địa chỉ của chính union |
cung-dia-chi.c
union Data d;
printf("%p\n", (void *)&d);
printf("%p\n", (void *)&d.i);
printf("%p\n", (void *)&d.f);
printf("%p\n", (void *)&d.c);
/* Bốn dòng in ra CÙNG một địa chỉ */terminal
./cung-dia-chi
0x7ffd3a2c1e4c 0x7ffd3a2c1e4c 0x7ffd3a2c1e4c 0x7ffd3a2c1e4c
#So với struct
struct S { int i; float f; char c; }; /* mỗi trường có chỗ riêng */
union U { int i; float f; char c; }; /* mọi trường dùng chung */| Tiêu chí | struct | union |
|---|---|---|
| Kích thước | Tổng các trường cộng phần đệm, ở đây 12 | Bằng trường lớn nhất, ở đây 4 |
| Số giá trị lưu được cùng lúc | Tất cả | Đúng một |
| Địa chỉ các trường | Khác nhau | Giống nhau |
| Ghi vào một trường | Không ảnh hưởng trường khác | Phá mọi trường khác |
| Khởi tạo | Đặt được nhiều trường | Chỉ đặt được một, mặc định là trường đầu |
| Dùng khi | Cần lưu nhiều thứ cùng lúc | Cần lưu một trong nhiều thứ |
khoi-tao.c
union Data {
int i;
float f;
char s[8];
};
union Data a = { 42 }; /* khởi tạo THÀNH VIÊN ĐẦU, tức i */
union Data b = { .f = 3.14f }; /* có chỉ định, C99, rõ ràng hơn */
union Data c = { .s = "abc" };
union Data d = { 0 }; /* xóa sạch */
/* KHÔNG đặt được hai thành viên cùng lúc */
union Data e = { .i = 1, .f = 2.0f }; /* biên dịch qua, nhưng chỉ f có hiệu lực */Union bên trong struct
typedef struct {
int loai;
union { /* union vô danh của C11, xem Bài 15.4 */
int i;
double d;
char *s;
};
} GiaTri;
GiaTri g = { .loai = 0, .i = 42 };
printf("%d\n", g.i); /* không cần viết g.u.i */Đây là mẫu quan trọng nhất của cả chương, và Bài 17.2 dành riêng cho nó.
#Quy tắc chỉ đọc thành viên vừa ghi
Quy tắc cơ bản
Chỉ được đọc thành viên mà bạn vừa ghi vào gần nhất. Đọc thành viên khác cho ra kết quả phụ thuộc cách máy biểu diễn dữ liệu, và không di động.
Ghi một, đọc cái khác
union Data d;
d.f = 3.14f;
printf("%d\n", d.i); /* đọc thành viên KHÔNG phải cái vừa ghi */
/* Kết quả phụ thuộc: kích thước kiểu, thứ tự byte, cách biểu diễn số thực.
Cùng mã, hai máy khác nhau cho hai kết quả khác nhau. */Nhớ thành viên nào đang hoạt động
typedef enum { KIEU_INT, KIEU_FLOAT } Kieu;
typedef struct {
Kieu kieu; /* NHÃN: nhớ thành viên nào đang hoạt động */
union {
int i;
float f;
};
} GiaTri;
GiaTri g = { .kieu = KIEU_FLOAT, .f = 3.14f };
if (g.kieu == KIEU_FLOAT)
printf("%.2f\n", g.f); /* chỉ đọc đúng cái đang hoạt động */| Tình huống | An toàn không | Ghi chú |
|---|---|---|
| Ghi i rồi đọc i | Có | Trường hợp bình thường |
| Ghi f rồi đọc mảng unsigned char | Có trong C | Xem byte thô, luôn hợp lệ vì mọi biểu diễn byte đều là unsigned char hợp lệ |
| Ghi f rồi đọc i cùng kích thước | Hợp lệ trong C nhưng không di động | Kết quả phụ thuộc cách máy biểu diễn số thực |
| Ghi thành viên nhỏ rồi đọc thành viên lớn | Không | Phần dôi ra chưa từng được ghi, chứa rác |
| Đọc trước khi ghi bất cứ gì | Không | Toàn bộ union chứa rác |
thanh-vien-lon.c
union U { char c; long long ll; };
union U u;
u.c = 'A'; /* chỉ ghi 1 byte */
printf("%lld\n", u.ll); /* đọc 8 byte, trong đó 7 byte là RÁC */terminal
gcc -fsanitize=memory -g bai.c -o t && ./t
WARNING: MemorySanitizer: use-of-uninitialized-value
#0 in main bai.c:8#Ngoại lệ về tiền tố chung
Chuẩn C có một ngoại lệ quan trọng cho quy tắc chỉ đọc thành viên vừa ghi, và nó chính là nền tảng của union có nhãn.
Tiền tố chung
Nếu một union chứa nhiều struct, và các struct đó bắt đầu bằng cùng một dãy thành viên có kiểu tương thích, thì bạn được phép đọc phần chung đó qua bất kỳ thành viên nào, dù thành viên đó có phải cái vừa ghi hay không.
tien-to-chung.c
typedef enum { HINH_TRON, HINH_CHU_NHAT } LoaiHinh;
typedef struct { LoaiHinh loai; double r; } HinhTron;
typedef struct { LoaiHinh loai; double rong, cao; } HinhChuNhat;
typedef union {
LoaiHinh loai; /* cũng là tiền tố chung */
HinhTron tron;
HinhChuNhat chu_nhat;
} Hinh;
double dien_tich(const Hinh *h)
{
/* Đọc h->loai là HỢP LỆ dù ta vừa ghi vào h->tron hay h->chu_nhat,
vì loai là tiền tố chung của cả hai. */
switch (h->loai) {
case HINH_TRON:
return 3.14159265358979 * h->tron.r * h->tron.r;
case HINH_CHU_NHAT:
return h->chu_nhat.rong * h->chu_nhat.cao;
}
return 0.0;
}
Hinh a = { .tron = { .loai = HINH_TRON, .r = 2.0 } };
printf("%.2f\n", dien_tich(&a)); /* 12.57 */Cách phổ biến hơn: nhãn nằm ngoài union
nhan-ngoai.c
/* Cách này không cần dựa vào ngoại lệ tiền tố chung, nên đơn giản hơn
và di động hơn. Đây là mẫu bạn nên dùng mặc định. */
typedef struct {
LoaiHinh loai; /* nhãn nằm NGOÀI union */
union {
struct { double r; } tron;
struct { double rong, cao; } chu_nhat;
};
} Hinh;
double dien_tich(const Hinh *h)
{
switch (h->loai) {
case HINH_TRON: return 3.14159265358979 * h->tron.r * h->tron.r;
case HINH_CHU_NHAT: return h->chu_nhat.rong * h->chu_nhat.cao;
}
return 0.0;
}
Hinh a = { .loai = HINH_TRON, .tron = { .r = 2.0 } };| Nhãn trong tiền tố chung | Nhãn ngoài union | |
|---|---|---|
| Dựa vào ngoại lệ của chuẩn | Có | Không |
| Kích thước | Nhỏ hơn một chút | Lớn hơn, vì nhãn nằm riêng |
| Dễ hiểu | Kém, phải biết ngoại lệ | Rất, ai đọc cũng hiểu |
| Dễ sai khi sửa | Có, đổi thứ tự trường là hỏng | Không |
| Dùng ở đâu | Mã hệ thống cũ, ví dụ struct sockaddr | Mã mới |
Tự làm thử
- Ghi vào một thành viên union rồi đọc thành viên khác, in cả hai giá trị và giải thích con số nhận được.
- In
sizeofvà_Alignofcủa bốn union trong bài, giải thích vì sao union chứa mảng mười ký tự lại là 12 byte. - In địa chỉ của union và của từng thành viên, xác nhận tất cả giống nhau.
- So sánh
sizeofcủa một struct và một union cùng ba trường, giải thích chênh lệch. - Ghi thành viên nhỏ rồi đọc thành viên lớn, chạy dưới
-fsanitize=memorynếu có Clang. - Cài mẫu tiền tố chung với ba loại hình, viết hàm tính diện tích cho cả ba.
- Viết lại cùng bài toán bằng cách đặt nhãn ngoài union, so sánh
sizeofvà độ dễ đọc của hai 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
- Mọi thành viên của union bắt đầu ở cùng địa chỉ, nên ghi vào một thành viên là ghi đè lên mọi thành viên khác.
sizeofbằng thành viên lớn nhất, làm tròn lên bội của căn chỉnh lớn nhất.- Chỉ đọc thành viên vừa ghi. Trong C, đọc thành viên khác là hợp lệ nhưng không di động, còn trong C++ thì là hành vi không xác định.
- Luôn dùng khởi tạo có chỉ định với union, vì không có nó thì giá trị luôn gán cho thành viên đầu tiên.
- Ngoại lệ tiền tố chung cho phép đọc phần đầu chung của nhiều struct trong cùng union, và đó là nền tảng của union có nhãn.