Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 15.418 phút đọc

Struct lồng nhau

Sau bài này bạn sẽ làm được

  • Khai báo và truy cập struct lồng nhiều tầng
  • Dùng thành viên vô danh của C11
  • Hiểu phần đệm khi struct lồng nhau
  • Chọn giữa lồng trực tiếp và lồng qua con trỏ

Struct chứa struct cho phép bạn xây khái niệm phức tạp từ khái niệm đơn giản, đúng như cách bạn nghĩ về bài toán. Bài này cũng giới thiệu thành viên vô danh của C11, một tính năng ít người biết nhưng rất tiện.

#Struct chứa struct

long-nhau.c
typedef struct {
    int ngay, thang, nam;
} Ngay;

typedef struct {
    char so_nha[16];
    char duong[64];
    char thanh_pho[32];
} DiaChi;

typedef struct {
    char   ho_ten[64];
    Ngay   ngay_sinh;      /* struct lồng trong struct */
    DiaChi dia_chi;
} Nguoi;

Nguoi a;

a.ngay_sinh.nam   = 2000;
a.dia_chi.thanh_pho[0] = 'H';

printf("%d\n", a.ngay_sinh.nam);

Truy cập nhiều tầng chỉ là ghép nhiều dấu chấm. Không có giới hạn nào về số tầng, dù trong thực tế quá ba tầng là dấu hiệu nên thiết kế lại.

Biểu thứcKiểuNghĩa là
aNguoiCả người
a.ngay_sinhNgayCả ngày sinh, dùng như một struct
a.ngay_sinh.namintMột trường
&a.ngay_sinhNgay *Con trỏ tới struct con
p->ngay_sinh.namintKhi p là Nguoi sao

Khai báo struct con ngay bên trong

/* Cách này hợp lệ nhưng thường không nên */
typedef struct {
    char ho_ten[64];

    struct {
        int ngay, thang, nam;
    } ngay_sinh;            /* struct vô danh có tên trường */
} Nguoi;

Nguoi a;

a.ngay_sinh.nam = 2000;     /* dùng bình thường */

/* Nhưng bạn KHÔNG viết được hàm nhận riêng ngay_sinh,
   vì kiểu của nó không có tên. */

#Khởi tạo struct lồng nhau

khoi-tao.c
/* Cách 1: ngoặc lồng ngoặc, theo thứ tự */
Nguoi a = {
    "Nguyen Van A",
    { 1, 1, 2000 },
    { "12", "Le Loi", "Ha Noi" },
};

/* Cách 2: có chỉ định, nên dùng */
Nguoi b = {
    .ho_ten = "Tran Thi B",
    .ngay_sinh = { .ngay = 15, .thang = 6, .nam = 2001 },
    .dia_chi   = { .duong = "Tran Hung Dao", .thanh_pho = "Hue" },
};

/* Cách 3: chỉ định sâu, bỏ qua tầng giữa */
Nguoi c = {
    .ho_ten = "Le Van C",
    .ngay_sinh.nam = 2002,      /* các trường khác của ngay_sinh tự bằng 0 */
};

/* Cách 4: xóa sạch rồi gán từng phần */
Nguoi d = { 0 };

d.ngay_sinh = (Ngay){ 20, 12, 1999 };

#Thành viên vô danh của C11

Thành viên vô danh
Một struct hoặc union lồng bên trong mà không có tên trường. Các trường của nó được truy cập trực tiếp như thể chúng thuộc struct cha.
Có tên trường: phải viết đủ đường dẫn
typedef struct {
    struct { int x, y; } toa_do;
    int mau;
} Diem;

Diem p;

p.toa_do.x = 10;
p.toa_do.y = 20;
p.mau = 0xFF;
Vô danh: truy cập thẳng
typedef struct {
    struct { int x, y; };      /* KHÔNG có tên trường */
    int mau;
} Diem;

Diem p;

p.x   = 10;      /* truy cập thẳng, như thể x là trường của Diem */
p.y   = 20;
p.mau = 0xFF;

Ứng dụng: mô phỏng kế thừa

ke-thua.c
typedef struct {
    int  loai;
    char ten[32];
} Co_So;

typedef struct {
    Co_So;              /* thành viên vô danh: mọi trường của Co_So thành của Hinh */
    double dien_tich;
} Hinh;

Hinh h;

h.loai = 1;             /* trường của Co_So, truy cập thẳng */
snprintf(h.ten, sizeof h.ten, "hinh tron");
h.dien_tich = 12.57;

Ứng dụng thứ hai: union vô danh

union-vo-danh.c
typedef enum { KIEU_INT, KIEU_DOUBLE, KIEU_CHUOI } Kieu;

typedef struct {
    Kieu kieu;

    union {                    /* union vô danh, đúng chuẩn C11 */
        int    i;
        double d;
        char  *s;
    };
} GiaTri;

GiaTri a = { .kieu = KIEU_INT, .i = 42 };

if (a.kieu == KIEU_INT)
    printf("%d\n", a.i);      /* không cần viết a.u.i */

Đây là mẫu union có nhãn, và Bài 17.2 sẽ nói kỹ về nó. Thành viên vô danh làm mã gọn hơn hẳn so với việc phải đặt tên cho union rồi viết thêm một tầng ở mọi chỗ dùng.

#Bố cục bộ nhớ khi lồng

bo-cuc.c
#include <stddef.h>
#include <stdio.h>

typedef struct { char c; int i; } Trong;      /* 8 byte, 3 byte đệm */

typedef struct {
    char  d;      /* 1 byte */
    Trong t;      /* 8 byte, cần căn chỉnh 4 */
    char  e;      /* 1 byte */
} Ngoai;

int main(void)
{
    printf("sizeof Trong = %zu\n", sizeof(Trong));
    printf("sizeof Ngoai = %zu\n", sizeof(Ngoai));
    printf("d tai %zu\n", offsetof(Ngoai, d));
    printf("t tai %zu\n", offsetof(Ngoai, t));
    printf("e tai %zu\n", offsetof(Ngoai, e));

    return 0;
}
terminal
./bo-cuc
sizeof Trong = 8
sizeof Ngoai = 16
d tai 0
t tai 4
e tai 12
ByteNội dungGhi chú
0dTrường đầu tiên
1 tới 3đệmVì Trong cần căn chỉnh 4
4t.cTrường đầu của struct con
5 tới 7đệmĐệm nội bộ của Trong
8 tới 11t.iTrường thứ hai của struct con
12eTrường cuối
13 tới 15đệmĐể cả Ngoai chia hết cho 4
Lồng làm phình bộ nhớ
typedef struct { char c; int i; } Trong;    /* 8 byte */

typedef struct {
    char  d;
    Trong t;
    char  e;
} Ngoai;                                    /* 16 byte cho 7 byte dữ liệu */
Làm phẳng khi cần tiết kiệm
typedef struct {
    int  i;      /* gom mọi trường vào một tầng và sắp từ lớn xuống nhỏ */
    char c, d, e;
} Phang;                                    /* 8 byte */

#Lồng trực tiếp hay lồng qua con trỏ

/* Lồng trực tiếp: struct con nằm ngay trong struct cha */
typedef struct {
    char ho_ten[64];
    Ngay ngay_sinh;
} A;

/* Lồng qua con trỏ: struct cha chỉ giữ địa chỉ */
typedef struct {
    char  ho_ten[64];
    Ngay *ngay_sinh;
} B;
Lồng trực tiếpLồng qua con trỏ
Kích thước struct chaCộng cả struct conChỉ thêm 8 byte
Số lần cấp phátMộtHai, và phải giải phóng cả hai
Truy cậpMột phép cộng độ lệchThêm một lần đọc bộ nhớ
Thân thiện bộ nhớ đệmRất, dữ liệu nằm liềnKém, hai vùng rời nhau
Chia sẻ được với struct khácKhông
Cho phép trường vắng mặtKhôngCó, dùng NULL
Tự tham chiếuKhông được, sẽ vô hạnĐược

Bốn câu hỏi để chọn

  • Struct con có luôn tồn tại không? Nếu có thì lồng trực tiếp. Nếu có lúc vắng mặt thì dùng con trỏ và NULL.
  • Có ai khác dùng chung struct con không? Nếu có thì phải dùng con trỏ.
  • Struct con có lớn không? Nếu rất lớn và hiếm khi truy cập, dùng con trỏ để struct cha gọn.
  • Có tự tham chiếu không? Nếu có thì bắt buộc dùng con trỏ.

Ví dụ tổng hợp

tong-hop.c
typedef struct { int ngay, thang, nam; } Ngay;

typedef struct {
    char so_nha[16];
    char duong[64];
} DiaChi;

typedef struct SinhVien {
    /* Lồng trực tiếp: luôn có, nhỏ, không chia sẻ */
    char   ho_ten[64];
    Ngay   ngay_sinh;

    /* Lồng qua con trỏ: có thể vắng mặt */
    DiaChi *dia_chi_tam_tru;      /* NULL nếu không có */

    /* Con trỏ vì tự tham chiếu */
    struct SinhVien *nguoi_bao_ho;

    /* Mảng động: số lượng không biết trước */
    double *diem;
    size_t  so_diem;
} SinhVien;

Bốn kiểu quan hệ trong một struct, mỗi kiểu chọn theo đúng câu hỏi ở trên. Bài 15.7 sẽ viết cặp hàm tạo và hủy đầy đủ cho struct này.

Tự làm thử

  1. Khai báo struct ba tầng lồng nhau, truy cập trường sâu nhất và in ra.
  2. Khởi tạo struct lồng bằng cả bốn cách trong bài, so sánh độ dễ đọc.
  3. Bỏ ngoặc trong danh sách khởi tạo, biên dịch với -Wall và đọc cảnh báo.
  4. Dùng thành viên vô danh của C11 cho một union bên trong struct, so với bản có tên.
  5. In sizeofoffsetof mọi trường của struct lồng, vẽ bố cục ra giấy rồi đối chiếu.
  6. Làm phẳng struct đó và đo mức tiết kiệm bộ nhớ với một triệu bản sao.
  7. Thử khai báo struct chứa chính nó trực tiếp, chép lại thông báo lỗi rồi sửa bằng con trỏ.

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

  • Struct lồng nhau truy cập bằng nhiều dấu chấm, và struct con dùng được như một giá trị độc lập.
  • Khởi tạo có chỉ định hoạt động cả với đường dẫn sâu như .a.b = 1, và trường không nhắc tới tự bằng 0.
  • Thành viên vô danh của C11 làm trường của struct con trở thành trường của struct cha, rất tiện với union có nhãn.
  • Struct con là một khối liền và mang theo cả phần đệm nội bộ, nên lồng nhiều tầng có thể lãng phí đáng kể.
  • Mặc định lồng trực tiếp. Chỉ dùng con trỏ khi struct con có thể vắng mặt, được chia sẻ, quá lớn, hoặc khi cần tự tham chiếu.