Struct và hàm
Sau bài này bạn sẽ làm được
- Biết struct được truyền qua thanh ghi hay qua bộ nhớ
- Thiết kế bộ hàm thao tác trên một struct
- So sánh struct với hàm nhiều tham số
- Dùng struct làm gói tham số tùy chọn
Struct đổi cách bạn thiết kế hàm. Thay vì bảy tham số rời rạc, bạn có một đối tượng. Thay vì ba tham số ra, bạn trả về một struct. Bài này gom lại những mẫu đó, kèm phần cơ chế bên dưới để bạn biết khi nào chúng tốn kém.
#Truyền và trả về
typedef struct { double x, y; } Diem;
/* Truyền theo giá trị: hàm nhận bản sao */
double do_dai(Diem a, Diem b)
{
double dx = b.x - a.x;
double dy = b.y - a.y;
return sqrt(dx * dx + dy * dy);
}
/* Trả về theo giá trị */
Diem trung_diem(Diem a, Diem b)
{
Diem m = { (a.x + b.x) / 2, (a.y + b.y) / 2 };
return m;
}
/* Hoặc gọn hơn bằng hằng phức hợp */
Diem cong(Diem a, Diem b)
{
return (Diem){ a.x + b.x, a.y + b.y };
}
Diem p = { 0, 0 };
Diem q = { 3, 4 };
printf("%.1f\n", do_dai(p, q)); /* 5.0 */
Diem m = trung_diem(p, q);
printf("%.1f %.1f\n", m.x, m.y); /* 1.5 2.0 */| Cách | Chi phí | Dùng khi |
|---|---|---|
| void f(S s) | Chép cả struct | Struct nhỏ, và hàm muốn bản sao riêng để sửa |
| void f(const S *s) | Chép 8 byte | Mặc định cho hàm chỉ đọc struct lớn |
| void f(S *s) | Chép 8 byte | Hàm sửa struct của người gọi |
| S f(void) | Chép cả struct về | Struct nhỏ, hàm tạo giá trị mới |
| int f(S *ra) | Chép 8 byte, người gọi cấp chỗ | Struct lớn, hoặc cần báo lỗi riêng |
#Quy ước gọi hàm với struct
Trình biên dịch quyết định truyền struct qua thanh ghi hay qua bộ nhớ dựa vào kích thước và thành phần của nó. Biết ngưỡng giúp bạn chọn đúng mà không phải đoán.
| Kích thước struct | Truyền thế nào trên System V AMD64 |
|---|---|
| Tới 8 byte, toàn số nguyên | Một thanh ghi nguyên, ví dụ rdi |
| Tới 16 byte, toàn số nguyên | Hai thanh ghi nguyên |
| Tới 16 byte, toàn số thực | Một hoặc hai thanh ghi xmm |
| Lớn hơn 16 byte | Chép ra ngăn xếp, hàm nhận qua bộ nhớ |
| Có mảng bên trong | Luôn chép qua bộ nhớ, bất kể kích thước |
typedef struct { int x, y; } Nho; /* 8 byte, một thanh ghi */
typedef struct { double x, y; } Vua; /* 16 byte, hai thanh ghi xmm */
typedef struct { double x, y, z; } Lon; /* 24 byte, qua bộ nhớ */
typedef struct { char s[8]; } CoMang; /* 8 byte nhưng có mảng, qua bộ nhớ */
void f_nho(Nho a);
void f_vua(Vua a);
void f_lon(Lon a);f_nho:
; struct 8 byte nam gon trong rdi, khong cham bo nho
ret
f_lon:
; struct 24 byte da duoc nguoi goi chep ra ngan xep
; ham doc no qua [rsp+8]
retstruct 8 byte theo gia tri : 0.008 s struct 16 byte theo gia tri : 0.009 s struct 64 byte theo gia tri : 0.061 s struct 64 byte qua con tro : 0.010 s
#Struct làm gói tham số
int ve_hinh(int x, int y, int rong, int cao,
int mau_vien, int mau_nen, int do_day);
/* Chỗ gọi: bảy con số, không ai đoán được cái nào là cái nào */
ve_hinh(10, 20, 100, 50, 0xFF0000, 0x00FF00, 2);
/* Và đổi chỗ hai tham số cùng kiểu thì trình biên dịch im lặng */typedef struct {
int x, y;
int rong, cao;
int mau_vien;
int mau_nen;
int do_day;
} ThamSoVe;
int ve_hinh(const ThamSoVe *t);
/* Chỗ gọi: đọc là hiểu, và không thể nhầm thứ tự */
ve_hinh(&(ThamSoVe){
.x = 10, .y = 20,
.rong = 100, .cao = 50,
.mau_vien = 0xFF0000,
.mau_nen = 0x00FF00,
.do_day = 2,
});#Thiết kế giao diện quanh struct
#ifndef NGAN_XEP_H
#define NGAN_XEP_H
#include <stddef.h>
typedef struct NganXep NganXep; /* kiểu mờ, xem Bài 15.3 */
/* Vòng đời */
NganXep *nx_tao(size_t suc_chua_ban_dau);
void nx_huy(NganXep *nx);
/* Thao tác chính, luôn trả về mã lỗi */
int nx_day(NganXep *nx, int gia_tri);
int nx_lay(NganXep *nx, int *ra);
/* Truy vấn, luôn nhận const */
size_t nx_so_luong(const NganXep *nx);
int nx_rong(const NganXep *nx);
int nx_dinh(const NganXep *nx, int *ra);
#endif| Quy ước | Nội dung | Vì sao |
|---|---|---|
| 1 | Mọi hàm cùng tiền tố, ở đây là nx_ | Tránh đụng tên, và nhìn là biết thuộc nhóm nào |
| 2 | Tham số đầu tiên luôn là con trỏ tới struct | Nhất quán, và giống cách hầu hết ngôn ngữ khác làm |
| 3 | Hàm chỉ đọc nhận const con trỏ | Nhìn nguyên mẫu là biết hàm có sửa gì không |
| 4 | Cặp tạo và hủy đối xứng | Mỗi hàm tạo có đúng một hàm hủy tương ứng |
| 5 | Trả về mã lỗi, kết quả đi qua tham số ra | Phân biệt được thất bại với giá trị hợp lệ |
#include <stdlib.h>
#include "ngan-xep.h"
struct NganXep {
int *du_lieu;
size_t n;
size_t suc_chua;
};
NganXep *nx_tao(size_t suc_chua_ban_dau)
{
NganXep *nx = calloc(1, sizeof *nx);
if (nx == NULL) return NULL;
if (suc_chua_ban_dau > 0) {
nx->du_lieu = malloc(suc_chua_ban_dau * sizeof *nx->du_lieu);
if (nx->du_lieu == NULL) { free(nx); return NULL; }
nx->suc_chua = suc_chua_ban_dau;
}
return nx;
}
void nx_huy(NganXep *nx)
{
if (nx == NULL) return;
free(nx->du_lieu);
free(nx);
}
int nx_day(NganXep *nx, int gia_tri)
{
if (nx == NULL) return -1;
if (nx->n == nx->suc_chua) {
size_t moi = nx->suc_chua ? nx->suc_chua * 2 : 4;
int *tam = realloc(nx->du_lieu, moi * sizeof *tam);
if (tam == NULL) return -1;
nx->du_lieu = tam;
nx->suc_chua = moi;
}
nx->du_lieu[nx->n++] = gia_tri;
return 0;
}
int nx_lay(NganXep *nx, int *ra)
{
if (nx == NULL || ra == NULL || nx->n == 0) return -1;
*ra = nx->du_lieu[--nx->n];
return 0;
}
size_t nx_so_luong(const NganXep *nx)
{
return nx != NULL ? nx->n : 0;
}
int nx_rong(const NganXep *nx)
{
return nx == NULL || nx->n == 0;
}
int nx_dinh(const NganXep *nx, int *ra)
{
if (nx == NULL || ra == NULL || nx->n == 0) return -1;
*ra = nx->du_lieu[nx->n - 1];
return 0;
}day 1 2 3 lay: 3 2 1 rong: co
All heap blocks were freed -- no leaks are possible
#Trả về nhiều giá trị
Bài 8.6 đã trình bày ba cách trả về nhiều giá trị. Giờ bạn có struct, hãy so sánh lại hai cách chính.
int phan_tich(const int *a, size_t n,
int *min, int *max, double *tb);
int mn, mx;
double tb;
if (phan_tich(a, n, &mn, &mx, &tb) == 0)
printf("%d %d %.2f\n", mn, mx, tb);typedef struct {
int min, max;
double tb;
int hop_le; /* 0 nếu mảng rỗng */
} ThongKe;
ThongKe phan_tich(const int *a, size_t n);
ThongKe t = phan_tich(a, n);
if (t.hop_le)
printf("%d %d %.2f\n", t.min, t.max, t.tb);| Tham số ra | Trả về struct | |
|---|---|---|
| Chỗ gọi | Phải khai báo trước ba biến rời | Một biến, đọc là hiểu |
| Quên khởi tạo | Có thể quên, và nhận rác | Không thể quên |
| Báo lỗi | Qua giá trị trả về, rất rõ | Phải thêm một trường cờ |
| Struct lớn | Không tốn thêm gì | Chép cả struct, dù có tối ưu giá trị trả về |
| Bỏ qua kết quả không cần | Truyền NULL | Không bỏ được, nhưng cũng không tốn gì |
| Ghép lời gọi | Không | Có, dùng làm đối số cho hàm khác |
Struct làm giá trị trả về của hàm chuẩn
#include <stdlib.h>
/* Thư viện chuẩn có hai hàm trả về struct */
div_t div(int so_bi_chia, int so_chia);
ldiv_t ldiv(long so_bi_chia, long so_chia);
div_t r = div(17, 5);
printf("%d du %d\n", r.quot, r.rem); /* 3 du 2 */Hai hàm này tồn tại vì trên một số kiến trúc, lệnh chia cho ra cả thương và số dư trong một lần, nên tính riêng a / b và a % b có thể chậm gấp đôi. Chúng cũng là ví dụ chính thức nhất cho thấy trả về struct là cách được chuẩn C thừa nhận.
Tự làm thử
- Viết bộ hàm thao tác trên struct điểm hai chiều: cộng, trừ, độ dài, trung điểm. Truyền và trả về theo giá trị.
- Đo thời gian truyền struct 8, 16, 32 và 64 byte theo giá trị và theo con trỏ, mỗi cách mười triệu lần.
- Xem mã hợp ngữ do trình biên dịch sinh ra cho hàm nhận struct 8 byte và 24 byte, so sánh hai bản.
- Viết hàm bảy tham số rồi viết lại bằng struct tham số, so sánh chỗ gọi và thử thêm một tham số mới.
- Cài đủ thư viện ngăn xếp trong bài theo năm quy ước, chạy dưới valgrind.
- Mở rộng ngăn xếp thành hàng đợi bằng cách chỉ thêm hai hàm, giữ nguyên năm quy ước.
- Viết hàm phân tích mảng theo cả bốn cách trả về nhiều giá trị, so sánh mã ở chỗ gọi.
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
- Struct truyền và trả về theo giá trị được, khác hẳn mảng. Đó là lý do nó thay được tham số ra trong nhiều trường hợp.
- Tới khoảng mười sáu byte, struct được truyền qua thanh ghi nên không tốn gì thêm. Lớn hơn thì dùng
constcon trỏ. - Trả về struct lớn không tốn thêm lần sao chép nào nhờ tối ưu giá trị trả về, nên đừng tránh nó vì sợ chi phí.
- Struct tham số là cách C mô phỏng tham số có tên và tham số tùy chọn, và thêm trường mới không phá mã cũ.
- Năm quy ước thiết kế giao diện quanh struct sẽ lặp lại ở mọi cấu trúc dữ liệu của Phần 9.