Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 15.620 phút đọc

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ề

truyen.c
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áchChi phíDùng khi
void f(S s)Chép cả structStruct nhỏ, và hàm muốn bản sao riêng để sửa
void f(const S *s)Chép 8 byteMặc định cho hàm chỉ đọc struct lớn
void f(S *s)Chép 8 byteHà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 structTruyền thế nào trên System V AMD64
Tới 8 byte, toàn số nguyênMột thanh ghi nguyên, ví dụ rdi
Tới 16 byte, toàn số nguyênHai thanh ghi nguyên
Tới 16 byte, toàn số thựcMột hoặc hai thanh ghi xmm
Lớn hơn 16 byteChép ra ngăn xếp, hàm nhận qua bộ nhớ
Có mảng bên trongLuôn chép qua bộ nhớ, bất kể kích thước
quy-uoc.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);
terminal
# Xem trình biên dịch sinh mã gì
gcc -S -O2 -masm=intel quy-uoc.c -o - | head -20
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]
        ret
terminal
# Đo thật: mười triệu lời gọi với ba kích thước
./do-quy-uoc
struct 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ố

Bảy tham số rời rạc
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 */
Một struct tham số
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

Mẫu đối tượng của C
Một struct cùng một bộ hàm thao tác trên nó, mọi hàm nhận con trỏ tới struct làm tham số đầu tiên, và mọi hàm cùng tiền tố tên.
ngan-xep.h
#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 ướcNội dungVì sao
1Mọ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
2Tham số đầu tiên luôn là con trỏ tới structNhất quán, và giống cách hầu hết ngôn ngữ khác làm
3Hà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
4Cặp tạo và hủy đối xứngMỗi hàm tạo có đúng một hàm hủy tương ứng
5Trả về mã lỗi, kết quả đi qua tham số raPhân biệt được thất bại với giá trị hợp lệ
ngan-xep.c
#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;
}
terminal
gcc -std=c17 -Wall -Wextra -g -fsanitize=address ngan-xep.c main.c -o t && ./t
day 1 2 3
lay: 3 2 1
rong: co
valgrind --leak-check=full ./t
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.

Ba tham số ra
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);
Trả về struct
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ố raTrả về struct
Chỗ gọiPhải khai báo trước ba biến rờiMột biến, đọc là hiểu
Quên khởi tạoCó thể quên, và nhận rácKhông thể quên
Báo lỗiQua giá trị trả về, rất rõPhải thêm một trường cờ
Struct lớnKhô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ầnTruyền NULLKhông bỏ được, nhưng cũng không tốn gì
Ghép lời gọiKhôngCó, 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 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ử

  1. 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ị.
  2. Đ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.
  3. 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.
  4. 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.
  5. Cài đủ thư viện ngăn xếp trong bài theo năm quy ước, chạy dưới valgrind.
  6. 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.
  7. 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 const con 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.