Tạo bởi Trần Văn Điêp|
Lập Trình C

[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:

struct Student { char name[50]; int age; float gpa; };

Sau khi định nghĩa, bạn có thể khai báo biến kiểu này bằng:

struct Student s1;

Để rút gọn tên kiểu, thường dùng typedef:

typedef struct { char name[50]; int age; float gpa; } Student;

Khi dùng typedef, bạn có thể khai báo trực tiếp:

Student s2;

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:

    struct Point { int x, y; } p1, p2;

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:

  1. Gán từng thành phần

    Student s; strcpy(s.name, "An"); s.age = 20; s.gpa = 3.5;

    Lưu ý cần <string.h> cho strcpy.

  2. Khởi tạo danh sách (aggregate initialization)

    Student s = {"An", 20, 3.5};

    Thành phần được gán theo thứ tự khai báo trong struct.

  3. Khởi tạo một phần (designated initializer - C99)

    Student s = {.age = 20, .gpa = 3.5};

    Thuộc tính không gán sẽ được khởi tạo về 0.

  4. Gán struct trực tiếp

    Student a = {"A", 19, 3.2}; Student b = a; // copy toàn bộ nội dung

    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:

Student students[100]; // lưu tối đa 100 sinh viên int n = 0;

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 qsort vớ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:

int cmp(const void *a, const void *b) { const Student *s1 = a; const Student *s2 = b; if (s1->gpa < s2->gpa) return -1; if (s1->gpa > s2->gpa) return 1; return 0; } qsort(students, n, sizeof(Student), cmp);

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:

    students[0].age = 21;
  • Dùng -> khi bạn có con trỏ tới struct:

    Student *p = &students[0]; p->age = 22; // tương đương (*p).age = 22;

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:

    Student *p = malloc(sizeof(Student)); if (p) { strcpy(p->name, "Binh"); p->age = 23; free(p); }

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:

  1. Truyền theo giá trị (by value)
    Hàm nhận một bản sao của struct:

    void print_student(Student s) { printf("%s %d %.2f\n", s.name, s.age, s.gpa); }

    Thay đổi s trong hàm không ảnh hưởng đến biến gốc.

  2. Truyền theo con trỏ (by reference)
    Truyền địa chỉ để hàm có thể cập nhật dữ liệu:

    void update_gpa(Student *s, float g) { s->gpa = g; }

    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:

typedef struct { int day, month, year; } Date; typedef struct { char name[50]; Date dob; // nested struct float gpa; } Student;

Bạn truy cập như:

students[i].dob.year = 2000;

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ụ:

typedef struct { char c; // 1 byte int x; // 4 bytes, but aligned to 4 } A;

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 pack hoặ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:

Student *p = malloc(sizeof(Student));

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:

    strcpy(s.name, input); // ensure input fits into s.name

    Giải pháp: dùng strncpy hoặc kiểm tra độ dài.

  • Trả về địa chỉ biến cục bộ:

    Student* f() { Student s; return &s; // ❌ sai }

    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:

#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char id[10]; char name[50]; int age; float gpa; } Student; void printStudent(const Student *s) { printf("%s - %s - %d - %.2f\n", s->id, s->name, s->age, s->gpa); } int main(void) { Student students[100]; int n = 0; // Thêm 2 sinh viên mẫu strcpy(students[n].id, "SV001"); strcpy(students[n].name, "Tran Van A"); students[n].age = 20; students[n].gpa = 3.4; n++; strcpy(students[n].id, "SV002"); strcpy(students[n].name, "Le Thi B"); students[n].age = 21; students[n].gpa = 3.8; n++; printf("Danh sach sinh vien:\n"); for (int i = 0; i < n; i++) printStudent(&students[i]); // Tìm sinh viên có gpa cao nhất int idx = 0; for (int i = 1; i < n; i++) if (students[i].gpa > students[idx].gpa) idx = i; printf("Sinh vien diem cao nhat:\n"); printStudent(&students[idx]); return 0; }

Đâ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)?

Phản hồi từ học viên

5

Tổng 0 đánh giá

Đăng nhập để làm bài kiểm tra

Chưa có kết quả nào trước đó