[Video] Tìm hiểu Struct trong C | Khóa học lập trình C
Mở bài
Khi bạn bắt đầu đi sâu vào lập trình C, một trong những khái niệm quan trọng không thể bỏ qua là struct — hay còn gọi là cấu trúc. Struct giúp lập trình viên gom nhóm các biến có liên quan (cùng hoặc khác kiểu dữ liệu) thành một đơn vị hợp lý, từ đó tổ chức dữ liệu theo hướng mô tả thực tế (ví dụ: thông tin sinh viên gồm tên, tuổi, điểm). Việc nắm vững Struct trong C không chỉ giúp mã nguồn rõ ràng, dễ bảo trì mà còn là nền tảng để hiểu các cấu trúc dữ liệu phức tạp hơn như danh sách liên kết, cây, hay các mô hình dữ liệu trong ứng dụng thực tế.
Bài viết này được thiết kế như một chương trong khóa học lập trình C, cung cấp giải thích từ cơ bản đến nâng cao: cú pháp khai báo, khởi tạo, mảng struct, con trỏ tới struct, truyền struct vào hàm, struct lồng nhau (nested struct), vấn đề can lệch bộ nhớ (padding & alignment), và các ví dụ thực hành thực tế. Mỗi phần có ví dụ mã minh họa rõ ràng và lời khuyên thực tế để bạn có thể áp dụng ngay trong dự án của mình. Hãy cùng khám phá cách dùng struct để biến dữ liệu rời rạc thành các thực thể có ý nghĩa!
Struct là gì và tại sao nên dùng struct trong C
Struct (cấu trúc) là một kiểu dữ liệu do người dùng định nghĩa, cho phép gom các biến liên quan thành một nhóm có tên. Mỗi thành phần trong struct được gọi là field hoặc member. Ví dụ, thay vì khai báo rời rạc char name[50]; int age; float gpa; cho một sinh viên, ta có thể gói gọn thành một biến kiểu struct Student gồm các thành phần trên.
Lợi ích chính của struct:
-
Tổ chức dữ liệu: Struct biểu diễn đối tượng thực tế (entity) rõ ràng hơn, giúp mã dễ đọc và dễ hiểu.
-
Tái sử dụng: Với typedef và struct, bạn có thể tái sử dụng mẫu dữ liệu trong nhiều hàm/module.
-
Thích hợp cho API: Dùng struct làm tham số/hàm trả về giúp truyền nhiều thông tin một cách gọn gàng.
-
Chuẩn bị cho cấu trúc dữ liệu nâng cao: Linked list, stack, tree thường dựa trên struct có chứa con trỏ.
So sánh ngắn: struct khác array ở chỗ array chứa nhiều phần tử cùng kiểu, còn struct chứa các phần tử có thể khác kiểu (string, số, ...). Khi thiết kế chương trình, nếu dữ liệu có nhiều thuộc tính, struct là lựa chọn hợp lý.
Cú pháp khai báo struct và kiểu dùng typedef
Cú pháp cơ bản để định nghĩa một struct:
Sau khi định nghĩa, bạn có thể khai báo biến kiểu này bằng:
Để rút gọn tên kiểu, thường dùng typedef:
Khi dùng typedef, bạn có thể khai báo trực tiếp:
Một số lưu ý:
-
Tên struct (
Student) và tên typedef có thể trùng hoặc khác nhau tuỳ mục đích. -
Có thể định nghĩa struct và khai báo biến ngay lập tức:
Khi thiết kế API cho module, nên đặt định nghĩa struct trong file header .h để nhiều file cùng sử dụng. Nếu struct chỉ dùng nội bộ module, thêm static hoặc giữ định nghĩa trong file .c để ẩn (encapsulation nhẹ).
Khởi tạo struct: các cách gán và khởi tạo ban đầu
Có nhiều cách để khởi tạo struct trong C:
-
Gán từng thành phần
Lưu ý cần
<string.h>chostrcpy. -
Khởi tạo danh sách (aggregate initialization)
Thành phần được gán theo thứ tự khai báo trong struct.
-
Khởi tạo một phần (designated initializer - C99)
Thuộc tính không gán sẽ được khởi tạo về 0.
-
Gán struct trực tiếp
Struct có thể gán trực tiếp; mọi thành phần được sao chép.
Những phương pháp này giúp linh hoạt khi bạn cần tạo dữ liệu mẫu, khởi tạo mặc định, hoặc sao chép nhanh. Lưu ý rằng khi struct chứa mảng (ví dụ char name[50]), sao chép struct sẽ bao gồm toàn bộ nội dung mảng.
Mảng struct và thao tác trên tập hợp đối tượng
Rất thường dùng struct kết hợp mảng để lưu danh sách bản ghi, ví dụ danh sách sinh viên:
Một số thao tác cơ bản:
-
Thêm: gán giá trị cho
students[n++]. -
Tìm kiếm: duyệt mảng và so sánh tên hoặc mã.
-
Sắp xếp: dùng
qsortvới hàm so sánh dựa trên field trong struct. -
In danh sách: duyệt và in từng trường.
Ví dụ sắp xếp theo điểm gpa dùng qsort:
Khi dùng mảng struct, lưu ý dung lượng cố định; nếu cần động, hãy kết hợp pointer và malloc/realloc.
Con trỏ tới struct và toán tử truy cập ->, .: cách dùng và ví dụ
Con trỏ tới struct thường được dùng khi truyền vào hàm hoặc khi dùng bộ nhớ động:
-
Dùng dấu
.để truy cập field khi bạn có một biến struct: -
Dùng
->khi bạn có con trỏ tới struct:
Sử dụng con trỏ struct hữu ích khi:
-
Truyền tham số vào hàm để hàm sửa nội dung struct (pass-by-reference).
-
Cấp phát động cho struct:
Khi dùng malloc, hãy luôn kiểm tra NULL và free sau khi sử dụng để tránh rò rỉ bộ nhớ.
Truyền struct vào hàm: truyền theo giá trị và theo con trỏ
Bạn có thể truyền struct vào hàm theo hai cách:
-
Truyền theo giá trị (by value)
Hàm nhận một bản sao của struct:Thay đổi
strong hàm không ảnh hưởng đến biến gốc. -
Truyền theo con trỏ (by reference)
Truyền địa chỉ để hàm có thể cập nhật dữ liệu:Gọi:
update_gpa(&students[i], 3.8);
Ưu/nhược điểm:
-
Truyền by value đơn giản, an toàn nhưng tốn chi phí sao chép nếu struct lớn.
-
Truyền by reference tiết kiệm bộ nhớ và cho phép sửa nội dung; cần kiểm tra con trỏ NULL.
Khi thiết kế API, ghi rõ ai sở hữu dữ liệu và ai chịu trách nhiệm giải phóng bộ nhớ (khi dùng malloc).
Nested struct (struct lồng nhau) và union: mở rộng cấu trúc dữ liệu
Nested struct (struct chứa struct khác) hữu ích khi một đối tượng phức tạp có thành phần là một đối tượng con:
Bạn truy cập như:
Hoặc nếu dùng con trỏ: p->dob.month = 5;
Ngoài ra, union là kiểu đặc biệt cho phép các field chia sẻ vùng nhớ (tiết kiệm nhưng giới hạn). Thường dùng union kèm struct để tạo cấu trúc dữ liệu linh hoạt (ví dụ variant types), nhưng lưu ý vấn đề an toàn kiểu và đọc trường đúng hiện trạng.
Memory layout, padding và sizeof struct: hiểu để tối ưu
Khi làm việc với struct, bạn cần hiểu cách bộ nhớ bố trí: compiler thường chèn padding giữa các field để đảm bảo các trường được căn chỉnh (alignment) phù hợp với kiến trúc CPU. Padding làm sizeof(struct) có thể lớn hơn tổng kích thước từng field.
Ví dụ:
Kích thước có thể là 8 bytes vì 3 byte padding sau char.
Mẹo tối ưu:
-
Sắp xếp các field từ lớn đến nhỏ (int, short, char) để giảm padding.
-
Dùng
#pragma packhoặc attribute packed (cẩn trọng) nếu cần layout chặt cho giao tiếp nhị phân, nhưng có thể làm chậm truy cập do misalignment.
Luôn dùng sizeof để biết kích thước thực tế khi cấp phát động:
Lỗi thường gặp và cách phòng tránh khi dùng struct
Một số lỗi phổ biến:
-
Sao chép chuỗi không kiểm soát:
Giải pháp: dùng
strncpyhoặc kiểm tra độ dài. -
Trả về địa chỉ biến cục bộ:
Trả về vùng heap hoặc để caller cấp phát.
-
Quên free khi
malloc→ memory leak. -
Truy cập ngoài phạm vi mảng trong struct → buffer overflow.
-
Không kiểm tra con trỏ NULL trước khi dereference.
Nguyên tắc: viết code an toàn, kiểm tra biên (boundary checking), và document rõ trách nhiệm về bộ nhớ.
Ví dụ thực tế hoàn chỉnh: quản lý danh sách sinh viên nhỏ
Một chương trình ngắn minh họa các thao tác cơ bản: thêm, in, tìm kiếm, sắp xếp:
Đây là mẫu cơ bản có thể mở rộng: dùng mảng động, input từ người dùng, ghi/đọc file, v.v.
Kết luận
Struct là công cụ mạnh trong ngôn ngữ C giúp mô hình hoá và tổ chức dữ liệu một cách trực quan và hiệu quả. Biết cách khai báo, khởi tạo, dùng mảng struct, con trỏ tới struct, truyền struct vào hàm, cũng như hiểu về memory layout và padding, sẽ giúp bạn thiết kế chương trình chắc chắn hơn, tối ưu tài nguyên và dễ bảo trì. Trong khuôn khổ khóa học lập trình C, struct là bước chuyển tiếp quan trọng từ biến đơn giản sang cấu trúc dữ liệu phức tạp — mở đường cho việc học con trỏ nâng cao, danh sách liên kết, cây, và các thuật toán xử lý dữ liệu lớn.
Nếu bạn đang học hoặc dạy lập trình C, hãy thực hành đa dạng bài tập: từ lưu danh sách học sinh, quản lý sản phẩm, tới xây module nhóm dùng header và source file. Việc kết hợp lý thuyết với thực hành sẽ giúp bạn nắm vững Struct trong C và sẵn sàng cho các chủ đề nâng cao tiếp theo. Nếu bạn muốn, mình có thể soạn ngay một bộ bài tập kèm lời giải chi tiết về struct — bạn muốn dạng bài nào: cơ bản (khai báo, khởi tạo), trung cấp (mảng struct, sắp xếp), hay nâng cao (struct động, nested, file I/O)?