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ệc | Union và switch | Bảng hàm ảo |
|---|---|---|
| Thêm một hình mới | Sửa cả bốn hàm | Thêm một tệp, không sửa hàm nào |
| Thêm một thao tác mới | Thêm một hàm có switch | Thê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ót | Có, nhờ -Wswitch | Không trực tiếp |
| Kích thước một đối tượng | Bằ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ới | Khô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.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ãn | Bảng hàm ảo |
|---|---|---|
| Bộ nhớ mỗi đối tượng | Bằ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ới | Sửa mọi hàm có switch | Thêm một tệp |
| Thêm thao tác mới | Thêm một hàm | Sửa vtable của mọi loại |
| Người ngoài thêm loại | Không | Được, nếu vtable công khai |
| Chi phí một lời gọi | Một lần rẽ nhánh | Hai 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 | Được | Hiếm khi |
| Bắt thiếu sót lúc biên dịch | Có, -Wswitch | Chỉ 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ử
- Cài
Hinh,HinhTronvàHinhChuNhatđầy đủ, rồi tính tổng diện tích của một mảng ba hình. - In
offsetof(HinhTron, base)và xác nhận nó bằng 0. - Đổi thứ tự trường để
basekhông còn ở đầu, chạy lại, và giải thích kết quả. - Thêm
_Static_assertkiểmoffsetofvà xác nhận nó bắt được lỗi trên. - Thêm
HinhTamGiacvào dự án mà không sửa một dòng nào trong vòng lặp tính tổng. - Đặt
vebằng NULL cho một loại và xác nhậnhinh_vekhô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
constchỉ 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 chovirtual.