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

Đa hình thủ công bằng vtable

Sau bài này bạn sẽ làm được

  • Thiết kế struct cơ sở chứa con trỏ tới bảng hàm ảo
  • Cài lớp con bằng cách nhúng struct cơ sở làm trường đầu
  • Gọi hàm qua bảng ảo và giải thích vì sao ép kiểu hợp lệ
  • So sánh với cách dùng enum và switch

C không có class, không có virtual, không có kế thừa. Nó có struct và con trỏ hàm, và hai thứ đó đủ để dựng lại toàn bộ cơ chế đa hình. Đây chính xác là cách nhân Linux tổ chức trình điều khiển thiết bị, và cũng chính xác là cách trình biên dịch C++ cài đặt từ khóa virtual.

#Vấn đề: nhiều loại, một cách xử lý

Union có nhãn cộng switch
typedef enum { H_TRON, H_CHU_NHAT, H_TAM_GIAC } LoaiHinh;

typedef struct {
    LoaiHinh loai;
    union {
        struct { double r; }          tron;
        struct { double a, b; }       chu_nhat;
        struct { double a, b, c; }    tam_giac;
    };
} Hinh;

double dien_tich(const Hinh *h) {
    switch (h->loai) {
        case H_TRON:     return PI * h->tron.r * h->tron.r;
        case H_CHU_NHAT: return h->chu_nhat.a * h->chu_nhat.b;
        case H_TAM_GIAC: /* cong thuc Heron */ ;
    }

    return 0;
}

double chu_vi(const Hinh *h)  { switch (h->loai) { /* lai ba nhanh */ } }
void   ve(const Hinh *h)      { switch (h->loai) { /* lai ba nhanh */ } }
void   luu(const Hinh *h)     { switch (h->loai) { /* lai ba nhanh */ } }
Bảng hàm ảo
typedef struct Hinh Hinh;

typedef struct {
    const char *ten;
    double (*dien_tich)(const Hinh *);
    double (*chu_vi)(const Hinh *);
    void   (*ve)(const Hinh *);
    void   (*huy)(Hinh *);
} HinhVTable;

struct Hinh { const HinhVTable *vt; };

/* Bon ham cong khai, moi cai MOT dong: */
double hinh_dien_tich(const Hinh *h) { return h->vt->dien_tich(h); }
double hinh_chu_vi(const Hinh *h)    { return h->vt->chu_vi(h); }
void   hinh_ve(const Hinh *h)        { if (h->vt->ve) h->vt->ve(h); }
void   hinh_huy(Hinh *h)             { if (h) h->vt->huy(h); }
ViệcUnion và switchBảng hàm ảo
Thêm một hình mớiSửa cả bốn hàmThêm một tệp, không sửa hàm nào
Thêm một thao tác mớiThêm một hàm có switchThêm một trường vào bảng, sửa mọi hình
Trình biên dịch bắt thiếu sótCó, nhờ -WswitchKhông trực tiếp
Kích thước một đối tượngBằng biến thể lớn nhấtĐúng bằng những gì nó cần
Người dùng thư viện thêm loại mớiKhông đượcĐược

#Bảng hàm ảo

Bảng hàm ảo
Một struct chỉ gồm con trỏ hàm, khai báo const và tồn tại đúng một bản cho mỗi loại. Mỗi đối tượng giữ một con trỏ tới bảng của loại mình, chứ không giữ cả bảng.
Mỗi đối tượng chỉ tốn thêm một con trỏ. Bảng dùng chung cho mọi đối tượng cùng loại.
hinh.h
#ifndef HINH_H
#define HINH_H

typedef struct Hinh Hinh;

typedef struct {
    const char *ten;
    double (*dien_tich)(const Hinh *);
    double (*chu_vi)(const Hinh *);
    void   (*huy)(Hinh *);
} HinhVTable;

struct Hinh {
    const HinhVTable *vt;
};

/* Ham tao cua tung loai */
Hinh *tao_tron(double r);
Hinh *tao_chu_nhat(double a, double b);

/* Giao dien chung, khong biet loai gi */
double hinh_dien_tich(const Hinh *h);
double hinh_chu_vi(const Hinh *h);
void   hinh_huy(Hinh *h);

#endif /* HINH_H */

#Lớp con bằng cách nhúng

tron.c
#include <stdlib.h>

#include "hinh.h"

#define PI 3.141592653589793

typedef struct {
    Hinh   base;          /* PHAI la truong DAU TIEN */
    double r;
} HinhTron;

static double tron_dt(const Hinh *h) {
    const HinhTron *t = (const HinhTron *)h;

    return PI * t->r * t->r;
}

static double tron_cv(const Hinh *h) {
    const HinhTron *t = (const HinhTron *)h;

    return 2 * PI * t->r;
}

static void tron_huy(Hinh *h) { free(h); }

static const HinhVTable TRON_VT = {
    .ten       = "Tron",
    .dien_tich = tron_dt,
    .chu_vi    = tron_cv,
    .huy       = tron_huy,
};

Hinh *tao_tron(double r) {
    HinhTron *t = malloc(sizeof *t);

    if (!t) return NULL;

    t->base.vt = &TRON_VT;
    t->r       = r;

    return &t->base;
}
terminal
gcc -std=c11 -Wall -Wextra -o hinh *.c && ./hinh
Tron     dien tich 12.5664  chu vi 12.5664
ChuNhat  dien tich 12.0000  chu vi 14.0000
Tron     dien tich  3.1416  chu vi  6.2832
tong dien tich = 27.7080
./hinh --bo-cuc
offsetof(HinhTron, base) = 0
offsetof(HinhTron, r)    = 8
sizeof(Hinh)     = 8
sizeof(HinhTron) = 16
&doi_tuong == &doi_tuong.base ? co
Dùng đa hình
Hinh *ds[] = {
    tao_tron(2.0),
    tao_chu_nhat(3.0, 4.0),
    tao_tron(1.0),
};

double tong = 0;

for (size_t i = 0; i < 3; ++i) {
    printf("%-8s dien tich %7.4f\n",
           ds[i]->vt->ten, hinh_dien_tich(ds[i]));

    tong += hinh_dien_tich(ds[i]);
}

for (size_t i = 0; i < 3; ++i) hinh_huy(ds[i]);

/* Vong lap nay KHONG BIET co bao nhieu loai hinh ton tai.
   Them HinhTamGiac vao du an: vong lap nay khong doi mot ky tu. */

#Vì sao ép kiểu như vậy hợp lệ

/* Chuan C, muc 6.7.2.1:

   "A pointer to a structure object, suitably converted, points to
    its initial member, and vice versa."

   Dich: con tro toi mot doi tuong struct, sau khi chuyen doi
   thich hop, tro toi thanh vien DAU TIEN cua no, va nguoc lai.

   Nen:
     (Hinh *)con_tro_tron          hop le, cho ra &tron->base
     (HinhTron *)con_tro_hinh      hop le, VOI DIEU KIEN
                                   doi tuong that su la mot HinhTron */

#Cách nhân Linux dùng

Trích từ include/linux/fs.h, rút gọn
struct file_operations {
    struct module *owner;
    loff_t  (*llseek) (struct file *, loff_t, int);
    ssize_t (*read)   (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)  (struct file *, const char __user *, size_t, loff_t *);
    int     (*open)   (struct inode *, struct file *);
    int     (*release)(struct inode *, struct file *);
    long    (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    /* ... vai chuc con tro ham nua ... */
};
Một trình điều khiển ký tự điển hình
static ssize_t den_read(struct file *f, char __user *bo_dem,
                        size_t n, loff_t *vt) { /* ... */ }

static ssize_t den_write(struct file *f, const char __user *bo_dem,
                         size_t n, loff_t *vt) { /* ... */ }

static const struct file_operations DEN_FOPS = {
    .owner   = THIS_MODULE,
    .read    = den_read,
    .write   = den_write,
    .open    = den_open,
    .release = den_release,
};

/* Dang ky voi nhan */
cdev_init(&den_cdev, &DEN_FOPS);

/* Tu day tro di, khi tien trinh nguoi dung goi read() tren
   /dev/den, nhan goi DEN_FOPS.read, ma khong he biet gi ve
   trinh dieu khien nay.

   Day chinh xac la co che o tren, chi khac ten. */

#So với enum và switch

Tiêu chíUnion có nhãnBảng hàm ảo
Bộ nhớ mỗi đối tượngBằng biến thể lớn nhất cộng nhãnĐúng nhu cầu cộng một con trỏ
Thêm loại mớiSửa mọi hàm có switchThêm một tệp
Thêm thao tác mớiThêm một hàmSửa vtable của mọi loại
Người ngoài thêm loạiKhôngĐược, nếu vtable công khai
Chi phí một lời gọiMột lần rẽ nhánhHai lần đọc bộ nhớ, một lần gọi gián tiếp
Trình biên dịch nội tuyếnĐượcHiếm khi
Bắt thiếu sót lúc biên dịchCó, -WswitchChỉ bằng _Static_assert và kiểm NULL
Lưu ra tệpĐược, nhãn là sốKhông trực tiếp, con trỏ đổi mỗi lần chạy

Tự làm thử

  1. Cài Hinh, HinhTron và HinhChuNhat đầy đủ, rồi tính tổng diện tích của một mảng ba hình.
  2. In offsetof(HinhTron, base) và xác nhận nó bằng 0.
  3. Đổi thứ tự trường để base không còn ở đầu, chạy lại, và giải thích kết quả.
  4. Thêm _Static_assert kiểm offsetof và xác nhận nó bắt được lỗi trên.
  5. Thêm HinhTamGiac vào dự án mà không sửa một dòng nào trong vòng lặp tính tổng.
  6. Đặt ve bằng NULL cho một loại và xác nhận hinh_ve không sập.

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

  • Bảng hàm ảo là một struct const chỉ gồm con trỏ hàm, tồn tại một bản cho mỗi loại, và mỗi đối tượng chỉ giữ con trỏ tới nó.
  • Lớp con nhúng struct cơ sở làm trường đầu tiên, và chuẩn C bảo đảm con trỏ tới struct cũng trỏ tới thành viên đầu của nó.
  • Ép kiểu ngược lại chỉ hợp lệ khi đối tượng thật sự thuộc loại đó, và C không có cách nào kiểm tra hộ bạn.
  • Union có nhãn dễ khi thêm thao tác, còn bảng hàm ảo dễ khi thêm loại. Chọn theo chiều nào thay đổi nhiều hơn.
  • Đây là cơ chế mà nhân Linux dùng cho file_operations, và cũng là cơ chế mà trình biên dịch C++ sinh ra cho virtual.