Bài 13.522 phút đọc
Con trỏ hàm
Sau bài này bạn sẽ làm được
- Khai báo và dùng con trỏ hàm đúng cú pháp
- Xây bảng điều phối thay cho chuỗi if hoặc switch
- Dùng typedef để làm con trỏ hàm dễ đọc
- Biết chi phí của lời gọi gián tiếp
Tới giờ bạn truyền dữ liệu vào hàm. Con trỏ hàm cho phép bạn truyền cả hành vi. Đây là công cụ đứng sau qsort, sau mọi cơ chế xử lý sự kiện, và sau cách C mô phỏng lập trình hướng đối tượng.
#Hàm cũng có địa chỉ
Mã máy của một hàm nằm ở vùng .text trong bộ nhớ, như Bài 12.1 đã vẽ. Vậy nó có địa chỉ, và địa chỉ đó lưu vào biến được.
dia-chi-ham.c
#include <stdio.h>
int cong(int a, int b) { return a + b; }
int main(void)
{
printf("dia chi cua cong : %p\n", (void *)cong);
printf("dia chi cua main : %p\n", (void *)main);
printf("cong == &cong : %d\n", cong == &cong); /* 1 */
return 0;
}terminal
./dia-chi-ham
dia chi cua cong : 0x55f3a2401149 dia chi cua main : 0x55f3a2401169 cong == &cong : 1
#Cú pháp khai báo và gọi
int (*p)(int, int); /* p là con trỏ tới hàm nhận hai int, trả về int */| Phần | Nghĩa là |
|---|---|
| int | Kiểu trả về của hàm |
| (*p) | p là con trỏ. Ngoặc bắt buộc |
| (int, int) | Danh sách kiểu tham số |
con-tro-ham.c
#include <stdio.h>
static int cong(int a, int b) { return a + b; }
static int tru(int a, int b) { return a - b; }
static int nhan(int a, int b) { return a * b; }
int main(void)
{
int (*phep)(int, int);
phep = cong;
printf("%d\n", phep(7, 3)); /* 10 */
phep = tru;
printf("%d\n", phep(7, 3)); /* 4 */
phep = nhan;
printf("%d\n", (*phep)(7, 3)); /* 21, cách viết rõ ràng hơn */
return 0;
}Truyền hàm vào hàm khác
truyen-ham.c
/* Áp dụng một phép toán lên mọi phần tử của mảng. */
void ap_dung(int *a, size_t n, int (*phep)(int))
{
if (a == NULL || phep == NULL) return;
for (size_t i = 0; i < n; ++i)
a[i] = phep(a[i]);
}
static int binh_phuong(int x) { return x * x; }
static int am(int x) { return -x; }
int a[5] = { 1, 2, 3, 4, 5 };
ap_dung(a, 5, binh_phuong); /* 1 4 9 16 25 */
ap_dung(a, 5, am); /* -1 -4 -9 -16 -25 */Cùng một vòng lặp, hành vi khác nhau tùy hàm truyền vào. Đây là điểm bắt đầu của Bài 13.6 về hàm gọi lại.
#Bảng điều phối
Bảng điều phối
Một mảng con trỏ hàm, dùng để chọn hàm cần gọi bằng chỉ số thay vì bằng chuỗi
if hoặc switch.Chuỗi if else if
int tinh(char op, int a, int b)
{
if (op == '+') return a + b;
else if (op == '-') return a - b;
else if (op == '*') return a * b;
else if (op == '/') return b != 0 ? a / b : 0;
else if (op == '%') return b != 0 ? a % b : 0;
return 0;
}
/* Thêm một phép mới phải sửa hàm này. */Bảng điều phối
typedef int (*Phep)(int, int);
static int cong(int a, int b) { return a + b; }
static int tru(int a, int b) { return a - b; }
static int nhan(int a, int b) { return a * b; }
static int chia(int a, int b) { return b != 0 ? a / b : 0; }
static int du(int a, int b) { return b != 0 ? a % b : 0; }
static const struct {
char ky_hieu;
Phep ham;
} BANG[] = {
{ '+', cong },
{ '-', tru },
{ '*', nhan },
{ '/', chia },
{ '%', du },
};
int tinh(char op, int a, int b, int *ra)
{
for (size_t i = 0; i < sizeof BANG / sizeof BANG[0]; ++i)
if (BANG[i].ky_hieu == op) {
*ra = BANG[i].ham(a, b);
return 0;
}
return -1;
}
/* Thêm một phép mới chỉ là thêm một hàm và một dòng vào BANG. */Bảng tra trực tiếp bằng chỉ số
bang-chi-so.c
typedef void (*XuLy)(const char *);
static void xu_ly_them(const char *s) { printf("them: %s\n", s); }
static void xu_ly_xoa(const char *s) { printf("xoa: %s\n", s); }
static void xu_ly_sua(const char *s) { printf("sua: %s\n", s); }
enum { LENH_THEM, LENH_XOA, LENH_SUA, SO_LENH };
static const XuLy BANG[SO_LENH] = {
[LENH_THEM] = xu_ly_them,
[LENH_XOA] = xu_ly_xoa,
[LENH_SUA] = xu_ly_sua,
};
void chay(int lenh, const char *tham_so)
{
if (lenh < 0 || lenh >= SO_LENH || BANG[lenh] == NULL) {
fprintf(stderr, "Lenh khong hop le\n");
return;
}
BANG[lenh](tham_so);
}| Chuỗi if else if | Bảng điều phối | |
|---|---|---|
| Thời gian tìm | Tuyến tính theo số nhánh | Hằng số nếu tra bằng chỉ số |
| Thêm nhánh mới | Sửa hàm | Thêm một dòng dữ liệu |
| Đọc mã | Thấy ngay từng nhánh làm gì | Phải nhảy tới hàm tương ứng |
| Trình biên dịch tối ưu | Có thể nhúng từng nhánh | Không nhúng được lời gọi gián tiếp |
| Phù hợp với | Ít nhánh, lô gic khác nhau nhiều | Nhiều nhánh cùng dạng |
#typedef làm mã dễ đọc
Viết trần
void ap_dung(int *a, size_t n, int (*phep)(int));
int (*chon(char c))(int, int);
int (*bang[10])(const char *, size_t);
void (*sig(int, void (*)(int)))(int);Dùng typedef
typedef int (*PhepMotNgoi)(int);
typedef int (*PhepHaiNgoi)(int, int);
typedef int (*XuLyChuoi)(const char *, size_t);
typedef void (*XuLyTinHieu)(int);
void ap_dung(int *a, size_t n, PhepMotNgoi phep);
PhepHaiNgoi chon(char c);
XuLyChuoi bang[10];
XuLyTinHieu sig(int, XuLyTinHieu);Cú pháp gọi qua typedef
typedef.c
typedef int (*PhepHaiNgoi)(int, int);
static int cong(int a, int b) { return a + b; }
int main(void)
{
PhepHaiNgoi p = cong;
printf("%d\n", p(3, 4)); /* 7 */
printf("%d\n", (*p)(3, 4)); /* 7, tương đương */
PhepHaiNgoi bang[2] = { cong, NULL };
if (bang[0] != NULL)
printf("%d\n", bang[0](5, 6)); /* 11 */
return 0;
}#Chi phí của lời gọi gián tiếp
terminal
# Gọi một hàm cộng đơn giản một tỷ lần
./do-goi
goi truc tiep : 0.281 s goi qua con tro ham : 1.104 s ti le : 3.9 lan
| Chi phí thêm | Giải thích |
|---|---|
| Không nhúng được | Trình biên dịch không biết hàm nào sẽ được gọi nên không chép thân hàm vào |
| Đọc thêm một lần bộ nhớ | Phải lấy địa chỉ từ biến con trỏ trước khi nhảy |
| Dự đoán rẽ nhánh kém hơn | Bộ xử lý khó đoán đích nhảy nếu con trỏ đổi liên tục |
| Mất tối ưu xuyên hàm | Trình biên dịch không suy luận được gì về thân hàm được gọi |
Khi nào con trỏ hàm là lựa chọn đúng
- Cần chọn hành vi lúc chạy. Người dùng chọn thuật toán sắp xếp, hoặc chọn bộ lọc.
- Viết hàm tổng quát.
qsortkhông biết trước kiểu dữ liệu, nên nó phải hỏi bạn cách so sánh. - Đăng ký xử lý sự kiện. Thư viện gọi lại mã của bạn khi có chuyện xảy ra.
- Nạp thư viện động. Hàm được tìm lúc chạy nên chỉ có địa chỉ.
- Mô phỏng hướng đối tượng. Một struct chứa các con trỏ hàm chính là bảng phương thức ảo. Chương 30 sẽ nói kỹ.
huong-doi-tuong.c
/* Mô phỏng giao diện: struct chứa dữ liệu và các thao tác. */
typedef struct HinhVT {
double (*dien_tich)(const void *);
double (*chu_vi)(const void *);
} HinhVT;
typedef struct {
const HinhVT *vt;
double r;
} HinhTron;
static double tron_dien_tich(const void *p)
{
const HinhTron *h = p;
return 3.14159265358979 * h->r * h->r;
}
static double tron_chu_vi(const void *p)
{
const HinhTron *h = p;
return 2 * 3.14159265358979 * h->r;
}
static const HinhVT TRON_VT = { tron_dien_tich, tron_chu_vi };
HinhTron t = { &TRON_VT, 2.0 };
printf("%.2f\n", t.vt->dien_tich(&t)); /* 12.57 */Đây chính xác là cách C++ cài đặt hàm ảo, chỉ khác là trình biên dịch làm hộ. Chương 15 sẽ dạy struct đầy đủ, và Chương 30 sẽ quay lại mẫu này.
Tự làm thử
- In địa chỉ của ba hàm khác nhau, xác nhận
fvà&fbằng nhau. - Khai báo con trỏ hàm, gán lần lượt ba hàm và gọi qua nó, in kết quả từng lần.
- Viết hàm
ap_dungnhận con trỏ hàm một ngôi, dùng nó với ba phép toán khác nhau. - Cài máy tính đơn giản bằng bảng điều phối, so sánh với bản dùng chuỗi
ifvề số dòng khi thêm phép thứ sáu. - Cài bảng tra bằng chỉ số với khởi tạo có chỉ định, cố tình bỏ trống một ô và kiểm tra cơ chế phát hiện
NULL. - Đo thời gian gọi trực tiếp và gọi qua con trỏ hàm một tỷ lần, thử với hàm rỗng và hàm làm việc thật.
- Cài mẫu bảng phương thức ảo cho hai loại hình học, gọi cùng một hàm trên cả hai.
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
- Hàm có địa chỉ, và tên hàm tự phân rã thành con trỏ.
fvà&ftương đương. - Khai báo là
int (*p)(int, int). Thiếu ngoặc thì thành hàm trả về con trỏ, hoàn toàn khác. - Bảng điều phối tách dữ liệu khỏi lô gic, nên thêm hành vi mới chỉ là thêm một dòng dữ liệu.
- Luôn dùng
typedefcho con trỏ hàm, và luôn kiểm tra khácNULLtrước khi gọi. - Lời gọi gián tiếp chậm hơn vì không nhúng được, nhưng chi phí đó không đáng kể với hàm làm việc thật.