Chuyển tới nội dung chính

BÀI 8. KIỂU DỮ LIỆU CẤU TRÚC

Mục tiêu​

Sau bài này, học viên:

  • Định nghĩa được struct, khai báo biến, khởi tạo, truy cập thành viên bằng . và ->.
  • Dùng được typedef để đặt tên kiểu rõ, và enum cho tập hằng có tên.
  • Lưu được một danh sách bằng mảng cấu trúc, truyền cấu trúc vào hàm theo giá trị hoặc theo con trỏ, và chọn cách phù hợp.
  • Viết được các kiểu toán học và nghiệp vụ: phân số, số phức, điểm, ngày, giờ, sinh viên.
  • Ở mức cao thủ: hiểu phần đệm căn chỉnh (padding), union như vùng nhớ dùng chung, và cách sắp mảng struct bằng qsort với hàm so sánh.

Sau bài này, tám chủ đề của môn nhập môn đã khép: chương trình, thuật toán, thành phần ngôn ngữ, rẽ nhánh, lặp, hàm, mảng, cấu trúc. Phần cao thủ của bài là mức làm được một chương trình nhỏ có kiểu dữ liệu riêng, hợp đồng hàm rõ, và bộ nhớ được giải thích được.


1. Vì sao cần cấu trúc​

Mảng chỉ chứa một kiểu. Ba mảng song song ma_so[], diem_giua[], diem_cuoi[] biểu diễn một sinh viên bằng ba ô nhớ không gắn nhau. Một lần sắp xếp quên hoán đổi một mảng là dữ liệu nói dối.

Cấu trúc (struct) gom các thành viên khác kiểu thành một đối tượng. Một sinh viên là một giá trị. Mảng sinh viên là một danh sách. Hoán đổi hai phần tử là hoán đổi cả hồ sơ.

struct SinhVien {
int ma;
char ten[50];
double giua_ky;
double cuoi_ky;
};

Tên SinhVien là thẻ (tag). Kiểu đầy đủ là struct SinhVien. Biến:

struct SinhVien sv;
sv.ma = 1;

Dấu . truy cập thành viên của một đối tượng. Thành viên có thể là mảng, con trỏ, hoặc struct khác.

Một biến struct là một căn nhà. Mỗi thành viên là một phòng. Địa chỉ căn nhà là &sv. Địa chỉ phòng mã là &sv.ma.

struct SinhVien sv
┌──────────────────────────────────────────┐
│ ma ten[50] giua cuoi │
│ ┌────┐ ┌──────────────┐ ┌─────┐ ┌─────┐│
│ │ 1 │ │ A n \0 ... │ │ 7.5 │ │ 8.0 ││
│ └────┘ └──────────────┘ └─────┘ └─────┘│
└──────────────────────────────────────────┘
&sv.ma &sv.ten &sv.giua

sv.ma đi vào phòng mã.
&sv là số nhà của cả căn, kiểu SinhVien *.

Khởi tạo theo thứ tự thành viên, hoặc theo tên từ C99:

struct SinhVien a = {1, "An", 7.5, 8.0};
struct SinhVien b = {.ma = 2, .cuoi_ky = 9.0, .giua_ky = 8.5, .ten = "Binh"};

Thành viên không nêu trong khởi tạo chỉ định được gán 0 / '\0'. Gán a = b được phép: C sao chép từng byte của struct. Không so sánh a == b. Hãy so từng thành viên, hoặc viết hàm bang_nhau.


2. typedef và enum​

typedef struct SinhVien SinhVien;

typedef struct {
int tu;
int mau;
} PhanSo;

typedef tạo tên mới cho một kiểu đã có. Nó không tạo kiểu mạnh theo nghĩa ngăn bạn trộn nhầm mọi int. Nó làm chữ ký hàm đọc được.

enum đặt tên cho các hằng nguyên:

typedef enum {
NAM,
NU,
KHAC
} GioiTinh;

Mặc định NAM là 0, NU là 1, KHAC là 2. Enum dùng trong switch rất rõ. Nhớ enum vẫn là số nguyên; gán một số ngoài tập vẫn biên dịch được trừ khi bạn tự kiểm.


3. Truyền struct cho hàm​

Truyền theo giá trị sao chép cả struct. An toàn, dễ suy luận, đắt nếu struct lớn, và hàm không sửa được bản của nơi gọi.

double diem_tong(SinhVien sv)
{
return 0.4 * sv.giua_ky + 0.6 * sv.cuoi_ky;
}

Truyền con trỏ khi muốn sửa, hoặc khi struct lớn:

void chuan_hoa_ten(SinhVien *sv)
{
if (sv == NULL) {
return;
}
/* sửa sv->ten */
}

sv->ma tương đương (*sv).ma. Mũi tên đi theo địa chỉ rồi lấy thành viên. Tiền điều kiện: con trỏ khác NULL.

Quy ước của khóa học:

  • Hàm chỉ đọc, struct nhỏ (điểm, phân số, ngày): truyền giá trị được.
  • Hàm sửa, hoặc struct chứa mảng lớn: truyền const Ten * khi chỉ đọc, Ten * khi ghi.

Mảng struct truyền như mọi mảng: địa chỉ phần tử đầu và số phần tử.

void in_danh_sach(const SinhVien *ds, int n);

4. Struct lồng nhau và mảng thành viên​

typedef struct {
int gio;
int phut;
int giay;
} Gio;

typedef struct {
char ten[40];
Gio bat_dau;
int thoi_luong_phut;
} BuoiHoc;

buoi.bat_dau.gio đi hai tầng. Mỗi tầng là một đối tượng thật, không phải con trỏ, nên không dùng -> trừ khi tầng đó là con trỏ.

Mảng thành viên ten[40] nằm trong chính struct. Sao chép struct là sao chép luôn mảng tên. Điều này khác hẳn một con trỏ char *ten chỉ sao chép địa chỉ và dễ thành hai struct cùng trỏ một chuỗi, hoặc trỏ vào bộ nhớ đã free.


5. Căn chỉnh và phần đệm (cao thủ)​

Trình biên dịch chèn byte đệm để thành viên nằm ở địa chỉ chia hết cho độ căn của nó. Trên nhiều máy, int cần địa chỉ chia hết cho 4.

struct Lech {
char c;
int x;
};

sizeof thường là 8 chứ không phải 5: 1 byte cho c, 3 byte đệm, 4 byte cho x.

Địa chỉ 0 1 2 3 4 5 6 7
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ c │ đệm│ đệm│ đệm│ x │
└────┴────┴────┴────┴────┴────┴────┴────┘
1 byte 3 byte trống 4 byte

Ba ngăn trống không có tên. Chúng có mặt để x bắt đầu ở địa chỉ chia hết cho 4.

Hệ quả:

  • sizeof struct không bằng tổng sizeof thành viên.
  • Ghi struct thẳng ra file nhị phân bằng fwrite cả khối sẽ ghi cả byte đệm. File đó không đáng tin khi đổi trình biên dịch hoặc đổi kiến trúc.
  • Muốn tiết kiệm ở chương trình nhúng, hãy xếp thành viên lớn trước, hoặc chấp nhận đệm để code rõ.

Đừng dùng sizeof của một struct làm giao thức mạng nếu bạn chưa khóa căn chỉnh và thứ tự byte.

Trường bit nhồi vài bit vào một thành viên nguyên:

struct Co {
unsigned int khoi : 1;
unsigned int pin_yeu : 1;
unsigned int muc : 3;
};

: 1 nghĩa là thành viên đó rộng 1 bit, không phải 1 byte. Dùng khi cần tiết kiệm trong bộ nhớ nhúng. Không lấy địa chỉ của một trường bit. Không giả định thứ tự bit trong từ là giống mọi máy. Bài giải thông thường của khóa vẫn dùng mặt nạ bit ở Bài 3 cho rõ hơn.

Struct tự trỏ là struct có một thành viên là con trỏ tới cùng kiểu đó:

struct Nut {
int gia_tri;
struct Nut *ke;
};

ke không chứa một struct Nut bên trong chính nó, vì như vậy căn nhà sẽ chứa một căn nhà cùng cỡ và kích thước không hữu hạn. ke chỉ chứa một số nhà. Đây là nền của danh sách liên kết. Khóa này dừng ở việc đọc được khai báo. Việc cấp phát nhiều nút và nối chúng thuộc môn cấu trúc dữ liệu.


6. union​

union các thành viên dùng chung một vùng nhớ. Kích thước bằng thành viên lớn nhất (cộng căn chỉnh). Ghi một thành viên rồi đọc thành viên khác là cách tái diễn giải bit, chỉ làm khi bạn cố ý và hiểu biểu diễn.

Dùng đúng đắn nhất trong khóa này: union phân biệt, kèm một trường nhãn.

typedef enum { HINH_TRON, HINH_CHU_NHAT } LoaiHinh;

typedef struct {
LoaiHinh loai;
union {
double ban_kinh;
struct {
double dai;
double rong;
} cn;
} so_do;
} Hinh;

Chỉ đọc ban_kinh khi loai == HINH_TRON. Nhãn và vùng nhớ phải khớp. Đây là cách C diễn tả “một trong các dạng” mà không lãng phí một struct chứa mọi trường của mọi dạng cùng lúc — dù sự lãng phí ấy đôi khi dễ đọc hơn và vẫn chấp nhận được.

union không phải hai phòng cạnh nhau. Đó là một phòng mang hai biển tên. Ghi theo biển này rồi đọc theo biển kia là đọc cùng những byte theo một kiểu khác.

struct: hai phòng riêng union: một phòng, hai biển
┌────────────┬────────────┐ ┌────────────┐
│ ban_kinh │ dai, rong │ │ ban_kinh │
│ │ │ │ hay │
│ │ │ │ dai, rong │
└────────────┴────────────┘ └────────────┘
chiếm cả hai chiếm phần lớn hơn

Nhãn loai nằm ngoài union, như tờ giấy dán cửa
nói hiện giờ phòng đang được dùng theo biển nào.

7. Sắp xếp mảng struct​

qsort trong <stdlib.h>:

void qsort(void *base, size_t n, size_t size,
int (*cmp)(const void *, const void *));

Hàm so sánh trả âm, 0, hoặc dương. Bên trong phải đổi const void * về const SinhVien *.

int theo_ma(const void *p, const void *q)
{
const SinhVien *a = p;
const SinhVien *b = q;
if (a->ma < b->ma) {
return -1;
}
if (a->ma > b->ma) {
return 1;
}
return 0;
}

Đừng return a->ma - b->ma khi ma là int có thể âm hoặc sát biên: phép trừ có thể tràn. So sánh ba nhánh an toàn hơn.

qsort không hứa ổn định. Hai sinh viên cùng điểm có thể đổi thứ tự tương đối. Nếu cần ổn định, hãy thêm khóa phụ (mã số) trong hàm so sánh.


8. Ví dụ​

Ví dụ 8.1 — Giờ, phút, giây và khoảng cách​

Phân tích. Một thời điểm trong ngày là số giây từ 0 đến 86399, nhưng người dùng nghĩ bằng giờ phút giây. Struct giữ ba thành viên. Mọi phép tính đi qua số giây để khỏi trừ có nhớ ở mười chỗ.

Tiền điều kiện: mỗi thành viên trong miền của nó. Hàm hop_le là cửa ngõ.

Lưu đồ đổi sang giây.

Mã nguồn.

#include <stdio.h>

typedef struct {
int gio;
int phut;
int giay;
} ThoiGian;

int hop_le(ThoiGian t)
{
return t.gio >= 0 && t.gio <= 23
&& t.phut >= 0 && t.phut <= 59
&& t.giay >= 0 && t.giay <= 59;
}

int thanh_giay(ThoiGian t)
{
if (!hop_le(t)) {
return -1;
}
return t.gio * 3600 + t.phut * 60 + t.giay;
}

ThoiGian tu_giay(int s)
{
ThoiGian t;
t.gio = s / 3600;
t.phut = (s % 3600) / 60;
t.giay = s % 60;
return t;
}

int main(void)
{
ThoiGian a = {8, 30, 0};
ThoiGian b = {10, 5, 15};
int da = thanh_giay(a);
int db = thanh_giay(b);
ThoiGian lech = tu_giay(db - da);

printf("Khoang cach: %02d:%02d:%02d\n", lech.gio, lech.phut, lech.giay);
return 0;
}

Kết quả.

Khoang cach: 01:35:15

Từ 8:30:00 đến 10:05:15 là 1 giờ 35 phút 15 giây.

Phân tích chi tiết. Trả struct theo giá trị là hợp lệ. tu_giay tạo một struct cục bộ và return nó; trình biên dịch sao chép ra nơi gọi. Không trả con trỏ tới t cục bộ. Nếu b sớm hơn a, db - da âm và tu_giay không còn đúng hợp đồng “s không âm”. Bản đầy đủ phải lấy trị tuyệt đối hoặc đi vòng qua nửa đêm bằng (db - da + 86400) % 86400.


Ví dụ 8.2 — Phân số tối giản​

Phân tích. Phân số có tử và mẫu. Mẫu khác 0. Dấu nằm ở tử. Rút gọn bằng UCLN. Cộng hai phân số: a/b + c/d = (ad + bc) / bd, rồi rút gọn.

Lưu đồ cộng.

Quy ước lỗi 0/0 phải được ghi trong hợp đồng. Người gọi kiểm tra mẫu bằng 0.

Mã nguồn.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
long long tu;
long long mau;
} PhanSo;

long long ucln(long long a, long long b)
{
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return a;
}

PhanSo rut_gon(PhanSo p)
{
long long g;
if (p.mau < 0) {
p.tu = -p.tu;
p.mau = -p.mau;
}
if (p.tu == 0) {
p.mau = 1;
return p;
}
g = ucln(p.tu, p.mau);
p.tu /= g;
p.mau /= g;
return p;
}

PhanSo cong(PhanSo a, PhanSo b)
{
PhanSo kq;
if (a.mau == 0 || b.mau == 0) {
kq.tu = 0;
kq.mau = 0;
return kq;
}
kq.tu = a.tu * b.mau + b.tu * a.mau;
kq.mau = a.mau * b.mau;
return rut_gon(kq);
}

int main(void)
{
PhanSo a = {1, 2};
PhanSo b = {1, 3};
PhanSo c = cong(a, b);
printf("%lld/%lld\n", c.tu, c.mau);
return 0;
}

Kết quả.

5/6

Phân tích chi tiết. 1/2 + 1/3 = 5/6. Dấu mẫu được đẩy lên tử trong rut_gon, nên -1/-2 thành 1/2. Nhân a.tu * b.mau có thể tràn long long. Bản cao thủ kiểm tra trước khi nhân, cùng kỹ thuật Bài 3. In 0/0 là tín hiệu lỗi, không phải phân số. Đừng rút gọn khi mẫu bằng 0.


Ví dụ 8.3 — Số phức​

Phân tích. z = a + b i. Cộng từng phần. Nhân: (a+bi)(c+di) = (ac - bd) + (ad + bc) i.

Lưu đồ nhân.

Mã nguồn.

#include <stdio.h>

typedef struct {
double thuc;
double ao;
} SoPhuc;

SoPhuc cong_sp(SoPhuc x, SoPhuc y)
{
SoPhuc z;
z.thuc = x.thuc + y.thuc;
z.ao = x.ao + y.ao;
return z;
}

SoPhuc nhan_sp(SoPhuc x, SoPhuc y)
{
SoPhuc z;
z.thuc = x.thuc * y.thuc - x.ao * y.ao;
z.ao = x.thuc * y.ao + x.ao * y.thuc;
return z;
}

void in_sp(SoPhuc z)
{
printf("%.2f %c %.2fi\n", z.thuc, z.ao < 0 ? '-' : '+', z.ao < 0 ? -z.ao : z.ao);
}

int main(void)
{
SoPhuc x = {1, 2};
SoPhuc y = {3, -4};
in_sp(cong_sp(x, y));
in_sp(nhan_sp(x, y));
return 0;
}

Kết quả.

4.00 - 2.00i
11.00 + 2.00i

Nhân tay: phần thực 1*3 - 2*(-4) = 11, phần ảo 1*(-4) + 2*3 = 2.

Phân tích chi tiết. in_sp tách dấu để khỏi in + -4.00i. Hàm in là hiệu ứng; hàm nhân là thuần. Giữ sự tách này khi làm bài tập mảng số phức: một hàm tính, một hàm in.


Ví dụ 8.4 — Danh sách sinh viên​

Phân tích. Tối đa 100 sinh viên. Mỗi người có mã, tên một từ, điểm giữa kỳ, điểm cuối kỳ. Việc cần làm: nhập, in, tìm theo mã, in thủ khoa theo điểm tổng kết 0.4 * giua + 0.6 * cuoi. Điểm ngoài [0, 10] bị từ chối từng người.

Lưu đồ tìm thủ khoa.

Mã nguồn.

#include <stdio.h>

typedef struct {
int ma;
char ten[40];
double giua;
double cuoi;
} SinhVien;

double tong_ket(SinhVien sv)
{
return 0.4 * sv.giua + 0.6 * sv.cuoi;
}

int tim_ma(const SinhVien *ds, int n, int ma)
{
for (int i = 0; i < n; i++) {
if (ds[i].ma == ma) {
return i;
}
}
return -1;
}

int thu_khoa(const SinhVien *ds, int n)
{
int vi = 0;
if (n <= 0) {
return -1;
}
for (int i = 1; i < n; i++) {
if (tong_ket(ds[i]) > tong_ket(ds[vi])) {
vi = i;
}
}
return vi;
}

int main(void)
{
SinhVien ds[3] = {
{1, "An", 7.0, 8.0},
{2, "Binh", 9.0, 9.5},
{3, "Chi", 8.0, 7.0}
};
int vi = thu_khoa(ds, 3);
int tim = tim_ma(ds, 3, 3);

printf("Thu khoa: %s %.2f\n", ds[vi].ten, tong_ket(ds[vi]));
printf("Ma 3 tai chi so %d\n", tim);
return 0;
}

Kết quả.

Thu khoa: Binh 9.30
Ma 3 tai chi so 2

Điểm Bình: 0.4*9 + 0.6*9.5 = 3.6 + 5.7 = 9.3. An: 0.4*7 + 0.6*8 = 7.6. Chi: 0.4*8 + 0.6*7 = 7.4.

Phân tích chi tiết. tim_ma trả chỉ số, không trả con trỏ, để bài này khỏi đụng đời sống con trỏ trỏ vào mảng cục bộ. -1 là “không có”. Thủ khoa hòa điểm giữ người đứng trước vì điều kiện là >, không phải >=. Muốn người có mã nhỏ hơn thắng khi hòa điểm, hãy thêm tiêu chí trong if.


Ví dụ 8.5 — qsort theo điểm rồi theo mã​

Phân tích. Sắp giảm dần theo điểm tổng kết. Hòa điểm thì mã tăng dần. In lại danh sách.

Lưu đồ hàm so sánh. qsort gọi hàm này nhiều lần. Mỗi lần chỉ trả lời “ai đứng trước”.

Mã nguồn.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
int ma;
char ten[40];
double giua;
double cuoi;
} SinhVien;

double tong_ket(SinhVien sv)
{
return 0.4 * sv.giua + 0.6 * sv.cuoi;
}

int so_sanh(const void *p, const void *q)
{
const SinhVien *a = p;
const SinhVien *b = q;
double da = tong_ket(*a);
double db = tong_ket(*b);
if (da < db) {
return 1;
}
if (da > db) {
return -1;
}
if (a->ma < b->ma) {
return -1;
}
if (a->ma > b->ma) {
return 1;
}
return 0;
}

int main(void)
{
SinhVien ds[3] = {
{3, "Chi", 8.0, 8.0},
{1, "An", 8.0, 8.0},
{2, "Binh", 5.0, 6.0}
};
qsort(ds, 3, sizeof ds[0], so_sanh);
for (int i = 0; i < 3; i++) {
printf("%d %s %.2f\n", ds[i].ma, ds[i].ten, tong_ket(ds[i]));
}
return 0;
}

Kết quả.

1 An 8.00
3 Chi 8.00
2 Binh 5.60

An và Chi cùng 8.0; mã 1 đứng trước mã 3 vì khóa phụ. Bình 5.6 đứng cuối vì sắp giảm theo điểm.

Phân tích chi tiết. return 1 khi da < db nghĩa là a phải đứng sau b trong thứ tự tăng mà qsort giả định — tức danh sách hiện ra giảm theo điểm. Hàm so sánh không được sửa dữ liệu. Nó có thể được gọi nhiều lần. Đừng printf bên trong nó để “xem cho vui” rồi quên xóa: vừa làm chậm vừa làm rối kết quả nếu bạn so sánh có hiệu ứng.


Ví dụ 8.6 — Diện tích hình bằng union phân biệt​

Phân tích. Một hình hoặc là tròn hoặc là chữ nhật. Diện tích phụ thuộc nhãn. Sai nhãn là lỗi logic, không phải lỗi cú pháp.

Lưu đồ.

Mã nguồn.

#include <stdio.h>

typedef enum {
HINH_TRON,
HINH_CHU_NHAT
} LoaiHinh;

typedef struct {
LoaiHinh loai;
union {
double ban_kinh;
struct {
double dai;
double rong;
} cn;
} so_do;
} Hinh;

double dien_tich(Hinh h)
{
if (h.loai == HINH_TRON) {
double r = h.so_do.ban_kinh;
return 3.141592653589793 * r * r;
}
if (h.loai == HINH_CHU_NHAT) {
return h.so_do.cn.dai * h.so_do.cn.rong;
}
return -1.0;
}

int main(void)
{
Hinh tron = {.loai = HINH_TRON};
Hinh cn = {.loai = HINH_CHU_NHAT};
tron.so_do.ban_kinh = 1.0;
cn.so_do.cn.dai = 2.0;
cn.so_do.cn.rong = 3.0;
printf("Tron: %.4f\n", dien_tich(tron));
printf("Chu nhat: %.4f\n", dien_tich(cn));
return 0;
}

Kết quả.

Tron: 3.1416
Chu nhat: 6.0000

Phân tích chi tiết. Gán ban_kinh ghi đè cùng vùng nhớ với dai và rong. Nếu quên cập nhật loai mà vẫn để HINH_TRON sau khi ghi chiều dài, dien_tich sẽ diễn giải các byte đó như bán kính và ra một số rất lạ, vẫn “chạy”. Nhãn là một phần của dữ liệu, không phải chú thích.


9. Bài tập có gợi ý​

Bài 8.G1 — Ngày tháng​

Struct Ngay gồm ngày, tháng, năm. Hàm hop_le dùng quy tắc nhuận ở Bài 4. Hàm ngay_tiep trả về ngày hôm sau, sang tháng và sang năm khi cần.

Gợi ý. Đừng cộng 1 vào ngày rồi vá mười trường hợp rời. Hãy tính số ngày tối đa của tháng hiện tại. Nếu vượt, đưa về ngày 1 tháng sau.

Bài 8.G2 — Điểm trong mặt phẳng​

Struct Diem với double x, y. Khoảng cách hai điểm. Diện tích tam giác qua công thức Heron hoặc nửa giá trị tuyệt đối của tích có hướng. Ba điểm thẳng hàng nếu diện tích gần 0.

Gợi ý. Sai số tương đối hoặc tuyệt đối phải được ghi trong hợp đồng, đừng dùng == 0.

Bài 8.G3 — Mảng phân số​

Nhập n phân số, in tổng và phân số lớn nhất. So sánh a/b với c/d bằng a*d và c*b sau khi đã chuẩn hóa dấu, cẩn thận tràn.

Gợi ý. So sánh bằng nhân chéo trong long long. Nếu lo tràn, rút gọn trước khi so.

Bài 8.G4 — Sắp sinh viên​

Dùng qsort. Khóa chính là điểm tổng kết giảm, khóa phụ là mã tăng.

Gợi ý. Nhìn Ví dụ 8.5. Bộ kiểm thử phải có hai người cùng điểm.

Bài 8.G5 — Đo sizeof​

In sizeof của struct có char rồi int, và struct xếp int rồi char. Giải thích nếu hai số khác nhau.

Gợi ý. Vẽ từng byte trên giấy, đánh dấu byte đệm. Đừng ngạc nhiên nếu một trình biên dịch cho kết quả khác một trình biên dịch khác ở struct phức tạp; hãy mô tả đúng máy bạn.

Bài 8.G6 — Đừng sao chép nông​

Tạo struct có char *ten trỏ tới bộ nhớ malloc. Gán struct này cho struct kia. free một bên rồi in tên bên còn lại. Quan sát lỗi. Sau đó sửa thiết kế: hoặc mảng char ten[40] nằm trong struct, hoặc hàm sao chép sâu cấp phát vùng mới.

Gợi ý. Đây là bài quan sát. Chạy dưới debugger nếu được. Bản nộp cuối phải hết lỗi dùng sau free.


10. Bài tập tự làm​

Cơ bản​

  1. Struct phân số: nhập, in, rút gọn, cộng, trừ, nhân, chia. Chia cho phân số 0 phải bị từ chối.
  2. Struct số phức: cộng, trừ, nhân, và mô-đun sqrt(a*a + b*b).
  3. Struct thời gian: nhập, kiểm tra, cộng thêm một số giây, in hh:mm:ss.
  4. Struct điểm: khoảng cách tới gốc tọa độ, điểm đối xứng qua trục hoành.

Vững​

  1. Danh sách tối đa 100 nhân viên: mã, tên, lương long long. In người lương cao nhất, người lương thấp nhất, lương trung bình. Từ chối lương âm.
  2. Sắp nhân viên theo lương tăng, hòa lương thì theo mã tăng, bằng qsort.
  3. Struct HonSo gồm phần nguyên và phân số tối giản. Viết đổi hỗn số sang phân số và ngược lại.
  4. Mảng tối đa 50 hình chữ nhật. Mỗi hình có mã, chiều dài, chiều rộng. In hình có diện tích lớn nhất và những hình là hình vuông.

Nâng cao​

  1. Ngày trong năm. Đổi Ngay sang thứ tự ngày trong năm (1..365 hoặc 366) và ngược lại. Dùng mảng số ngày mỗi tháng, tháng 2 phụ thuộc năm nhuận. Viết bảng kiểm thử tối thiểu: 1/1, 31/12, 29/2 năm nhuận, 1/3 năm không nhuận.
  2. Đa thức bậc không quá 20. Struct gồm bậc và mảng hệ số double he_so[21]. Cộng hai đa thức, nhân hai đa thức. In theo kiểu 3.00x^2 + 1.00, bỏ hạng tử 0, xử lý bậc 0.
  3. Union gói tin. Một bộ đệm 8 byte mô tả hai loại gói: loại 1 chứa một int mã lệnh và một int tham số; loại 2 chứa một double số đo. Struct bọc ngoài có trường loai. Viết hàm in. Viết một ca cố ý đọc sai thành viên và giải thích vì sao kết quả không đáng dùng, dù chương trình không báo lỗi biên dịch.

Siêu khó, có ứng dụng​

  1. Sổ liên lạc mini. Tối đa 200 sinh viên, mỗi người tối đa 10 điểm thành phần. Struct lồng nhau:

    DiemMon { char mon[30]; double diem; }
    SinhVien { int ma; char ten[40]; DiemMon mon[10]; int so_mon; }

    Chức năng: thêm sinh viên, thêm điểm cho một mã, in bảng điểm một mã, in điểm trung bình mỗi sinh viên, tìm thủ khoa theo trung bình. Từ chối khi đầy. Không dùng mảng song song. Viết hợp đồng cho từng hàm: ai sở hữu dữ liệu, lỗi nào trả về mã nào. Bộ kiểm thử ít nhất 10 ca gồm danh sách rỗng, thêm quá sức chứa, mã không tồn tại, sinh viên chưa có môn.

  2. Sổ giao dịch tài khoản. TaiKhoan có số tài khoản, chủ tài khoản, số dư long long đơn vị đồng. GiaoDich có loại GUI hoặc RUT, số tiền, và struct thời gian. Một tài khoản giữ mảng tối đa 100 giao dịch. Rút không vượt số dư. Mọi thay đổi số dư đi qua hàm gui và rut, không sửa trường số dư từ main. In sao kê theo thời gian nhập vào (coi như đã đúng thứ tự; nếu muốn sắp, dùng qsort với khóa thời gian đổi ra giây). Đây là bài tổng hợp hàm, mảng, struct, rẽ nhánh và tính đúng của số nguyên tiền tệ.

  3. Lịch sử làm việc của một thiết bị. Mỗi bản ghi: mã thiết bị, enum trạng thái TAT, CHAY, LOI, số giây hoạt động, và một union: khi LOI thì có mã lỗi int, khi CHAY thì có nhiệt độ double, khi TAT thì không có số đo. Viết hàm mo_ta in đúng trường của đúng trạng thái. Viết hàm đếm số bản ghi LOI trong mảng. Viết hàm tìm nhiệt độ cao nhất trong các bản ghi đang CHAY. Nêu bằng một đoạn văn vì sao nhét cả ma_loi và nhiet_do thành hai trường thường vẫn đúng, và union mua được điều gì, mất điều gì.


11. Câu hỏi tự kiểm​

  1. Vì sao không so sánh hai struct bằng ==?
  2. . và -> khác nhau khi nào?
  3. Truyền struct theo giá trị có sửa được biến nơi gọi không?
  4. Vì sao sizeof struct có thể lớn hơn tổng các thành viên?
  5. union khác struct về bộ nhớ thế nào?
  6. Hàm so sánh của qsort trả về số nào thì phần tử đầu đứng trước?

12. Tóm tắt cả khóa​

Cấu trúc biến nhiều dữ liệu liên quan thành một giá trị có tên. Mảng cấu trúc là danh sách hồ sơ. Hàm nhận giá trị khi muốn sự tách bạch, nhận con trỏ khi muốn sửa hoặc tránh sao chép lớn. enum đặt tên trạng thái. union dùng chung vùng nhớ và chỉ an toàn khi có nhãn. qsort sắp được bất kỳ mảng nào nếu bạn đưa đúng kích thước phần tử và một quan hệ thứ tự.

Tám bài, nếu làm đủ bài tập siêu khó chứ không chỉ đọc ví dụ, phủ kín nhập môn và chạm tới cách người làm C thật sự viết:

  1. Chương trình là mã nguồn được dịch và nạp thành tiến trình, không phải chữ trên màn hình IDE.
  2. Thuật toán có tiền điều kiện, hậu điều kiện, bất biến và bậc chi phí trước khi có if.
  3. Kiểu, biểu thức, nhập xuất và bit là hợp đồng với máy, không phải ký hiệu trang trí.
  4. Nhánh diễn tả đặc tả loại trừ, đúng thứ tự, đúng đoản mạch.
  5. Vòng lặp phải tiến tới điểm dừng và đúng biên.
  6. Hàm là đơn vị kiểm thử được; đệ quy và con trỏ hàm là công cụ, không phải phong cách.
  7. Mảng là vùng nhớ liền nhau; chuỗi là mảng có '\0'; biên và free là trách nhiệm của bạn.
  8. Struct là cách nói “những thứ này là một đối tượng”.

Cao thủ C không phải người thuộc nhiều mẹo. Cao thủ là người chỉ ra được tiền điều kiện, biết chỗ nào là hành vi không xác định, đo được chi phí, và để chương trình còn đọc được sau sáu tháng.


13. Đào sâu: struct là một ngôi nhà có nhiều phòng​

Mảng là nhiều phòng cùng kiểu. Struct là một căn nhà gồm các phòng khác kiểu, mang một địa chỉ gốc. sv.ma là phòng mã. &sv là địa chỉ cả căn. &sv.ma là địa chỉ riêng phòng mã, kiểu int *, và đó chính là con trỏ Bài 3 trỏ vào một thành viên.

Truyền SinhVien sv là photo cả căn nhà cho hàm. Hàm sửa bản photo thì căn gốc không đổi. Truyền SinhVien *sv là đưa số nhà. sv->ma = 1 sửa đúng căn gốc. Mũi tên là * rồi .: đi tới căn, rồi vào phòng.

Gán a = b photo được vì struct là giá trị. So a == b không được vì ngôn ngữ không chọn giúp bạn "bằng" nghĩa là bằng từng phòng hay bằng mã. Hãy viết hàm.

union là nhiều cách gọi tên cùng một phòng nhớ. Không có phép màu. Nhãn enum phải do bạn giữ cho khớp với cách đang dùng. Padding là các ngăn trống trình biên dịch chèn để phòng int bắt đầu ở địa chỉ chia hết cho 4. sizeof nhìn thấy cả ngăn trống.

Ví dụ 8.7 — Sửa điểm qua địa chỉ struct​

Phân tích. Hàm cap_nhat nhận SinhVien * và một điểm cuối kỳ mới trong đoạn 0..10. NULL hoặc điểm sai thì trả 0, không ghi. Hợp lệ thì ghi sv->cuoi và trả 1. main in trước và sau để thấy căn gốc đổi.

Lưu đồ.

Mã nguồn.

#include <stdio.h>

typedef struct {
int ma;
double cuoi;
} SinhVien;

int cap_nhat(SinhVien *sv, double diem)
{
if (sv == NULL || diem < 0.0 || diem > 10.0) {
return 0;
}
sv->cuoi = diem;
return 1;
}

int main(void)
{
SinhVien sv = {.ma = 1, .cuoi = 5.0};
printf("Truoc: %.1f\n", sv.cuoi);
if (!cap_nhat(&sv, 8.5)) {
return 1;
}
printf("Sau: %.1f\n", sv.cuoi);
printf("Diem am bi tu choi? %d\n", cap_nhat(&sv, -1.0));
printf("Van giu: %.1f\n", sv.cuoi);
return 0;
}

Kết quả.

Truoc: 5.0
Sau: 8.5
Diem am bi tu choi? 0
Van giu: 8.5

Phân tích chi tiết. &sv là địa chỉ cả struct. sv->cuoi viết tắt của (*sv).cuoi. Lần gọi điểm âm trả 0 và không vào lệnh ghi, nên 8.5 còn nguyên. Nếu hàm nhận SinhVien sv theo giá trị, dòng sv.cuoi = diem chỉ sửa bản photo và dòng "Sau" vẫn là 5.0. Hãy thử bản sai đó một lần để đóng đinh sự khác nhau.

14. Bài tập mở rộng​

Dễ​

  1. Struct PhanSo, hàm in và hàm rút gọn. Vẽ lưu đồ rút gọn gồm đẩy dấu lên tử.
  2. Struct Diem gồm x, y. Hàm in. Không cần khoảng cách.
  3. Khai báo một SinhVien bằng khởi tạo chỉ định, in từng thành viên.

Vừa​

  1. Mảng 5 phân số, in phân số lớn nhất bằng nhân chéo. Vẽ lưu đồ tìm max.
  2. Ngay và hàm hop_le. Tái sử dụng quy tắc nhuận. Ca 29/2/1900.
  3. Truyền struct điểm theo giá trị vào hàm khoang_cach_goc. Không sửa điểm. Vẽ lưu đồ.

Khó​

  1. Mảng nhân viên, qsort theo lương tăng, hòa lương thì theo mã. Vẽ lưu đồ hàm so sánh như Ví dụ 8.5.
  2. Struct lồng Lop chứa tên lớp và mảng tối đa 40 SinhVien cùng si_so. Hàm thêm sinh viên, từ chối khi đủ 40.
  3. In sizeof một struct có char đứng trước int. Vẽ sơ đồ byte, đánh dấu byte đệm trên máy bạn.

Siêu khó​

  1. Đa thức bậc tối đa 10: struct { int bac; double a[11]; }. Hàm cộng. Vẽ lưu đồ cộng từng hệ số. In bỏ hạng tử 0.
  2. Union hình tròn hoặc chữ nhật như Ví dụ 8.6, thêm hình vuông. Hàm diện tích có default trả -1. Một ca cố ý sai nhãn, giải thích kết quả vô nghĩa.

Dự án thật​

  1. Sổ đoàn phí. Mỗi đoàn viên là một struct: mã, tên một từ, số tiền đã đóng long long, cờ đã đủ hay chưa. Mảng tối đa 100. Các hàm: thêm, ghi nhận một lần đóng thêm tiền, in danh sách chưa đủ mức quy định, in tổng đã thu. Mức quy định nhập một lần ở đầu chương trình. Cấm main sửa từng trường tiền; mọi cộng tiền đi qua một hàm nhận DoanVien *. Vẽ lưu đồ hàm cộng tiền, gồm từ chối số âm và con trỏ NULL. Nộp ít nhất 8 ca: danh sách rỗng, đóng quá một người, người không tồn tại, tiền âm, vừa đủ mức, danh sách đầy.

Bộ 25 bài tập​

  1. Khai báo một SinhVien bằng khởi tạo chỉ định và in từng thành viên. Gợi ý: Dùng .ma = và .ten = trong dấu ngoặc khởi tạo.
  2. Struct điểm x, y. Hàm in điểm. Gợi ý: printf hai thành viên. Chưa cần khoảng cách.
  3. Struct thời gian. Hàm kiểm tra hợp lệ và hàm đổi sang số giây. Gợi ý: Hợp lệ: giờ 0..23, phút và giây 0..59. Số giây = giờ3600 + phút60 + giây.
  4. Struct phân số. In và rút gọn. Đẩy dấu lên tử. Gợi ý: UCLN của trị tuyệt đối tử và mẫu. Mẫu âm thì đổi dấu cả hai.
  5. Cộng hai phân số 1/2 và 1/3, kết quả phải là 5/6. Gợi ý: Tử mới = ad + bc, mẫu mới = bd, rồi rút gọn.
  6. Số phức: cộng và nhân. Ca (1+2i) nhân (3-4i). Gợi ý: Phần thực ac-bd, phần ảo ad+bc.
  7. Vẽ sơ đồ byte của struct có char đứng trước int trên máy bạn. Gợi ý: Một byte cho char, vài byte trống, rồi int. sizeof thường lớn hơn 5.
  8. Viết một hàm nhận SinhVien theo giá trị, sửa điểm trong hàm, chứng minh bản gốc không đổi. Gợi ý: Hàm nhận SinhVien sv rồi sv.diem = 10. In lại biến ở main.
  9. Viết lại hàm đó nhận con trỏ. Bản gốc phải đổi. Ca NULL không ghi. Gợi ý: Hàm nhận SinhVien *sv. Ghi sv->diem. Đầu hàm nếu sv == NULL thì return.
  10. Mảng 5 phân số. In phân số lớn nhất bằng nhân chéo. Gợi ý: So sánh ad ? cb bằng nhân chéo sau khi đã chuẩn hóa dấu.
  11. Ngay hợp lệ, có năm nhuận. Ca 29/2/1900 không hợp lệ, 29/2/2000 hợp lệ. Gợi ý: Tách hàm nhuận. 1900 chia hết 100 nhưng không chia hết 400.
  12. Khoảng cách từ một điểm tới gốc tọa độ. Hàm không sửa điểm. Gợi ý: sqrt(xx + yy). Tham số truyền theo giá trị nên gốc không đổi.
  13. Hỗn số đổi sang phân số và ngược lại. Gợi ý: Hỗn số a b/c thành phân số (a*c+b)/c. Chiều ngược lấy nguyên a = tu/mau.
  14. Mảng tối đa 50 hình chữ nhật. In hình diện tích lớn nhất và các hình vuông. Gợi ý: Diện tích = dài * rộng. Vuông khi dài bằng rộng và cả hai dương.
  15. Nhân viên: mã, tên, lương long long. In lương cao nhất, thấp nhất, trung bình. Từ chối lương âm. Gợi ý: Một vòng tìm max, một vòng tìm min. Lương âm bỏ qua hoặc từ chối lúc nhập.
  16. qsort nhân viên theo lương tăng, hòa lương thì theo mã tăng. Gợi ý: Hàm so sánh: lương khác thì so lương, lương bằng thì so mã. Tránh trừ hai int sát biên.
  17. Lớp học lồng struct: tối đa 40 sinh viên, hàm thêm, từ chối khi đầy. Gợi ý: si_so == 40 thì từ chối. Ngược lại ghi vào mang[si_so] rồi tăng si_so.
  18. Đa thức bậc tối đa 10. Cộng hai đa thức và in, bỏ hạng tử 0. Gợi ý: Cộng hệ số cùng bậc. Khi in, bỏ hệ số 0 và xử lý dấu cộng.
  19. Ngày trong năm và ngược lại. Ca 1/1, 31/12, 29/2 năm nhuận, 1/3 năm không nhuận. Gợi ý: Mảng ngày của 12 tháng, tháng 2 cộng thêm 1 nếu nhuận.
  20. Union hình tròn, chữ nhật, hình vuông. Diện tích theo nhãn. Một ca cố ý sai nhãn, giải thích kết quả. Gợi ý: Đọc trường nào tùy enum. Sai nhãn vẫn chạy nhưng số in ra không có nghĩa.
  21. Gói tin 8 byte: loại 1 là hai int, loại 2 là một double. In đúng theo nhãn. Gợi ý: Nhãn nằm ngoài union. Đọc đúng thành viên đã ghi.
  22. Sổ liên lạc. Mỗi sinh viên tối đa 10 điểm môn. Thêm sinh viên, thêm điểm, in bảng một mã, in điểm trung bình, tìm thủ khoa. Từ chối khi đầy. Gợi ý: Struct môn nằm trong struct sinh viên. Trung bình chia cho so_mon, so_mon bằng 0 thì không chia.
  23. Sổ tài khoản. Số dư long long. Gửi và rút qua hàm, rút không vượt số dư. Mỗi giao dịch kèm thời gian. In sao kê. Gợi ý: rut kiểm tra số tiền và số dư trước khi trừ. main không gán số dư.
  24. Nhật ký thiết bị. Trạng thái tắt, chạy, lỗi. Union: mã lỗi hoặc nhiệt độ tùy trạng thái. Đếm bản ghi lỗi và tìm nhiệt độ cao nhất lúc đang chạy. Gợi ý: Chỉ đọc nhiệt độ khi trạng thái là chạy. Chỉ đọc mã lỗi khi trạng thái là lỗi.
  25. Sổ đoàn phí. Tối đa 100 đoàn viên. Thêm, đóng thêm tiền, in người chưa đủ mức, in tổng đã thu. Mọi cộng tiền đi qua hàm nhận con trỏ. Tám ca gồm danh sách rỗng, tiền âm, và danh sách đầy. Gợi ý: Hàm cong_tien(DoanVien *p, long long tien). p == NULL hoặc tien < 0 thì không ghi.