Mảng chứa nhiều giá trị cùng một kiểu. struct thì ngược lại: nó chứa vài giá trị thuộc nhiều kiểu khác nhau và coi chúng là một thứ duy nhất. Một điểm gồm x và y. Một nhân viên gồm tên, mã số và mức lương. Không có struct, bạn sẽ phải mang theo ba biến rời rạc và cầu mong không bao giờ nhầm lương của người này với người kia.
Khai báo kiểu struct
Khai báo struct đặt tên cho một tag và liệt kê các thành viên trong cặp ngoặc nhọn:
struct Point {
int x;
int y;
};
Dấu chấm phẩy sau ngoặc nhọn đóng là bắt buộc. Quên nó sẽ gây ra một trong những thông báo lỗi khó hiểu nhất mà người mới học C gặp phải, vì trình biên dịch lại báo lỗi ở dòng tiếp theo.
Câu lệnh này khai báo một kiểu, không phải một biến. Trong C, tên đầy đủ của kiểu là struct Point, có cả từ khóa:
Thành viên được đọc và ghi bằng toán tử chấm, p.x. Biến struct là một biến bình thường: nó nằm trên stack, có thể gán cho một struct khác cùng kiểu bằng =, và biến mất khi kết thúc phạm vi (scope) của nó.
Viết struct Point ở khắp nơi rất mệt, đó là lý do gần như mọi code C thực tế bọc phần khai báo trong một typedef để kiểu chỉ còn là Point.
Khởi tạo struct
Bạn có thể gán giá trị cho struct ngay lúc khai báo. Dạng theo vị trí liệt kê giá trị theo thứ tự khai báo:
struct Point p = {3, 4}; // x = 3, y = 4
Designated initializer thì ghi tên từng thành viên, rõ ràng và an toàn hơn:
Ba điều đáng chú ý:
- Dạng designated có thể liệt kê thành viên theo bất kỳ thứ tự nào, nên việc thêm hay sắp xếp lại thành viên về sau không âm thầm đẩy giá trị vào sai chỗ.
- Mọi thành viên bạn bỏ qua đều được khởi tạo bằng 0:
{0}là cách chuẩn để đặt toàn bộ struct về 0. - Thành viên
char name[32]là một mảng thật nằm trong struct, nên chuỗi được lưu ngay trong struct chứ không ở chỗ khác. Điều đó cũng có nghĩa là viếta.name = "Ada";sau khi khai báo là không hợp lệ; bạn cầnstrcpy(xem hàm xử lý chuỗi).
Một struct không có phần khởi tạo thì chưa được khởi tạo, và đọc thành viên của nó trước khi ghi là undefined behavior. = {0} không tốn gì mà loại bỏ hẳn cả nhóm bug này.
Gán và so sánh
Phép gán struct sao chép mọi thành viên:
a không bị đụng tới: b có bản sao riêng. Nhưng hãy để ý điều C không cung cấp: không có == cho struct. if (a == b) sẽ không biên dịch được. So sánh nghĩa là so sánh từng thành viên:
if (a.x == b.x && a.y == b.y) { /* equal */ }
Đừng dùng memcmp như một lối tắt. Trình biên dịch chèn các byte đệm (padding) vô hình giữa các thành viên để căn chỉnh, và các byte đó có thể chứa rác, nên hai struct có thành viên giống hệt nhau vẫn có thể khác nhau khi so từng byte.
Mảng struct
Vì struct là một kiểu như mọi kiểu khác, bạn có thể tạo mảng struct: cách chuẩn để lưu một bảng bản ghi.
staff[i].salary được đọc là "phần tử i, rồi đến thành viên salary của nó": phép lấy chỉ số có độ ưu tiên cao hơn dấu chấm, nên không cần ngoặc.
Struct bị sao chép khi truyền vào hàm
Đây là quy tắc khiến nhiều người mắc lỗi. Khi bạn truyền một struct vào hàm, C sao chép nó. Hàm làm việc trên bản sao riêng của nó:
p vẫn là (3, 4). Đọc một struct theo giá trị như distance_from_origin hoàn toàn ổn, và thường là lựa chọn rõ ràng nhất với struct nhỏ. Nhưng nếu hàm phải thay đổi struct của nơi gọi, hoặc nếu struct đủ lớn để việc sao chép gây lãng phí, hãy truyền con trỏ. Đó là chủ đề của trang struct và con trỏ.
Trả về struct theo giá trị cũng hoạt động như vậy và hoàn toàn hợp lệ:
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
Struct lồng nhau
Thành viên của struct có thể chính là một struct. Nối các dấu chấm để đi vào bên trong:
b.published.year đi vào từng cấp một. Lồng struct là cách bạn xây dựng mô hình dữ liệu thực tế trong C, và struct bên trong được lưu bên trong struct bên ngoài chứ không phải được trỏ tới, nên toàn bộ Book là một khối bộ nhớ liền mạch.
Kích thước, padding và thứ tự thành viên
sizeof cho biết struct lớn bao nhiêu, và kết quả thường lớn hơn tổng kích thước các thành viên:
Trên một máy thông thường, Wasteful chiếm 12 byte còn Tidy chiếm 8, dù cả hai chứa cùng ba thành viên. Trình biên dịch chèn padding để mỗi thành viên nằm ở địa chỉ phù hợp với kiểu của nó: một int thường cần địa chỉ là bội số của 4. Đặt các thành viên lớn trước thành viên nhỏ thường giúp xếp gọn hơn.
Điều này hiếm khi quan trọng (một mảng một triệu phần tử, cấu trúc của một gói tin mạng) và không bao giờ ảnh hưởng đến tính đúng đắn của code bạn viết. Nhưng nó giải thích vì sao sizeof làm bạn bất ngờ, và vì sao dùng memcmp trên struct là một ý tồi.
Câu hỏi thường gặp
Khai báo struct trong C như thế nào?
Viết struct, theo sau là tên tag và danh sách thành viên trong cặp ngoặc nhọn: struct Point { int x; int y; };. Câu lệnh này khai báo một kiểu, không phải một biến. Để tạo biến, bạn viết tiếp struct Point p;: từ struct là một phần của tên kiểu trong C, đó là lý do hầu hết code đi kèm với một typedef.
. và -> khác nhau thế nào khi dùng với struct?
Dùng . khi bạn có chính struct đó (p.x) và -> khi bạn có con trỏ tới nó (ptr->x). Mũi tên là cách viết tắt của (*ptr).x. Xem chi tiết ở struct và con trỏ.
Struct trong C được truyền theo giá trị hay theo tham chiếu?
Luôn theo giá trị. Truyền một struct vào hàm sẽ sao chép mọi thành viên, nên thay đổi bên trong hàm không ảnh hưởng tới biến của nơi gọi. Để sửa bản gốc, hoặc để tránh sao chép một struct lớn, hãy truyền con trỏ.
Khởi tạo struct trong C như thế nào?
Theo vị trí, struct Point p = {3, 4};, hoặc bằng designated initializer, struct Point p = {.y = 4, .x = 3};. Dạng designated ghi tên từng thành viên, nên vẫn đúng khi sau này ai đó sắp xếp lại struct, và các thành viên không được nêu tên sẽ được đặt bằng 0.