This C programming guide is currently only available in Vietnamese. Please toggle the language switch (🇻🇳) in the top navigation to read the full article.

In this fourth part of the series, we explore user-defined types using Struct, define aliases using the Typedef keyword for cleaner code, and understand the difference in memory layout between Struct and Union.

Các kiểu dữ liệu cơ bản như int, float, char chỉ giúp chúng ta mô tả các giá trị đơn lẻ. Tuy nhiên, trong thế giới thực tế, một thực thể thường bao gồm nhiều thuộc tính kết hợp lại. Ví dụ: Một Sinh Viên gồm có: Tên (chuỗi), Tuổi (số nguyên), Điểm số (số thực). Để gom nhóm các thông tin liên quan này lại với nhau thành một kiểu dữ liệu phức hợp tự định nghĩa, C cung cấp cho chúng ta từ khóa Struct (Structure).

1. Cấu trúc Struct là gì?

Struct cho phép gom nhiều biến có kiểu dữ liệu khác nhau lại thành một kiểu dữ liệu mới duy nhất. Các biến thành phần bên trong được gọi là thành viên (members) của Struct.

Ví dụ: Định nghĩa cấu trúc đại diện cho một Sinh Viên:

student_struct.c
#include <stdio.h>
#include <string.h>

// Dinh nghia struct Student
struct Student {
    char name[50];
    int age;
    double score;
};

int main() {
    // Khai bao va khoi tao bien kieu struct Student
    struct Student sv1;
    
    // Gan gia tri cho cac thanh vien bang toan tu cham (.)
    strcpy(sv1.name, "Nguyen Van A");
    sv1.age = 20;
    sv1.score = 8.5;

    // Khoi tao nhanh luc khai bao
    struct Student sv2 = {"Tran Thi B", 19, 9.2};

    // Truy xuat va in du lieu
    printf("Sinh vien 1: %s, %d tuoi, %.1f diem\n", sv1.name, sv1.age, sv1.score);
    printf("Sinh vien 2: %s, %d tuoi, %.1f diem\n", sv2.name, sv2.age, sv2.score);

    return 0;
}

2. Bản chất lưu trữ RAM: Structure Padding & Alignment

Mặc dù một Struct gộp nhiều biến, nhưng cách trình biên dịch sắp xếp các biến này trên RAM không diễn ra liên tục từng byte một. Để CPU có thể truy xuất dữ liệu từ RAM với hiệu năng cao nhất (thường là truy xuất theo từng dòng bus địa chỉ chia hết cho 4 hoặc 8 bytes gọi là Word Lines), trình biên dịch sẽ áp dụng quy tắc Căn biên dữ liệu (Data Alignment).

Quy tắc này sẽ tự động chèn các byte trống gọi là Padding Bytes vào giữa các thành viên của Struct.

A. Lỗi lãng phí bộ nhớ do Padding

Xét cấu trúc Struct sau đây:

struct BadStruct {
    char a;   // 1 byte
    int b;    // 4 bytes
    char c;   // 1 byte
};

Mặc dù tổng kích thước thật của các biến là \(1 + 4 + 1 = 6\) bytes, nhưng hàm sizeof(struct BadStruct) sẽ trả về 12 bytes!
Dưới đây là sơ đồ ô nhớ RAM mà trình biên dịch tạo ra cho struct này:

[a (1B)] [padding (3B)] [      b (4B)      ] [c (1B)] [padding (3B)] -> Tổng: 12 Bytes
              

B. Cách tối ưu sắp xếp (Field Reordering)

Để tối ưu hóa bộ nhớ, ta chỉ cần khai báo các biến có kích thước lớn ở phía trước và các biến nhỏ ở phía sau:

struct GoodStruct {
    int b;    // 4 bytes
    char a;   // 1 byte
    char c;   // 1 byte
};

Lúc này, sơ đồ ô nhớ sẽ gom các biến nhỏ lại chung một Word Line:

[      b (4B)      ] [a (1B)] [c (1B)] [padding (2B)] -> Tổng: 8 Bytes (Tiết kiệm 33% RAM!)
              

C. Sử dụng #pragma pack(1) để xóa bỏ hoàn toàn Padding

Trong các hệ thống nhúng (Embedded) hoặc khi lập trình truyền gói tin qua mạng (Network Protocols), ta cần kích thước struct phải chuẩn từng byte để gửi nhận dữ liệu trực tiếp. C cung cấp chỉ thị tiền xử lý để ép biên dịch không chèn padding:

#pragma pack(push, 1) // Ép căn biên về 1 byte (xóa padding)
struct PackedStruct {
    char a;   // 1 byte
    int b;    // 4 bytes
    char c;   // 1 byte
};
#pragma pack(pop) // Khôi phục lại căn biên mặc định

Lúc này, sizeof(struct PackedStruct) sẽ trả về đúng 6 bytes thực tế. Lợi thế là tiết kiệm RAM tối đa, nhưng nhược điểm là tốc độ đọc ghi của CPU lên struct này sẽ chậm đi một chút do địa chỉ nhớ bị lệch biên.

D. Kỹ thuật khai báo trường Bit (Bit-fields)

Khi bạn cần khai báo các biến Boolean hoặc số nguyên có phạm vi cực kỳ nhỏ (ví dụ chỉ từ 0 đến 7, tương đương 3 bit), việc dùng một biến int 32 bit là quá lãng phí. C cho phép chỉ định số bit tối đa cho mỗi biến thành viên:

struct StatusFlags {
    unsigned int isOnline : 1;  // Chỉ chiếm đúng 1 bit (0 hoặc 1)
    unsigned int role     : 3;  // Chiếm đúng 3 bits (giá trị từ 0 đến 7)
    unsigned int status   : 4;  // Chiếm đúng 4 bits
}; // Cả struct này sẽ được nén và chỉ tốn đúng 1 byte (8 bits) bộ nhớ!

3. Sử dụng từ khóa typedef để viết code sạch hơn

Khi khai báo biến struct ở ví dụ trên, ta luôn phải viết từ khóa struct Student sv1;. Điều này khiến mã nguồn trở nên dài dòng.

Từ khóa typedef (Type Definition) dùng để định nghĩa một bí danh (alias) cho kiểu dữ liệu hiện tại, giúp viết code ngắn gọn hơn.

typedef_example.c
#include <stdio.h>

// typedef ket hop struct giup tao ra ten kieu du lieu moi gon sach
typedef struct {
    int x;
    int y;
} Point;

int main() {
    // Khong can ghi chu "struct Point p1;"
    Point p1 = {10, 20};
    Point p2 = {30, 40};

    printf("Toa do diem 1: (%d, %d)\n", p1.x, p1.y);
    printf("Toa do diem 2: (%d, %d)\n", p2.x, p2.y);
    
    return 0;
}

4. Phân biệt Struct và Union

Union có cú pháp định nghĩa hoàn toàn tương tự Struct, nhưng cấu trúc lưu trữ trên RAM thì hoàn toàn khác biệt:

  • Struct: Mỗi thành viên được cấp phát một phân vùng nhớ riêng độc lập. Dung lượng của struct bằng tổng dung lượng các thành viên cộng lại (có thể cộng thêm padding để căn lề bộ nhớ).
  • Union: Tất cả các thành viên dùng chung một phân vùng nhớ duy nhất. Dung lượng của Union bằng dung lượng của thành viên lớn nhất. Tại một thời điểm, bạn chỉ có thể lưu trữ và truy xuất giá trị của một thành viên duy nhất. Nếu ghi đè thành viên này, giá trị của các thành viên khác sẽ bị thay đổi hoặc sai lệch.

Hãy cùng chạy chương trình so sánh kích thước dung lượng (kích thước byte) để thấy rõ sự khác biệt (lưu ý rằng kích thước của từng kiểu dữ liệu cụ thể như int, double và cấu trúc padding/căn lề bộ nhớ có thể thay đổi tùy theo kiến trúc hệ thống, tham khảo thêm đặc tả về Object Alignment (Căn lề đối tượng)Struct declarations trên cppreference.com):

struct_vs_union.c
#include <stdio.h>

typedef struct {
    char a;    // 1 byte
    int b;     // 4 bytes
    double c;  // 8 bytes
} MyStruct;

typedef union {
    char a;    // 1 byte
    int b;     // 4 bytes
    double c;  // 8 bytes
} MyUnion;

int main() {
    MyStruct s;
    MyUnion u;

    // In kich thuoc bo nho (Sizeof)
    printf("Kich thuoc Struct: %lu bytes\n", sizeof(s)); // Ket qua: thuong la 16 bytes (do padding)
    printf("Kich thuoc Union: %lu bytes\n", sizeof(u));  // Ket qua: dung 8 bytes (kich thuoc cua double c)

    // Thu thay doi gia tri trong Union
    u.c = 9.87;
    printf("u.c = %.2f\n", u.c);
    
    u.b = 100; // Ghi de len phan bo nho chung
    printf("u.b = %d\n", u.b);
    printf("u.c sau khi ghi de u.b: %.2f\n", u.c); // Gia tri u.c bi thay doi, khong con chinh xac!

    return 0;
}

📥 Tải về mã nguồn mẫu: struct_vs_union.c

Ứng dụng thực tế của Union:

Union thường được dùng khi lập trình nhúng (vi điều khiển) hoặc lập trình hệ thống ở các phân vùng tài nguyên RAM cực kỳ hạn chế, nơi ta có một biến có thể đại diện cho nhiều kiểu dữ liệu khác nhau nhưng không bao giờ xuất hiện đồng thời.

📝 Kiểm tra kiến thức bài 4
Nếu ta định nghĩa union Data { char a; int b; double c; } u; và thực hiện gán u.c = 9.87; tiếp theo gán u.b = 100;. Phát biểu nào sau đây là chính xác nhất?

Related Articles

Bài viết liên quan trong series

Lesson 8: Mastering Pointers in C: From Basics to Advanced Bài 8: Làm chủ Con Trỏ (Pointers) trong C từ cơ bản đến nâng cao Lesson 6: Arrays, Strings & Text Processing in C Bài 6: Mảng, Chuỗi ký tự & Xử lý văn bản trong C Back to C Series Overview Quay lại Lộ trình C Series