Con trỏ struct
Sau bài này bạn sẽ làm được
- Dùng toán tử mũi tên thành thạo
- Chọn giữa truyền theo giá trị và truyền con trỏ
- Cài mẫu kiểu mờ để giấu chi tiết bên trong
- Tránh các lỗi truy cập trường qua con trỏ rỗng
Struct và con trỏ gặp nhau ở gần như mọi dòng mã C thật. Bài này gom lại cú pháp, cách truyền hiệu quả, và một mẫu thiết kế quan trọng: giấu hoàn toàn nội dung struct khỏi người dùng thư viện.
#Toán tử mũi tên
typedef struct {
char ten[64];
int tuoi;
double diem;
} SinhVien;
SinhVien a = { .ten = "An", .tuoi = 20, .diem = 8.5 };
SinhVien *p = &a;
/* Ba cách viết, ba mức dễ đọc */
printf("%d\n", a.tuoi); /* trực tiếp */
printf("%d\n", (*p).tuoi); /* qua con trỏ, đúng nhưng rườm rà */
printf("%d\n", p->tuoi); /* dạng viết tắt, luôn dùng cách này */p->truong là viết tắt hoàn toàn tương đương của (*p).truong. Nó giải tham chiếu con trỏ rồi lấy trường, trong một bước.Nhiều tầng
typedef struct { int x, y; } Diem;
typedef struct { Diem goc; Diem *dich; } DoanThang;
DoanThang d;
DoanThang *p = &d;
d.goc.x; /* struct trong struct: dùng dấu chấm cả hai lần */
p->goc.x; /* con trỏ rồi struct: mũi tên rồi chấm */
p->dich->x; /* con trỏ rồi con trỏ: mũi tên cả hai lần */
(*(*p).dich).x; /* cùng nghĩa với dòng trên, và đây là lý do có mũi tên */Gán và tăng qua con trỏ
p->tuoi = 21;
p->tuoi++;
++p->tuoi; /* mũi tên ưu tiên cao hơn ++ tiền tố, nên đây là ++(p->tuoi) */
p->diem += 0.5;
strcpy(p->ten, "Binh");
snprintf(p->ten, sizeof p->ten, "Binh"); /* an toàn hơn */
/* Cẩn thận: hai cái này khác nhau */
p++->tuoi; /* lấy trường tuoi của p HIỆN TẠI rồi tăng p */
(++p)->tuoi; /* tăng p trước rồi mới lấy trường */#Truyền struct vào hàm
| Nguyên mẫu | Sao chép | Hàm sửa được | Dùng khi |
|---|---|---|---|
| void f(SinhVien sv) | Cả struct | Không, chỉ bản sao | Struct nhỏ, và bạn muốn bản sao độc lập |
| void f(const SinhVien *sv) | 8 byte | Không, trình biên dịch chặn | Mặc định cho hàm chỉ đọc |
| void f(SinhVien *sv) | 8 byte | Có | Hàm cần sửa dữ liệu của người gọi |
| void f(SinhVien **sv) | 8 byte | Đổi được cả con trỏ | Hàm cấp phát hoặc giải phóng |
/* Chỉ đọc: dùng const con trỏ */
void in_sv(const SinhVien *sv)
{
if (sv == NULL) return;
printf("%-12s %3d %.1f\n", sv->ten, sv->tuoi, sv->diem);
}
/* Có sửa: bỏ const */
void tang_tuoi(SinhVien *sv)
{
if (sv == NULL) return;
sv->tuoi++;
}
/* Cần bản sao độc lập: truyền theo giá trị */
SinhVien voi_diem_moi(SinhVien sv, double diem)
{
sv.diem = diem; /* chỉ sửa bản sao */
return sv; /* trả về bản đã sửa, bản gốc không đổi */
}theo gia tri : 0.412 s con tro : 0.089 s ti le : 4.6 lan
void in_sv(SinhVien sv); /* chép 80 byte chỉ để đọc */
void in_sv(SinhVien *sv); /* thiếu const, người gọi không biết có bị sửa không */
void in_sv(SinhVien *sv)
{
printf("%s\n", sv->ten); /* thiếu kiểm tra NULL */
}void in_sv(const SinhVien *sv)
{
if (sv == NULL) return;
printf("%s\n", sv->ten);
}#Kiểu mờ để giấu chi tiết
#ifndef DANHSACH_H
#define DANHSACH_H
#include <stddef.h>
/* Khai báo trước: kiểu tồn tại, nhưng nội dung nằm ở nơi khác. */
typedef struct DanhSach DanhSach;
DanhSach *ds_tao(void);
void ds_huy(DanhSach *ds);
int ds_them(DanhSach *ds, int gia_tri);
int ds_lay(const DanhSach *ds, size_t i, int *ra);
size_t ds_so_luong(const DanhSach *ds);
#endif#include <stdlib.h>
#include "danhsach.h"
/* Nội dung thật chỉ có ở file này. */
struct DanhSach {
int *du_lieu;
size_t n;
size_t suc_chua;
};
DanhSach *ds_tao(void)
{
return calloc(1, sizeof(DanhSach));
}
void ds_huy(DanhSach *ds)
{
if (ds == NULL) return;
free(ds->du_lieu);
free(ds);
}
int ds_them(DanhSach *ds, int gia_tri)
{
if (ds == NULL) return -1;
if (ds->n == ds->suc_chua) {
size_t moi = ds->suc_chua ? ds->suc_chua * 2 : 4;
int *tam = realloc(ds->du_lieu, moi * sizeof *tam);
if (tam == NULL) return -1;
ds->du_lieu = tam;
ds->suc_chua = moi;
}
ds->du_lieu[ds->n++] = gia_tri;
return 0;
}
int ds_lay(const DanhSach *ds, size_t i, int *ra)
{
if (ds == NULL || ra == NULL || i >= ds->n) return -1;
*ra = ds->du_lieu[i];
return 0;
}
size_t ds_so_luong(const DanhSach *ds)
{
return ds != NULL ? ds->n : 0;
}#include <stdio.h>
#include "danhsach.h"
int main(void)
{
DanhSach *ds = ds_tao();
if (ds == NULL) return 1;
for (int i = 0; i < 10; ++i)
if (ds_them(ds, i * i) != 0) { ds_huy(ds); return 1; }
for (size_t i = 0; i < ds_so_luong(ds); ++i) {
int v;
if (ds_lay(ds, i, &v) == 0) printf("%d ", v);
}
printf("\n");
ds_huy(ds);
return 0;
}0 1 4 9 16 25 36 49 64 81
Biến thể: giấu một phần
/* Phần công khai người dùng đọc được, phần riêng thì không. */
typedef struct {
int rong, cao; /* công khai, đọc thoải mái */
void *rieng; /* con trỏ tới phần chỉ thư viện biết */
} Hinh;Cách này cho phép truy cập nhanh những trường hay dùng mà vẫn giấu được phần phức tạp. Nhưng nó mất một phần lợi ích: đổi hai trường công khai vẫn buộc mọi người biên dịch lại.
#Bốn lỗi thường gặp
Lỗi 1: truy cập trường qua con trỏ rỗng
SinhVien *p = NULL;
p->tuoi = 20; /* ghi vào địa chỉ bằng đúng độ lệch của trường tuoi */Bài 14.5 đã nói: địa chỉ gây lỗi bằng đúng offsetof của trường. Nhìn con số trong thông báo lỗi là biết ngay trường nào.
Lỗi 2: trả về con trỏ tới struct cục bộ
SinhVien *tao_sv(void)
{
SinhVien sv = { .ten = "An", .tuoi = 20 };
return &sv; /* sv chết khi hàm kết thúc */
}/* Cách 1: trả về theo giá trị */
SinhVien tao_sv(void)
{
SinhVien sv = { .ten = "An", .tuoi = 20 };
return sv;
}
/* Cách 2: cấp phát động, người gọi giải phóng */
SinhVien *tao_sv(void)
{
SinhVien *sv = calloc(1, sizeof *sv);
if (sv == NULL) return NULL;
snprintf(sv->ten, sizeof sv->ten, "An");
sv->tuoi = 20;
return sv;
}
/* Cách 3: người gọi cấp chỗ */
int tao_sv(SinhVien *ra)
{
if (ra == NULL) return -1;
*ra = (SinhVien){ .tuoi = 20 };
snprintf(ra->ten, sizeof ra->ten, "An");
return 0;
}Lỗi 3: quên rằng gán struct là sao chép nông
typedef struct {
char *ten; /* con trỏ, không phải mảng */
int tuoi;
} Nguoi;
Nguoi a = { malloc(64), 20 };
strcpy(a.ten, "An");
Nguoi b = a; /* b.ten và a.ten là CÙNG một địa chỉ */
b.ten[0] = 'B';
printf("%s\n", a.ten); /* "Bn": sửa qua b thì a cũng đổi */
free(a.ten);
printf("%s\n", b.ten); /* b.ten treo */Bài 15.7 sẽ nói kỹ về sao chép nông và sao chép sâu. Điểm cần nhớ bây giờ: dấu bằng giữa hai struct chép từng byte, và byte của một con trỏ chính là địa chỉ chứ không phải nội dung.
Lỗi 4: dùng struct chưa hoàn chỉnh
typedef struct DanhSach DanhSach; /* chỉ khai báo, chưa định nghĩa */
DanhSach a; /* LỖI: storage size of 'a' isn't known */
DanhSach *p; /* HỢP LỆ: con trỏ tới kiểu chưa hoàn chỉnh vẫn được */
p->n; /* LỖI: dereferencing pointer to incomplete type */
sizeof(DanhSach); /* LỖI: invalid application of sizeof */#Con trỏ tới một trường
typedef struct {
char ten[64];
int tuoi;
double diem;
} SinhVien;
SinhVien sv = { .ten = "An", .tuoi = 20, .diem = 8.5 };
int *pt = &sv.tuoi; /* con trỏ tới một trường */
char *pn = sv.ten; /* trường mảng phân rã như thường */
double *pd = &sv.diem;
*pt = 21;
printf("%d\n", sv.tuoi); /* 21 */Đi ngược từ trường về struct
#include <stddef.h>
/* Từ con trỏ tới một trường, tính ra con trỏ tới cả struct.
Đây là macro container_of nổi tiếng của nhân Linux. */
#define CHUA_TRONG(ptr, kieu, truong) \
((kieu *)((char *)(ptr) - offsetof(kieu, truong)))
SinhVien sv = { .tuoi = 20 };
int *pt = &sv.tuoi;
SinhVien *ps = CHUA_TRONG(pt, SinhVien, tuoi);
printf("%d\n", ps->tuoi); /* 20, và ps == &sv */Tự làm thử
- Truy cập cùng một trường bằng cả ba cách trong bài, thử viết
*p.truongvà đọc thông báo lỗi. - In
sizeofcủap,*p,p->tenvàp->tuoi, giải thích bốn con số. - Đo thời gian truyền struct 80 byte theo giá trị và theo con trỏ, mỗi cách mười triệu lần.
- Cài đủ thư viện danh sách kiểu mờ trong bài, thử truy cập trường từ
main.cvà đọc thông báo lỗi. - Đổi cài đặt bên trong từ mảng động sang một cấu trúc khác, xác nhận
main.ckhông phải sửa gì. - Gán một struct chứa con trỏ cho struct khác, sửa qua bản sao và quan sát bản gốc cũng đổi.
- Cài macro
CHUA_TRONG, dùng nó để đi từ con trỏ trường về con trỏ struct và kiểm chứng hai địa chỉ khớp nhau.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
p->xlà viết tắt của(*p).x. Đừng viết*p.xvì dấu chấm ưu tiên cao hơn dấu sao.- Dùng
const T *làm mặc định cho tham số struct, bỏconstkhi hàm cần sửa, và truyền theo giá trị chỉ với struct tới khoảng mười sáu byte. sizeof p->mangcho ra kích thước thật của trường mảng, khác với tham số mảng của hàm.- Kiểu mờ giấu hoàn toàn nội dung struct, cho phép đổi cài đặt mà không ai phải sửa mã.
- Gán struct là sao chép nông: con trỏ bên trong được chép nguyên giá trị, nên hai bản cùng trỏ một chỗ.