Con trỏ hàm là gì?
Con trỏ hàm là một biến lưu trữ địa chỉ của một hàm, thông qua biến đó, có thể gọi hàm mà nó trỏ tới. Điều này rất thuận tiện khi muốn định nghĩa các chức năng khác nhau cho một nhóm các đối tượng khá giống nhau.
Sử dụng con trỏ hàm như thế nào?
Một con trỏ hàm có thể khởi tạo theo mẫu sau:
<kiểu trả về> (*<tên con trỏ>)(<danh sách đối số>);
Ví dụ về con trỏ hàm nhận vào một biến kiểu int và trả về dữ liệu kiểu void.
void (*func)(int);
Nhưng đây mới chỉ là khai báo, cũng giống như mọi con trỏ khác, con trỏ hàm phải được định nghĩa giá trị trước khi sử dụng, nhưng không thể dùng từ khóa new hay malloc để cấp phát vùng nhớ cho một con trỏ hàm, vì như thế thì chẳng cách nào định nghĩa được các lệnh của nó, hơn nữa vùng nhớ của lệnh và của biến cũng khác nhau vậy chỉ còn một cách là cho nó trỏ đến một vùng nhớ lưu trữ giá trị khai báo sẵn.
Về bản chất, máy chỉ hiểu được các lệnh mã máy với 0 và 1, nên không chỉ có biến hay con trỏ, các câu lệnh cũng có địa chỉ của riêng nó. (Chi tiết về cách lưu trữ của một chương trình tham khảo tại bài viết Memory Segment).
Cũng nhưng những con trỏ thông thường, có thể truyền địa chỉ của một hàm vào con trỏ với cách đơn giản như sau:
void thefunc(int a) { // DO SOMETHING } int main() { // ... void (*func)(int); func = &thefunc; // ... }
Để gọi một hàm đã lưu trữ trong con trỏ hàm, chỉ cần gọi con trỏ với một danh sách đối số phù hợp:
func(1); // OR (*func)(1);
Lưu ý là con trỏ hàm và hàm được trỏ đến phải có cùng danh sách đối số và kiểu trả về.
Truyền hàm vào hàm
Do con trỏ hàm được khai báo dựa theo kiểu trả về và danh sách đối số của hàm sẽ được trỏ đến nên để thuận tiện hơn trong các thao tác xử lý, nên định kiểu cho mỗi kiểu con trỏ hàm để nó có một cái tên nhất định (thông qua từ khóa typedef). Chẳng hạn như:
typedef int (*Func_int)();
Vậy là đã có thể sử dụng kiểu dữ liệu Func_int như một kiểu dữ liệu định nghĩa các con trỏ hàm không đối số trả về một phần tử kiểu int.
Theo đó, có thể yêu cầu một đối số func_int từ một hàm, như sau:
void foo(func_int fun, int a) { // DO SOMETHING // ... }
Thao tác truyền hàm vào hàm được thực hiện như sau:
typedef int (*func_int)(); void foo(func_int fun, int a) { // DO SOMETHING // ... } int doSomething() { // DO SOMETHING // ... } int main() { // ... foo(&doSomething, 0); // ... }
Lập lịch thao tác với vector các con trỏ hàm
Con trỏ hàm xét cho cùng cũng là một con trỏ, nên có thể sử dụng các kiểu tổ chức dữ liệu như mảng, cây, danh sách liên kết… để lưu trữ và xử lý các hàm như những phần tử đơn thuần. Ở đây sẽ nói về cách tổ chức các con trỏ hàm với vector.
Cùng xét ví dụ sau, giả sử một Button cần xử lý lần lượt 2 hàm thefunc1 và thefunc2 sau theo thứ tự.
int thefunc1(int a) { return a++; } int thefunc2(int a) { return a + 3; }
Vậy trong trường hợp này, có thể viết lớp Button và sử dụng như sau:
class Button { public: Button() { func1 = &thefunc1; func2 = &thefunc2; } ~Button(){}; int Action(int a) { return (*func1)(a) + (*func2)(a); } private: int (*func1)(int); int (*func2)(int); }; int main() { Button b1; b1.Action(3); return 0; }
Thấy button b1 được gán vào 2 hàm xử lý bên ngoài (thefunc1 và thefunc2). Tuy nhiên không phải button nào cũng có 2 hàm xử lý, cho nên phải tìm ra một phương pháp tổ chức các con trỏ hàm hợp lý hơn, tổng quát hơn cho những trường hợp Button có số hàm xử lý không xác định.
Sau khi chỉnh sửa, có đoạn chương trình như sau:
#include <vector> // ... class Button { public: Button(){}; ~Button(){}; void Add(func f) { functions.push_back(f); } int Action(int a) { int result; for(int i = 0; i < functions.size(); ++i) result += (*functions[i])(a); return result; } private: std::vector<func> functions; }; int main() { Button b1; b1.Add(&thefunc1); b1.Add(&thefunc2); b1.Action(3); }
Vậy là với một lớp Button như thế này, có thể thêm vào danh sách thao tác của mỗi đối tượng kiểu Button một số lượng hàm tùy ý, tất nhiên, các hàm này phải có cùng kiểu trả về và danh sách đối số với con trỏ hàm.
Con trỏ hàm trong lập trình hướng đối tượng
Trong OOP, vấn đề khởi tạo và sử dụng một con trỏ hàm có phần phức tạp hơn lập trình hướng thủ tục do có vấn đề về tầm vực của hàm và các hàm phải được gọi thông qua đối tượng của lớp.
Nói về phần này có một ví dụ như sau:
#include <iostream> using namespace std; class Default { public: int Sum(int a,int b) { return a + b; } }; class Function { typedef int (Default::*func)(int, int); public: func p; Function(func fun, Default* object) { this->p = fun; Obj = object; } ~Function(){} int Action(int a, int b) { return (Obj->*p)(a, b); } private: Default* Obj; }; int main() { Default k; Function* f; f = new Function(&Default::Sum, &k); cout << f->Action(5, 6); getchar(); return 0; }
Những điều cần chú ý ở đây là:
- Con trỏ kiểu Default* trong hàm tạo của Function.
- Hàm Action của Function.
Chức năng của con trỏ kiểu Default trong lớp Function nhằm xác định đối tượng sẽ gọi đến hàm Sum của lớp Default, thấy rõ việc này nhất trong hàm Action của lớp Function. Không như thông thường, hàm Sum được gọi đến bằng cách như sau:
(Obj->*p)(a,b);
Với p là con trỏ trỏ đến hàm Sum, và Obj là con trỏ trỏ đến đối tượng k thuộc lớp Default. Cách gọi này về tư tưởng là tương đương với:
k->Sum(a, b);