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

Struct cơ bản

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

  • Khai báo và khởi tạo struct bằng ba cách
  • Giải thích vì sao sizeof của struct lớn hơn tổng các trường
  • Tính được phần đệm và sắp xếp trường để tiết kiệm bộ nhớ
  • Dùng offsetof để xem bố cục thật

Tới giờ bạn gom dữ liệu cùng kiểu bằng mảng. Struct cho bạn gom dữ liệu khác kiểu thành một khối có tên. Đây là bước cuối cùng để bạn tự định nghĩa được kiểu dữ liệu của riêng mình, và là nền tảng cho mọi cấu trúc dữ liệu ở Phần 9.

#Vì sao cần struct

Nhiều mảng song song
char   ten[100][64];
int    tuoi[100];
double diem[100];
size_t n = 0;

/* Ba mảng phải luôn cùng độ dài và cùng thứ tự.
   Xóa một sinh viên là phải dồn cả ba mảng.
   Truyền một sinh viên vào hàm là phải truyền ba tham số. */
void in_sv(const char *ten, int tuoi, double diem);
Một mảng struct
typedef struct {
    char   ten[64];
    int    tuoi;
    double diem;
} SinhVien;

SinhVien ds[100];
size_t   n = 0;

/* Một mảng, một khái niệm. Xóa là dồn một mảng.
   Truyền một sinh viên là truyền một thứ. */
void in_sv(const SinhVien *sv);
Struct
Một kiểu dữ liệu do bạn định nghĩa, gom nhiều giá trị có thể khác kiểu thành một đơn vị duy nhất. Mỗi giá trị bên trong gọi là một trường và có tên riêng.
MảngStruct
Kiểu các phần tửBắt buộc giống nhauCó thể khác nhau
Truy cập bằngChỉ số, tính lúc chạyTên trường, cố định lúc biên dịch
Số phần tửCó thể rất lớnThường vài tới vài chục
Gán bằng dấu bằngKhông đượcĐược
Truyền vào hàmPhân rã thành con trỏSao chép cả struct
Trả về từ hàmKhông đượcĐược

#Khai báo và truy cập

khai-bao.c
/* Cách 1: khai báo kiểu rồi khai báo biến riêng */
struct SinhVien {
    char   ten[64];
    int    tuoi;
    double diem;
};

struct SinhVien a;          /* phải viết đủ chữ struct */

/* Cách 2: khai báo kiểu và biến cùng lúc */
struct Diem {
    int x, y;
} goc, dich;

/* Cách 3: struct vô danh, chỉ tạo biến */
struct {
    int rong, cao;
} cua_so;

/* Cách 4: typedef, cách phổ biến nhất, xem Bài 15.2 */
typedef struct {
    int x, y;
} Diem;

Diem p;                     /* không cần chữ struct */
struct SinhVien a;

strcpy(a.ten, "Nguyen Van A");
a.tuoi = 20;
a.diem = 8.5;

printf("%s %d %.1f\n", a.ten, a.tuoi, a.diem);

Tên thẻ và không gian tên riêng

struct Diem { int x, y; };

int Diem = 5;               /* HỢP LỆ: tên thẻ struct nằm ở không gian tên riêng */

struct Diem p;              /* vẫn dùng được kiểu */
printf("%d\n", Diem);      /* và vẫn dùng được biến */

#Ba cách khởi tạo

khoi-tao.c
typedef struct {
    char   ten[64];
    int    tuoi;
    double diem;
} SinhVien;

/* Cách 1: theo thứ tự khai báo */
SinhVien a = { "An", 20, 8.5 };

/* Cách 2: khởi tạo có chỉ định, C99. Đây là cách nên dùng. */
SinhVien b = {
    .ten  = "Binh",
    .tuoi = 21,
    .diem = 9.0,
};

/* Cách 3: xóa sạch về 0 */
SinhVien c = { 0 };

/* Chỉ định một phần, trường không nhắc tới tự bằng 0 */
SinhVien d = { .tuoi = 22 };      /* ten rỗng, diem bằng 0.0 */
CáchƯu điểmNhược điểm
Theo thứ tựNgắn gọn với struct hai ba trườngĐổi thứ tự khai báo là mọi chỗ khởi tạo sai im lặng
Có chỉ địnhĐọc là hiểu, không phụ thuộc thứ tự, bỏ trường nào cũng đượcDài hơn một chút
Bằng 0Bảo đảm không có trường nào chứa rácPhải gán từng trường sau đó

Gán lại cả struct sau khi khai báo

SinhVien a;

a = (SinhVien){ .ten = "An", .tuoi = 20, .diem = 8.5 };   /* hằng phức hợp, C99 */

/* Xóa sạch một struct đã có */
a = (SinhVien){ 0 };

/* Cách cũ, vẫn dùng được */
memset(&a, 0, sizeof a);

#Phần đệm và căn chỉnh

padding.c
#include <stdio.h>

struct A {
    char   c;      /* 1 byte */
    int    i;      /* 4 byte */
    short  s;      /* 2 byte */
};

int main(void)
{
    printf("tong cac truong: %zu\n", sizeof(char) + sizeof(int) + sizeof(short));
    printf("sizeof struct A: %zu\n", sizeof(struct A));

    return 0;
}
terminal
./padding
tong cac truong: 7
sizeof struct A: 12
Phần đệm
Các byte trống mà trình biên dịch chèn giữa các trường để mỗi trường bắt đầu ở một địa chỉ phù hợp với kiểu của nó.
Cùng ba trường, hai thứ tự khai báo cho ra hai kích thước khác nhau. Ô nét đứt là phần đệm.
Quy tắcNội dung
Căn chỉnh của một trườngĐịa chỉ của nó phải chia hết cho yêu cầu căn chỉnh của kiểu, thường bằng kích thước kiểu
Căn chỉnh của cả structBằng căn chỉnh lớn nhất trong các trường
Kích thước của cả structPhải chia hết cho căn chỉnh của nó, nên có thể có đệm ở cuối
Thứ tự trườngTrình biên dịch KHÔNG được sắp xếp lại, phải giữ đúng thứ tự bạn viết

Xem bố cục thật bằng offsetof

offsetof.c
#include <stddef.h>
#include <stdio.h>

struct A { char c; int i; short s; };

int main(void)
{
    printf("c  tai byte %zu\n", offsetof(struct A, c));
    printf("i  tai byte %zu\n", offsetof(struct A, i));
    printf("s  tai byte %zu\n", offsetof(struct A, s));
    printf("tong cong   %zu byte\n", sizeof(struct A));
    printf("can chinh   %zu byte\n", _Alignof(struct A));

    return 0;
}
terminal
./offsetof
c  tai byte 0
i  tai byte 4
s  tai byte 8
tong cong   12 byte
can chinh   4 byte
terminal
# Cách khác: nhờ trình biên dịch in bố cục ra
gcc -fdump-lang-class padding.c 2>/dev/null || pahole ./padding
struct A {
        char     c;      /*  0  1 */
        /* XXX 3 bytes hole, try to pack */
        int      i;      /*  4  4 */
        short    s;      /*  8  2 */
        /* size: 12, cachelines: 1, members: 3 */
        /* sum members: 7, holes: 1, sum holes: 3 */
        /* padding: 2 */
};

#Sắp xếp trường để tiết kiệm

Thứ tự tùy ý
struct Xau {
    char   c;      /* byte 0, rồi 3 byte đệm */
    int    i;      /* byte 4 tới 7 */
    char   d;      /* byte 8, rồi 7 byte đệm vì x cần địa chỉ chia hết cho 8 */
    double x;      /* byte 16 tới 23 */
    short  s;      /* byte 24 tới 25, rồi 6 byte đệm ở cuối */
};

/* sizeof bằng 32, trong đó 16 byte là đệm */
Trường lớn trước, nhỏ sau
struct Tot {
    double x;      /* 8 byte */
    int    i;      /* 4 byte */
    short  s;      /* 2 byte */
    char   c;      /* 1 byte */
    char   d;      /* 1 byte, vừa khít, không cần đệm */
};

/* sizeof bằng 16, không có byte đệm nào */
terminal
./so-sanh
sizeof Xau = 32
sizeof Tot = 16
tiet kiem 50 phan tram

Ép bỏ phần đệm và vì sao thường không nên

/* Phần mở rộng của GCC và Clang, không thuộc chuẩn C */
struct __attribute__((packed)) Goi {
    unsigned char  loai;
    unsigned int   do_dai;
    unsigned short ma;
};

/* sizeof bằng 7 thay vì 12, không có đệm nào */

#Phép toán được và không được

PhépĐược khôngGhi chú
a = bĐượcSao chép từng byte, kể cả phần đệm
f(a) truyền theo giá trịĐượcSao chép cả struct
return aĐượcSao chép cả struct
&aĐượcCho ra con trỏ tới struct
sizeof aĐượcKể cả phần đệm
a == bKHÔNGKhông có toán tử so sánh cho struct
a + bKHÔNGKhông có phép toán số học nào
a < bKHÔNGKhông có thứ tự

Trường bit

truong-bit.c
/* Ghi rõ mỗi trường chiếm bao nhiêu BIT thay vì bao nhiêu byte. */
struct Co {
    unsigned int bat     : 1;      /* 1 bit */
    unsigned int che_do  : 3;      /* 3 bit, giá trị 0 tới 7 */
    unsigned int uu_tien : 4;      /* 4 bit, giá trị 0 tới 15 */
    unsigned int         : 0;      /* buộc trường sau bắt đầu ở đơn vị mới */
    unsigned int ma      : 8;
};

printf("%zu\n", sizeof(struct Co));      /* 8 trên phần lớn trình biên dịch */

Tự làm thử

  1. Khai báo struct sinh viên ba trường, khởi tạo bằng cả ba cách và in nội dung.
  2. Đổi thứ tự khai báo hai trường rồi chạy lại chương trình dùng khởi tạo theo thứ tự, quan sát kết quả sai.
  3. In sizeofoffsetof của mọi trường trong một struct năm trường, vẽ bố cục ra giấy.
  4. Sắp xếp lại thứ tự trường của struct đó để giảm kích thước, đo mức tiết kiệm.
  5. Cài packed lên cùng struct, so kích thước và đo thời gian đọc ghi một triệu lần.
  6. So sánh hai struct bằng memcmp và bằng hàm viết tay, tạo trường hợp hai cách cho kết quả khác nhau.
  7. Khai báo struct trường bit cho một thanh ghi tám bit, in sizeof và thử lấy địa chỉ một trường.

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 gom nhiều giá trị khác kiểu thành một đơn vị. Khác mảng, nó gán được, truyền theo giá trị được và trả về được.
  • Luôn dùng khởi tạo có chỉ định .ten = ..., vì nó không phụ thuộc thứ tự khai báo.
  • sizeof của struct thường lớn hơn tổng các trường, do phần đệm để mỗi trường được căn chỉnh đúng.
  • Khai báo trường từ lớn xuống nhỏ gần như luôn cho bố cục tối ưu, nhưng chỉ đáng làm khi có nhiều bản sao.
  • Struct không có toán tử so sánh. Đừng dùng memcmp vì nó so cả phần đệm, hãy viết hàm so từng trường.