← Lập trình C cơ bản

Bài 14 · Vận dụng · 22 phút· Cập nhật 11/06/2026

Struct & kiểu tự đặt tên

Biên soạn bởi Nguyễn Anh Tuấn

Dùng struct để gom nhiều trường khác kiểu; truy cập bằng dấu chấm, truyền struct vào hàm, typedef và thay mảng song song bằng mảng struct.

Mảng (Bài 13) chứa nhiều giá trị - nhưng phải cùng một kiểu. Trong khi một "việc cần làm" của app Todo gồm ba thuộc tính khác kiểu: mã số (int), tên việc (chuỗi), đã xong chưa (bool - Bài 7). Cách xoay xở đầu tiên là ba mảng song song, thẳng hàng theo chỉ số. Chạy thử nhiệm vụ "xoá một việc" trên cả hai cách lưu mà xem:

Nhiệm vụ: xoá việc "hoc bai" (đang ở vị trí thứ 2) khỏi cả hai cách lưu.

Cách 1 - ba mảng song song
idtenxong
1tuoi caytrue
2hoc baifalse
3rua chenfalse
3 mảng đang thẳng hàng - nhưng xoá là phải nhớ đủ 3 nút.
Cách 2 - một mảng struct Task
idtenxong
1tuoi caytrue
2hoc baifalse
3rua chenfalse
Mỗi hàng là MỘT khối: id, ten, xong dính nhau.
  • Mảng song song HOẠT ĐỘNG - nhưng mọi thao tác phải lặp đúng trên từng mảng, quên một cái là dữ liệu ghép sai.
  • Tệ nhất: C không báo lỗi gì - chương trình chạy tiếp với dữ liệu lệch.
  • struct chữa tận gốc: gom các thuộc tính thành MỘT bản ghi, đi đâu đi nguyên khối.

Khai báo structtự đặt một kiểu mới mô tả bản ghi của bạn; mỗi biến kiểu đó chứa đủ các trường (field), truy cập bằng dấu chấm:

task.c - khởi tạo theo TÊN trường, phong cách Modern C

#include <stdio.h>
#include <stdbool.h>

struct Task {
    int  id;
    char ten[32];
    bool xong;
};

int main(void) {
    struct Task t = { .id = 1, .ten = "tuoi cay", .xong = false };
    t.xong = true;
    printf("[%c] #%d %s\n", t.xong ? 'x' : ' ', t.id, t.ten);
    return 0;
}

Kết quả khi chạy

[x] #1 tuoi cay
  • Khai báo struct Task {…}; tạo ra kiểu mới - chưa tốn ô nhớ nào (nhớ Bài 4: đây là khai báo kiểu, biến t mới là định nghĩa).
  • Khởi tạo theo tên trường { .id = 1, … } (designated initializer): không sợ đảo thứ tự trường - Modern C khuyên dùng hẳn cách này.
  • t.xong ? 'x' : ' ' là toán tử ?: - gọn cho "nếu xong thì x, không thì để trống".

Đưa struct vào hàm là truyền theo giá trị: hàm nhận một bản sao. Sửa bản sao thì bản gốc bình an vô sự - muốn giữ thay đổi, hãy trả struct về và gán lại:

diem.c - sửa bản sao vô ích; trả về rồi gán mới ăn

#include <stdio.h>

struct Diem { int x, y; };

void keo_phai_hong(struct Diem d) {
    d.x = d.x + 10;              /* sua BAN SAO - vo ich */
}

struct Diem keo_phai(struct Diem d) {
    d.x = d.x + 10;
    return d;                    /* tra ban da sua ve   */
}

int main(void) {
    struct Diem a = { 3, 4 };
    keo_phai_hong(a);
    printf("sau keo_phai_hong: a.x = %d\n", a.x);
    a = keo_phai(a);
    printf("sau a = keo_phai(a): a.x = %d\n", a.x);
    return 0;
}

Kết quả khi chạy

sau keo_phai_hong: a.x = 3
sau a = keo_phai(a): a.x = 13

Sẽ gặp lại

Truyền giá trị an toàn nhưng struct to thì copy tốn, và có lúc bạn CẦN hàm sửa thẳng bản gốc. Lời giải là truyền địa chỉ của struct - mở màn cho bài Struct & con trỏ struct bên khoá C nâng cao. Ở khoá này, copy vào - copy ra là đủ dùng và dễ lành.

Ghép hai thứ đã học: mảng CÁC struct thay trọn bộ ba mảng song song. Kèm typedef để khỏi lặp chữ struct mỗi lần khai báo:

todo.c - danh sách việc bằng MỘT mảng Task

#include <stdio.h>
#include <stdbool.h>

typedef struct {
    int  id;
    char ten[32];
    bool xong;
} Task;

int main(void) {
    Task ds[3] = {
        { 1, "tuoi cay", true  },
        { 2, "hoc bai",  false },
        { 3, "rua chen", false }
    };
    int chua_xong = 0;
    for (int i = 0; i < 3; i++) {
        printf("[%c] #%d %s\n", ds[i].xong ? 'x' : ' ', ds[i].id, ds[i].ten);
        if (!ds[i].xong) chua_xong++;
    }
    printf("Con %d viec chua xong\n", chua_xong);
    return 0;
}

Kết quả khi chạy

[x] #1 tuoi cay
[ ] #2 hoc bai
[ ] #3 rua chen
Con 2 viec chua xong
  • ds[i].ten - đọc trong ra ngoài: phần tử thứ i của mảng, rồi lấy trường ten của nó.
  • typedef struct {…} Task; đặt bí danh: từ đây viết Task thay cho cả cụm - code gọn và dễ đọc hơn hẳn.
  • Xoá/thêm/sửa giờ chỉ đụng MỘT mảng - đúng điều công cụ Bước 1 đã chứng minh.

Trung thực: kích thước struct

sizeof(Task) có thể lớn hơn tổng kích thước các trường: trình biên dịch được phép chèn byte đệm (padding) để giữ alignment cho CPU đọc nhanh. Đừng tự cộng tay - cứ hỏi sizeof. Vì sao có đệm và đệm bao nhiêu: bài Union, biểu diễn byte & alignment của khoá C nâng cao mổ xẻ tận nơi.

Bạn vừa nhận công cụ đóng gói dữ liệu đầu tiên - đúng thứ mà Modern C dạy ngay từ Level 1, vì gần như mọi chương trình thật đều xoay quanh các bản ghi: học sinh, sản phẩm, giao dịch, việc cần làm…

  • Mảng = nhiều phần tử cùng kiểu; struct = một bản ghi nhiều trường khác kiểu; mảng struct = danh sách bản ghi.
  • struct gán được nguyên khối (a = b), truyền vào hàm là copy - an toàn, dễ đoán.
  • typedef cho kiểu một cái tên đáng đọc - gặp dày đặc trong code C ngoài đời.

Bài tiếp theo

Bài kế là Dự án cuối khoá: 3 app nhỏ - phiên bản đầu cố tình dùng mảng song song với các công cụ tối giản nhất, để mèo con cảm nhận đúng cái khó mà struct vừa giải. Làm xong đừng quên bài tập 6: nâng cấp app Todo lên struct - phần thưởng xứng đáng cho bài này.

Câu hỏi thường gặp

Mảng là NHIỀU phần tử CÙNG kiểu, truy cập bằng chỉ số ([i]). struct là MỘT bản ghi gồm các trường KHÁC kiểu, truy cập bằng tên (.id, .ten). Chúng bổ trợ nhau: thực tế hay dùng "mảng các struct" - nhiều bản ghi, mỗi bản ghi nhiều trường.

Cách theo TÊN trường (designated initializer, có từ C99 và được Modern C khuyên dùng) không phụ thuộc thứ tự khai báo trường: sau này ai đó thêm/đảo trường trong struct, code khởi tạo của bạn vẫn đúng. Cách theo vị trí thì lặng lẽ gán nhầm trường - bug khó tìm.

Có copy thật: struct 40 byte là copy 40 byte. Với struct nhỏ thì không đáng kể và đổi lại rất an toàn (hàm không phá được bản gốc). Struct lớn hoặc cần SỬA bản gốc thì truyền địa chỉ - đó là bài "Struct & con trỏ struct" của khoá C nâng cao.

ten là MẢNG char nằm trong struct - mảng không gán được bằng dấu = sau khi đã khai báo (chỉ lúc khởi tạo). Muốn đổi nội dung thì chép chuỗi vào bằng hàm strcpy của thư viện <string.h>. Lưu ý thú vị: bản thân struct thì GÁN ĐƯỢC nguyên khối (a = b copy mọi trường - kể cả mảng bên trong).

Không - typedef chỉ đặt BÍ DANH (tên gọi khác) cho kiểu có sẵn: typedef struct {…} Task; cho phép viết Task thay cho cả cụm. Máy không phân biệt hai tên. Nó là công cụ cho người đọc, và bạn sẽ gặp nó dày đặc trong code C thật.

Thường là KHÔNG - trình biên dịch được phép chèn byte đệm (padding) giữa các trường để giữ alignment cho CPU đọc nhanh. Chuyện vì sao và đệm bao nhiêu nằm ở bài "Union, biểu diễn byte & alignment" của khoá C nâng cao; bài này chỉ cần bạn biết: đừng tự cộng tay.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

struct giải quyết điểm yếu nào của mảng song song?

  1. 1

    Phá rồi chữa

    Trong công cụ Bước 1: cố tình xoá thiếu một mảng để gây lệch hàng, quan sát hàng dữ liệu sai; đặt lại và làm đúng đủ 3 mảng.

    Hoàn thành khi: Chỉ ra được chính xác hàng nào ghép sai dữ liệu khi lệch, và vì sao C không báo lỗi.

  2. 2

    Bản ghi đầu tiên của mèo con

    Khai báo struct MeoCon { ten[24], tuoi, can_nang } rồi tạo một biến bằng khởi tạo theo tên trường và in ra.

    Hoàn thành khi: Biên dịch sạch với bộ cờ của khoá; in đủ 3 trường bằng dấu chấm.

  3. 3

    Chứng minh "truyền giá trị"

    Viết hàm sinh_nhat(struct MeoCon m) tăng m.tuoi bên trong. Gọi xong in tuổi bản gốc. Rồi sửa thành m = sinh_nhat(m) (hàm trả struct).

    Hoàn thành khi: Bản đầu: tuổi gốc KHÔNG đổi. Bản sau: tuổi tăng 1 - hiểu rõ copy vào, copy ra.

  4. 4

    Sổ điểm 3 môn

    Tạo typedef struct { ten[16]; double diem; } Mon; mảng 3 Mon (Toán/Văn/Tin), duyệt vòng lặp tính điểm trung bình.

    Hoàn thành khi: In từng môn một dòng và dòng cuối là trung bình cộng - tự kiểm lại bằng máy tính tay.

  5. 5

    Gán nguyên khối

    Tạo Task a, rồi Task b = a;. Sửa b.ten (bằng strcpy) và b.xong. In cả a lẫn b.

    Hoàn thành khi: a giữ nguyên, b mang giá trị mới - struct gán = là copy trọn bộ, hai bản độc lập.

  6. 6

    Nâng cấp dự án cuối khoá (làm trước một bước)

    Mở bài kế (Dự án cuối khoá), lấy app Todo phiên bản mảng song song và viết lại bằng MỘT mảng Task như Bước 4.

    Hoàn thành khi: Mọi lệnh them/xong/in chạy như cũ nhưng code chỉ còn một mảng - không còn cảnh "sửa mảng này quên mảng kia".