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ảng | Struct | |
|---|---|---|
| Kiểu các phần tử | Bắt buộc giống nhau | Có thể khác nhau |
| Truy cập bằng | Chỉ số, tính lúc chạy | Tên trường, cố định lúc biên dịch |
| Số phần tử | Có thể rất lớn | Thường vài tới vài chục |
| Gán bằng dấu bằng | Không được | Được |
| Truyền vào hàm | Phân rã thành con trỏ | Sao chép cả struct |
| Trả về từ hàm | Khô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ểm | Nhượ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 được | Dài hơn một chút |
| Bằng 0 | Bảo đảm không có trường nào chứa rác | Phả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ó.
| Quy tắc | Nộ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ả struct | Bằng căn chỉnh lớn nhất trong các trường |
| Kích thước của cả struct | Phải chia hết cho căn chỉnh của nó, nên có thể có đệm ở cuối |
| Thứ tự trường | Trì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ông | Ghi chú |
|---|---|---|
| a = b | Được | Sao chép từng byte, kể cả phần đệm |
| f(a) truyền theo giá trị | Được | Sao chép cả struct |
| return a | Được | Sao chép cả struct |
| &a | Được | Cho ra con trỏ tới struct |
| sizeof a | Được | Kể cả phần đệm |
| a == b | KHÔNG | Không có toán tử so sánh cho struct |
| a + b | KHÔNG | Không có phép toán số học nào |
| a < b | KHÔNG | Khô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ử
- 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.
- Đổ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.
- In
sizeofvàoffsetofcủa mọi trường trong một struct năm trường, vẽ bố cục ra giấy. - 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.
- Cài
packedlên cùng struct, so kích thước và đo thời gian đọc ghi một triệu lần. - So sánh hai struct bằng
memcmpvà bằng hàm viết tay, tạo trường hợp hai cách cho kết quả khác nhau. - Khai báo struct trường bit cho một thanh ghi tám bit, in
sizeofvà 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. sizeofcủ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
memcmpvì nó so cả phần đệm, hãy viết hàm so từng trường.