Bỏ qua điều hướng, tới nội dung chính
Học C
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.
Ba thành viên cùng bắt đầu ở byte 0. Ghi vào một thành viên là ghi đè lên mọi thành viên khác.

#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ắcNội dung
Kích thước tối thiểuBằng thành viên lớn nhất
Căn chỉnhBằng căn chỉnh lớn nhất trong các thành viên
Kích thước cuối cùngLà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ênGiố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ístructunion
Kích thướcTổng các trường cộng phần đệm, ở đây 12Bằng trường lớn nhất, ở đây 4
Số giá trị lưu được cùng lúcTất cảĐúng một
Địa chỉ các trườngKhác nhauGiống nhau
Ghi vào một trườngKhông ảnh hưởng trường khácPhá mọi trường khác
Khởi tạoĐặt được nhiều trườngChỉ đặt được một, mặc định là trường đầu
Dùng khiCần lưu nhiều thứ cùng lúcCầ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ốngAn toàn khôngGhi chú
Ghi i rồi đọc iTrường hợp bình thường
Ghi f rồi đọc mảng unsigned charCó trong CXem 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ướcHợp lệ trong C nhưng không di độngKế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ớnKhôngPhần dôi ra chưa từng được ghi, chứa rác
Đọc trước khi ghi bất cứ gìKhôngToà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ố chungNhãn ngoài union
Dựa vào ngoại lệ của chuẩnKhông
Kích thướcNhỏ hơn một chútLớn hơn, vì nhãn nằm riêng
Dễ hiểuKém, phải biết ngoại lệRất, ai đọc cũng hiểu
Dễ sai khi sửaCó, đổi thứ tự trường là hỏngKhông
Dùng ở đâuMã hệ thống cũ, ví dụ struct sockaddrMã mới

Tự làm thử

  1. 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.
  2. In sizeof_Alignof củ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.
  3. 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.
  4. So sánh sizeof của một struct và một union cùng ba trường, giải thích chênh lệch.
  5. Ghi thành viên nhỏ rồi đọc thành viên lớn, chạy dưới -fsanitize=memory nếu có Clang.
  6. 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.
  7. Viết lại cùng bài toán bằng cách đặt nhãn ngoài union, so sánh sizeof và độ 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.
  • sizeof bằ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.