Lập trình C: Bài 13 – Kiểu cấu trúc – struct

Cách đơn giản nhất để tiếp cận về kiểu cấu trúc là xét ví dụ sinh viên. Một lớp có 100 sinh viên mỗi sinh viên gồm họ tên và mã sinh viên. Hãy nhập dữ liệu cho lớp đó. Hehe. Bạn nghĩ đến cách dùng 2 mảng: 1 mảng lưu tên, 1 mảng lưu mã sinh viên đúng không. Đúng, cách đó không sai… nhưng hãy xem yêu cầu tiếp theo… Thi học kỳ xong, hãy nhập điểm cho từng sinh viên, mỗi sinh viên gồm 10 môn (Toán, Tin, Hóa, Vật lý,…). Giờ bạn thấy sao nào… Dùng 12 mảng chăng… ồ không nên, hãy dùng kiểu cấu trúc. Với kiểu cấu trúc chỉ cần 1 mảng mà thôi.

1. Kiểu cấu trúc

Đối với mảng, chỉ có thể lưu nhiều thông tin có cùng kiểu dữ liệu. Nhưng với kiểu cấu trúc ta có thể lưu thông tin có nhiều kiểu dữ liệu khác nhau.

1.1 VD mở đầu

//code by nguyenvanquan7826
#include <stdio.h>
#include <stdlib.h>

// khai bao struct
struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double toan, tin, anh; // cac diem toan, tin, anh
};

/* Hay thay tat ca fflush(stdin); thanh __fpurge(stdin) khi ban lam tren linux*/

int main() 
{
    /* khai bao 2 bien sv1, sv2 va 1 mang
     * CNPMK10A gom 100 sinh vien
     */
    struct sinhvien sv1, sv2, CNPMK10A[100];

    printf("Nhap du lieu cho sv1:\n");
    printf("MSV: "); fflush(stdin);
    gets(sv1.MSV);
    printf("Ho ten: "); fflush(stdin);
    gets(sv1.hoten);
    printf("Diem toan, tin, anh: "); fflush(stdin);
    scanf("%lf %lf %lf", &sv1.toan, &sv1.tin, &sv1.anh);

    printf("Nhap du lieu cho sv2:\n");
    printf("MSV: "); fflush(stdin);
    gets(sv2.MSV);
    printf("Ho ten: "); fflush(stdin);
    gets(sv2.hoten);
    printf("Diem toan, tin, anh: "); fflush(stdin);
    scanf("%lf %lf %lf", &sv2.toan, &sv2.tin, &sv2.anh);

    printf("\n --------- Thong tin sinh vien -----\n");
    printf("%-20s %-30s %-7s %-7s %-7s\n", "MSV", "Ho ten", "Toan", "Tin", "Anh");
    printf("%-20s %-30s %-7.2lf %-7.2lf %-7.2lf\n", sv1.MSV, sv1.hoten, sv1.toan, sv1.tin, sv1.anh);
    printf("%-20s %-30s %-7.2lf %-7.2lf %-7.2lf\n", sv2.MSV, sv2.hoten, sv2.toan, sv2.tin, sv2.anh);

    return 0;
}

Kết quả:

Nhap du lieu cho sv1:
MSV: DTC1
Ho ten: Pham Thi Ha
Diem toan, tin, anh: 9 9 8
Nhap du lieu cho sv2:
MSV: DTC2
Ho ten: Nguyen Van Quan
Diem toan, tin, anh: 9 9 8

 --------- Thong tin sinh vien -----
MSV                  Ho ten                         Toan    Tin     Anh
DTC1                 Pham Thi Ha                    9.00    9.00    8.00
DTC2                 Nguyen Van Quan                9.00    9.00    8.00

Ở VD mở đầu này, chúng ta có rất nhiều điểu phải bàn 🙂

1.2 Xây dựng kiểu cấu trúc, khai báo biến cấu trúc

Như VD trên, để xây dựng 1 kiểu cấu trúc ta thực hiện theo cú pháp:

struct TenKieuCauTruc
{
    Khai báo các thành phần của kiểu;
};

Sau khi có kiểu cấu trúc rồi thì cái kiểu đó nó tương tự như 1 kiểu bình thường (int, float, char,…) và ta chỉ việc khai báo biến nữa là xong. Tuy nhiên khai báo biến thì cần có thêm từ khóa struct ở trước: (Đối với C++ thì không cần).

struct TenKieuCauTruc TenBienCauTruc;

Ngoài ta chúng ta còn một số cách xây dựng kiểu cấu trúc và khai báo biến cấu trúc như sau:

struct TenKieuCauTruc
{
    Khai báo các thành phần của kiểu;
} danh sách các biến thuộc kiểu cấu trúc;

Hoặc

struct
{
    Khai báo các thành phần của kiểu;
} danh sách các biến thuộc kiểu cấu trúc ;

Với cách khai báo này, ta bắt buộc phải khai báo các biến cấu trúc ở ngay sau cấu trúc vì không có tên kiểu cấu trúc để cho ta khai báo ở các vị trí khác nữa.
Trong phần này ta cần đề cập đến 1 từ khóa quan trọng nữa, đó là typedef. Từ khóa này dùng để định nghĩa 1 kiểu dữ liệu mới.

typedef struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double toan, tin, anh; // cac diem toan, tin, anh
} kieuSinhVien;

Khi này ta có kieuSinhVien là 1 kiểu dữ liệu (như int, double, …) và ta có thể khai báo các biến cấu trúc thông qua nó. Trong này có vài điều các bạn cần chú ý:

  • Với sinhvien (kiểu cấu trúc được đặt sau từ khóa struct) thì khi khai báo biến thuộc kiểu này ta vẫn phải có từ struct ở trước nó. (VD: sinhvien svA; -> Sai còn struct sinhvien svA; -> đúng), (chú ý trong C++ thì không cần).
  • Với kieuSinhVIen thì khi khai báo biến thuộc kiểu này ta không được có từ struct ở trước nó. (VD: struct kieuSinhVIen svA; -> sai, kieuSinhVIen svA; -> đúng).

Ngoài ra ta còn có thể khai báo kiểu cấu trúc lồng nhau: VD như trong 1 sinh viên có ngày sinh, trong ngày sinh lại có ngày, tháng, năm sinh.

struct ngaysinh 
{
    int ngay, thang, nam;
}

typedef struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double toan, tin, anh; // cac diem toan, tin, anh
    struct ngaysinh ns;
} kieuSinhVien;

Hoặc ta khai báo ngay trong cấu trúc:

typedef struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double toan, tin, anh; // cac diem toan, tin, anh
    struct ngaysinh 
    {
        int ngay, thang, nam;
    } ns;
} kieuSinhVien;

1.3 Truy cập đên các thành phần của cấu trúc

Để truy nhập đến thành phần của cấu trúc ta sử dụng toán tử chấm (.).

TenBienCauTruc.TenThanhPhan;

Như VD trên ta truy cập như sau:
sv1.hoten; sv1.toan; // truy xuất tới họ tên, điểm toán
sv1.ns.ngay; sv1.ns.thang; // truy xuất tới ngày sinh và tháng sinh.

Khi đã truy xuất được tới các thành phần của cấu trúc thì mỗi thành phần đó là 1 biến bình thường và ta gán giá trị hoặc nhập xuất giá trị cho chúng như cách bình thường mà chúng ta vẫn làm.

Ngoài ra nếu thành phần nào đó dài dòng thì ta có thể tránh việc dài dòng này bằng cách sử dụng từ khóa define.
VD thay vì viết:

sv1.ns.thang;
sv1.ns.nam;

Ta viết:

#define p sv1.ns
p.thang;
p.nam;

1.4 Gán các biến có cùng kiểu cấu trúc

//code by nguyenvanquan7826
#include <stdio.h>
#include <stdlib.h>

struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double toan, tin, anh; // cac diem toan, tin, anh
};

/* Hay thay tat ca fflush(stdin); thanh __fpurge(stdin) khi ban lam tren linux*/

int main() 
{
    /* khai bao 2 bien sv1, sv2 va 1 mang
     * CNPMK10A gom 100 sinh vien
     */
    struct sinhvien sv1, sv2, CNPMK10A[100];

    printf("Nhap du lieu cho sv1:\n");
    printf("MSV: "); fflush(stdin);
    gets(sv1.MSV);
    printf("Ho ten: "); fflush(stdin);
    gets(sv1.hoten);
    printf("Diem toan, tin, anh: "); fflush(stdin);
    scanf("%lf %lf %lf", &sv1.toan, &sv1.tin, &sv1.anh);

    sv2 = sv1; // gan gia tri cua sv1 cho sv2

    printf("\n --------- Thong tin sinh vien -----\n");
    printf("%-20s %-30s %-7s %-7s %-7s\n", "MSV", "Ho ten", "Toan", "Tin", "Anh");
    printf("%-20s %-30s %-7.2lf %-7.2lf %-7.2lf\n", sv2.MSV, sv2.hoten, sv2.toan, sv2.tin, sv2.anh);

    return 0;
}

Sau khi gán sv2 = sv1 thì mọi thông tin của sv1 có thì sv2 cũng có. Ngoài ra ta còn có thể gán giá trị khởi đầu cho cấu trúc.

struct sinhvien sv1 = {"ABC", "Nguyen Van Quan", 9, 9, 8, {4, 5, 1992}};

Khi đó ta có các dữ liệu ban đầu của sv1 là:

MSV: ABC
hoten: Nguyen Van Quan
toan: 9
tin: 9
anh: 8
ngày sinh: 4/5/1992.

2. Mảng cấu trúc

Bên trên ta đã tìm hiểu cơ bản về kiểu cấu trúc và một vài ví dụ về cấu trúc sinhvien. Bây giờ ta tìm hiểu cách biểu diễn 1 mảng 50 sinh viên của 1 lớp học có kiểu cấu trúc như trên. Ta xét VD:

//code by nguyenvanquan7826
#include <stdio.h>
#include <stdlib.h>

struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double diemTB; // diem trung binh
    struct ngaysinh 
    {
        int ngay, thang, nam;
    } ns;
};

int main() 
{
    int n = 2, i;
    struct sinhvien CNPMK10A[n];

    for (i = 0; i < n; i++) 
    {
        #define sv CNPMK10A[i]
        printf("Nhap du lieu cho sinh vien thu %d:\n", i + 1);
        printf("MSV: "); fflush(stdin)
        gets(sv.MSV);
        printf("Ho ten: "); fflush(stdin);
        gets(sv.hoten);
        printf("Diem TB: "); fflush(stdin)
        scanf("%lf", &sv.diemTB);
        printf("Ngay sinh: ");
        scanf("%d/%d/%d", &sv.ns.ngay, &sv.ns.thang, &sv.ns.nam);
    }   

    printf("\n --------- Thong tin sinh vien -----\n");
    printf("%-20s %-30s %-7s %-10s\n", "MSV", "Ho ten", "Diem Tb", "Ngay sinh");
    for (i = 0; i < n; i++) 
    {
        #define sv CNPMK10A[i]
        printf("%-20s %-30s %-7.2lf %02d/%02d/%4d\n", sv.MSV, sv.hoten, sv.diemTB, sv.ns.ngay, sv.ns.thang, sv.ns.nam);
    }
    return 0;
}

Kết quả:

Nhap du lieu cho sinh vien thu 1:
MSV: DTC1
Ho ten: Pham Thi Ha
Diem TB: 9.2
Ngay sinh: 21/01/1993
Nhap du lieu cho sinh vien thu 2:
MSV: DTC2
Ho ten: Nguyen Van Quan
Diem TB: 9.2
Ngay sinh: 31/12/1992

 --------- Thong tin sinh vien -----
MSV                  Ho ten                         Diem Tb Ngay sinh
DTC1                 Pham Thi Ha                    9.20    21/01/1993
DTC2                 Nguyen Van Quan                9.20    31/12/1992

3. Con trỏ cấu trúc

//code by nguyenvanquan7826
#include <stdio.h>
#include <stdlib.h>

struct sinhvien 
{
    char MSV[20]; // ma sinh vien
    char hoten[30]; // ho ten sinh vien
    double diemTB; // diem trung binh
    struct ngaysinh 
    {
        int ngay, thang, nam;
    } ns;
};

int main() 
{
    int n = 2, i;
    // cap phat bo nho
    struct sinhvien *CNPMK10A = (struct sinhvien*) malloc(n * sizeof(struct sinhvien));

    for (i = 0; i < n; i++) 
    {
        printf("Nhap du lieu cho sinh vien thu %d:\n", i + 1);
        printf("MSV: "); fflush(stdin);
        gets(CNPMK10A[i].MSV);
        printf("Ho ten: "); fflush(stdin);
        gets(CNPMK10A[i].hoten);
        printf("Diem TB: "); fflush(stdin);
        scanf("%lf", &(CNPMK10A+i)->diemTB);
        printf("Ngay sinh: ");
        scanf("%d/%d/%d", &(CNPMK10A+i)->ns.ngay, &(CNPMK10A+i)->ns.thang, &(CNPMK10A+i)->ns.nam);
    }   

    printf("\n --------- Thong tin sinh vien -----\n");
    printf("%-20s %-30s %-7s %-10s\n", "MSV", "Ho ten", "Diem Tb", "Ngay sinh");
    for (i = 0; i < n; i++) 
    {
        #define ns CNPMK10A[i].ns
        printf("%-20s %-30s %-7.2lf %02d/%02d/%4d\n", CNPMK10A[i].MSV, (*(CNPMK10A+i)).hoten, (CNPMK10A+i)->diemTB, ns.ngay, ns.thang, ns.nam);
    }
    return 0;
}

Truy cập các thành phần cấu trúc

Để truy cập để lấy dữ liệu các thành phần của con trỏ cấu trúc ta có 3 cách sau:

  • Cách 1: CNPMK10A[i].diemTB;
  • Cách 2: (*(CNPMK10A+i)).diemTB;
  • Cách 3: (CNPMK10A+i) ->diemTB;

Cả 3 cách trên đều truy cập tới DTB.

Để lấy địa chỉ ta cũng có 2 cách:

  • Cách 1: &CNPMK10A[i].DTB;
  • Cách 2: &(CNPMK10A+i)->diemTB