Bài 17.222 phút đọc
Union có nhãn
Sau bài này bạn sẽ làm được
- Cài union có nhãn để lưu nhiều kiểu trong một biến
- Viết hàm xử lý an toàn dựa trên nhãn
- Nhận ra mẫu này trong trình phân tích cú pháp và trình thông dịch
- So sánh với cách dùng con trỏ void
Union một mình rất nguy hiểm vì không có cách nào biết thành viên nào đang hoạt động. Ghép nó với một enum làm nhãn thì bạn được một kiểu biến thể an toàn, và đó là mẫu quan trọng nhất của cả Phần 7.
#Ghép enum với union
Union có nhãn
Một struct gồm hai phần: một enum cho biết thành viên nào đang hoạt động, và một union chứa dữ liệu. Mọi thao tác đều phải kiểm tra nhãn trước.
gia-tri.c
typedef enum {
GT_RONG,
GT_INT,
GT_DOUBLE,
GT_CHUOI,
GT_SO_LUONG,
} LoaiGiaTri;
typedef struct {
LoaiGiaTri loai;
union { /* union vô danh của C11, xem Bài 15.4 */
long i;
double d;
char *s; /* SỞ HỮU: phải giải phóng trong gt_huy */
};
} GiaTri;| Nhãn | Thành viên hoạt động | Ý nghĩa |
|---|---|---|
| GT_RONG | không có | Chưa có giá trị |
| GT_INT | i | Một số nguyên |
| GT_DOUBLE | d | Một số thực |
| GT_CHUOI | s | Một chuỗi do struct sở hữu |
kich-thuoc.c
#include <stdio.h>
int main(void)
{
printf("union rieng : %zu\n", sizeof(union { long i; double d; char *s; }));
printf("GiaTri : %zu\n", sizeof(GiaTri));
/* So với việc dùng struct thay cho union */
typedef struct {
LoaiGiaTri loai;
long i;
double d;
char *s;
} GiaTriPhi;
printf("GiaTriPhi : %zu\n", sizeof(GiaTriPhi));
return 0;
}terminal
./kich-thuoc
union rieng : 8 GiaTri : 16 GiaTriPhi : 32
Union tiết kiệm một nửa so với struct đầy đủ, và chênh lệch càng lớn khi số biến thể càng nhiều. Với một triệu giá trị thì đó là mười sáu megabyte.
#Bộ hàm thao tác an toàn
gia-tri.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Tạo: một hàm cho mỗi biến thể. */
GiaTri gt_int(long v)
{
return (GiaTri){ .loai = GT_INT, .i = v };
}
GiaTri gt_double(double v)
{
return (GiaTri){ .loai = GT_DOUBLE, .d = v };
}
/* Chuỗi cần cấp phát, nên hàm này có thể thất bại. */
int gt_chuoi(const char *s, GiaTri *ra)
{
if (s == NULL || ra == NULL) return -1;
size_t n = strlen(s) + 1;
char *p = malloc(n);
if (p == NULL) return -1;
memcpy(p, s, n);
*ra = (GiaTri){ .loai = GT_CHUOI, .s = p };
return 0;
}
/* Hủy: chỉ biến thể chuỗi mới cần dọn. Gọi nhiều lần vẫn an toàn. */
void gt_huy(GiaTri *g)
{
if (g == NULL) return;
if (g->loai == GT_CHUOI) {
free(g->s);
g->s = NULL;
}
g->loai = GT_RONG;
}
/* Đọc an toàn: trả về 0 nếu đúng kiểu, âm một nếu không. */
int gt_lay_int(const GiaTri *g, long *ra)
{
if (g == NULL || ra == NULL || g->loai != GT_INT) return -1;
*ra = g->i;
return 0;
}
/* In: switch không có default, để trình biên dịch bắt lỗi thiếu nhánh */
void gt_in(const GiaTri *g)
{
if (g == NULL) { printf("(null)\n"); return; }
switch (g->loai) {
case GT_RONG: printf("(rong)\n"); return;
case GT_INT: printf("%ld\n", g->i); return;
case GT_DOUBLE: printf("%.6g\n", g->d); return;
case GT_CHUOI: printf("\"%s\"\n", g->s); return;
case GT_SO_LUONG: break;
}
printf("(loai la: %d)\n", (int)g->loai);
}
/* Sao chép sâu: biến thể chuỗi phải cấp vùng mới, xem Bài 15.7 */
int gt_ban_sao(const GiaTri *goc, GiaTri *ra)
{
if (goc == NULL || ra == NULL) return -1;
if (goc->loai == GT_CHUOI)
return gt_chuoi(goc->s, ra);
*ra = *goc; /* các biến thể khác không có con trỏ, sao chép nông là đủ */
return 0;
}dung.c
int main(void)
{
GiaTri a = gt_int(42);
GiaTri b = gt_double(3.14);
GiaTri c;
if (gt_chuoi("xin chao", &c) != 0) return 1;
gt_in(&a);
gt_in(&b);
gt_in(&c);
long v;
if (gt_lay_int(&a, &v) == 0) printf("lay duoc: %ld\n", v);
if (gt_lay_int(&b, &v) != 0) printf("b khong phai int\n");
GiaTri d;
if (gt_ban_sao(&c, &d) == 0) {
gt_huy(&c);
gt_in(&d); /* vẫn hợp lệ, vì d có chuỗi riêng */
gt_huy(&d);
}
gt_huy(&a);
gt_huy(&b);
return 0;
}terminal
gcc -std=c17 -Wall -Wextra -g -fsanitize=address gia-tri.c dung.c -o t && ./t
42 3.14 "xin chao" lay duoc: 42 b khong phai int "xin chao"
valgrind --leak-check=full ./t
All heap blocks were freed -- no leaks are possible
#Ứng dụng: trình thông dịch nhỏ
Union có nhãn là cấu trúc trung tâm của mọi trình phân tích cú pháp và trình thông dịch. Đây là một ví dụ rút gọn nhưng chạy được.
bieu-thuc.h
typedef enum {
BT_SO, /* một số */
BT_CONG, /* a + b */
BT_TRU, /* a - b */
BT_NHAN, /* a * b */
BT_CHIA, /* a / b */
BT_SO_LUONG,
} LoaiBT;
typedef struct BieuThuc BieuThuc;
struct BieuThuc {
LoaiBT loai;
union {
double so; /* khi loai là BT_SO */
struct { BieuThuc *trai, *phai; } nhi; /* khi là phép toán hai ngôi */
};
};bieu-thuc.c
#include <stdlib.h>
BieuThuc *bt_so(double v)
{
BieuThuc *b = calloc(1, sizeof *b);
if (b == NULL) return NULL;
b->loai = BT_SO;
b->so = v;
return b;
}
BieuThuc *bt_nhi(LoaiBT loai, BieuThuc *trai, BieuThuc *phai)
{
if (trai == NULL || phai == NULL) return NULL;
BieuThuc *b = calloc(1, sizeof *b);
if (b == NULL) return NULL;
b->loai = loai;
b->nhi.trai = trai;
b->nhi.phai = phai;
return b;
}
/* Hủy đệ quy: từ trong ra ngoài, đúng như Bài 14.9 */
void bt_huy(BieuThuc *b)
{
if (b == NULL) return;
switch (b->loai) {
case BT_SO:
break;
case BT_CONG:
case BT_TRU:
case BT_NHAN:
case BT_CHIA:
bt_huy(b->nhi.trai);
bt_huy(b->nhi.phai);
break;
case BT_SO_LUONG:
break;
}
free(b);
}
typedef enum { TINH_OK, TINH_CHIA_KHONG, TINH_LOI } MaTinh;
/* Tính giá trị, đệ quy theo cấu trúc cây */
MaTinh bt_tinh(const BieuThuc *b, double *ra)
{
if (b == NULL || ra == NULL) return TINH_LOI;
double t, p;
MaTinh m;
switch (b->loai) {
case BT_SO:
*ra = b->so;
return TINH_OK;
case BT_CONG:
case BT_TRU:
case BT_NHAN:
case BT_CHIA:
m = bt_tinh(b->nhi.trai, &t);
if (m != TINH_OK) return m;
m = bt_tinh(b->nhi.phai, &p);
if (m != TINH_OK) return m;
switch (b->loai) {
case BT_CONG: *ra = t + p; return TINH_OK;
case BT_TRU: *ra = t - p; return TINH_OK;
case BT_NHAN: *ra = t * p; return TINH_OK;
case BT_CHIA:
if (p == 0.0) return TINH_CHIA_KHONG;
*ra = t / p;
return TINH_OK;
case BT_SO:
case BT_SO_LUONG:
break;
}
break;
case BT_SO_LUONG:
break;
}
return TINH_LOI;
}main.c
#include <stdio.h>
int main(void)
{
/* Biểu thức: (3 + 4) * 2 */
BieuThuc *a = bt_so(3);
BieuThuc *b = bt_so(4);
BieuThuc *c = bt_so(2);
if (a == NULL || b == NULL || c == NULL) goto loi;
BieuThuc *cong = bt_nhi(BT_CONG, a, b);
if (cong == NULL) goto loi;
a = b = NULL; /* cong đã sở hữu a và b */
BieuThuc *goc = bt_nhi(BT_NHAN, cong, c);
if (goc == NULL) { bt_huy(cong); goto loi; }
cong = c = NULL; /* goc đã sở hữu cong và c */
double kq;
if (bt_tinh(goc, &kq) == TINH_OK)
printf("%.2f\n", kq); /* 14.00 */
bt_huy(goc); /* hủy đệ quy cả cây */
return 0;
loi:
bt_huy(a);
bt_huy(b);
bt_huy(c);
return 1;
}terminal
gcc -std=c17 -Wall -Wextra -g -fsanitize=address bieu-thuc.c main.c -o t && ./t
14.00
valgrind --leak-check=full ./t
All heap blocks were freed -- no leaks are possible
#So với con trỏ void
Bài 13.7 đã giới thiệu void * làm công cụ lưu dữ liệu bất kỳ. Union có nhãn giải quyết cùng bài toán theo cách khác.
| Tiêu chí | void sao cộng nhãn | Union có nhãn |
|---|---|---|
| Số kiểu lưu được | Bất kỳ, kể cả kiểu chưa biết lúc viết | Cố định, phải liệt kê trước |
| Số lần cấp phát | Thêm một lần cho dữ liệu | Không thêm, dữ liệu nằm ngay trong struct |
| Truy cập dữ liệu | Thêm một lần đọc bộ nhớ | Trực tiếp |
| Kiểm tra kiểu | Không có, phải ép kiểu | Trình biên dịch biết kiểu của từng thành viên |
| Kích thước | Bằng con trỏ, tức 8 byte | Bằng thành viên lớn nhất |
| Thêm kiểu mới | Không phải sửa gì trong cấu trúc | Phải sửa union và mọi switch |
| Trình biên dịch bắt thiếu nhánh | Không | Có, nhờ switch không default |
void sao: mất kiểm tra kiểu
typedef struct {
int loai;
void *du_lieu; /* phải cấp phát riêng, và phải ép kiểu khi dùng */
} GiaTri;
GiaTri g = { .loai = GT_INT, .du_lieu = malloc(sizeof(long)) };
*(long *)g.du_lieu = 42;
/* Ép nhầm kiểu thì không ai báo */
printf("%f\n", *(double *)g.du_lieu);Union có nhãn: giữ kiểm tra kiểu
typedef struct {
LoaiGiaTri loai;
union { long i; double d; char *s; };
} GiaTri;
GiaTri g = { .loai = GT_INT, .i = 42 };
/* Đọc sai thành viên vẫn có kiểu đúng, và hàm đọc kiểm tra nhãn */
long v;
if (gt_lay_int(&g, &v) == 0) printf("%ld\n", v);#Năm quy tắc
| Quy tắc | Vì sao | |
|---|---|---|
| 1 | Nhãn nằm ngoài union, không dựa vào tiền tố chung | Đơn giản, di động, ai đọc cũng hiểu |
| 2 | Nhãn có giá trị 0 là trạng thái rỗng hợp lệ | Để khởi tạo bằng { 0 } cho ra đối tượng dùng được |
| 3 | Một hàm tạo cho mỗi biến thể | Nhãn và dữ liệu không bao giờ lệch nhau |
| 4 | Mọi switch trên nhãn đều không có default | Thêm biến thể mới thì trình biên dịch chỉ ra mọi chỗ cần sửa |
| 5 | Hàm hủy và hàm sao chép xử lý riêng biến thể có con trỏ | Tránh rò rỉ và giải phóng hai lần |
mau-day-du.c
/* Mẫu tối giản nhưng đầy đủ, dùng lại được cho mọi bài toán biến thể. */
typedef enum { X_RONG = 0, X_A, X_B, X_SO_LUONG } LoaiX;
typedef struct {
LoaiX loai;
union {
int a;
char *b; /* SỞ HỮU */
};
} X;
X x_a(int v);
int x_b(const char *s, X *ra);
void x_huy(X *x);
int x_ban_sao(const X *goc, X *ra);
void x_in(const X *x);Khi nào không cần union
Dùng union khi các biến thể gần giống nhau
typedef struct {
Loai loai;
union {
struct { int x, y; } diem;
struct { int x, y, mau; } diem_mau;
};
} DoiTuong;
/* Hai biến thể chỉ khác một trường. Union chỉ tiết kiệm 4 byte
mà đổi lại toàn bộ độ phức tạp của nhãn. */Dùng struct thường
typedef struct {
int x, y;
int mau; /* 0 nghĩa là không có màu */
} DoiTuong;
/* Đơn giản hơn nhiều, và tiết kiệm 4 byte không đáng để đánh đổi. */Tự làm thử
- Cài đủ kiểu
GiaTrivới bốn biến thể và bộ sáu hàm trong bài, chạy dưới valgrind. - So
sizeofcủa union có nhãn với struct chứa mọi trường, tính mức tiết kiệm với một triệu bản sao. - Thử cả ba cách phá vỡ bất biến trong bài, quan sát hậu quả từng cái.
- Thêm biến thể
GT_BOOLvào enum, biên dịch lại và để trình biên dịch chỉ cho bạn mọi chỗ cần sửa. - Cài trình thông dịch biểu thức trong bài, thêm phép lũy thừa và phép đảo dấu một ngôi.
- Thêm hàm in biểu thức ra dạng chuỗi có ngoặc, dùng đệ quy theo cấu trúc cây.
- Viết lại kiểu
GiaTribằngvoid *, so sánh số dòng, số lần cấp phát và độ an toàn với bản dùng union.
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
- Union có nhãn gồm một enum cho biết thành viên nào hoạt động và một union chứa dữ liệu.
- Đặt nhãn ngoài union, cho nhãn rỗng giá trị 0, và viết một hàm tạo riêng cho mỗi biến thể.
- Mọi
switchtrên nhãn đều không códefault, để trình biên dịch chỉ ra mọi chỗ cần sửa khi thêm biến thể. - Biến thể chứa con trỏ cần xử lý riêng trong hàm hủy và hàm sao chép sâu.
- Chọn union có nhãn khi tập kiểu là cố định và nhỏ, chọn
void *khi tập kiểu mở cho người dùng thư viện tự thêm.