Con trỏ hàm (Function pointers)

Chào các bạn đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.

Tiếp tục tìm hiểu về con trỏ trong ngôn ngữ lập trình C++, trong bài học này, mình sẽ giới thiệu đến các bạn một loại con trỏ mới có chức năng khá đặc biệt.

Như chúng ta đã biết, con trỏ có chức năng lưu trữ địa chỉ của một vùng nhớ nào đó trên bộ nhớ ảo. Tuy nhiên, bộ nhớ ảo được chia làm nhiều phân vùng khác nhau.

Như trong hình, hầu hết toàn bộ phân vùng của bộ nhớ ảo đều dùng để lưu trữ dữ liệu (biến đơn, giá trị, chuỗi kí tự, …). Nhưng dữ liệu lưu trên bộ nhớ ảo là cái được tạo ra sau khi chương trình nào đó được thực thi và nó xin cấp phát vùng nhớ trên bộ nhớ ảo để sử dụng. Trước đó, chương trình hoạt động dựa trên các dòng lệnh mà lập trình viên đưa ra sử dụng cú pháp của ngôn ngữ lập trình nào đó. Và trước khi chạy chương trình, mã nguồn (đã được biên dịch thành mã máy) đang lưu trữ trong máy tính (có thể là trên ổ cứng) cũng phải được hệ điều hành load lên RAM và quản lý bằng cách đưa vào bộ nhớ ảo. Vậy mã nguồn của chương trình sẽ lưu ở đâu trên bộ nhớ ảo? Đó là tại phân vùng Text (Text segment) hay còn gọi là phân vùng Code (Code segment). Tất cả các lệnh, các hàm… của chương trình sẽ được đưa vào phân vùng này, trong đó có cả hàm main nếu đó là chương trình C++. Như các bạn đã biết, một chương trình C++ sẽ có duy nhất một hàm main đóng vai trò là điểm bắt đầu của chương trình đó. Như vậy, sau khi được load mã nguồn C++ đã được biên dịch lên bộ nhớ ảo, hệ điều hành sẽ tìm đến vị trí (địa chỉ) của hàm main và chuyển mã nguồn đến cho CPU xử lý.

Mình có thể cho các bạn xem địa chỉ của hàm main của một chương trình C++ ở trên máy mình như hình bên dưới:

Như các bạn thấy, hàm main (hay bất kỳ hàm nào khác trong chương trình) có một địa chỉ xác định trên bộ nhớ ảo. Do đó, chúng ta có thể sử dụng con trỏ để trỏ đến địa chỉ của hàm main. Tuy nhiên, chúng ta cần lưu ý đến kiểu dữ liệu khai báo cho con trỏ phải tương thích với kiểu dữ liệu của vùng nhớ. Ví dụ, con trỏ kiểu int dùng để trỏ đến vùng nhớ kiểu int, con trỏ trỏ đến hằng (Pointer to const) dùng để trỏ đến vùng nhớ hằng… Và để trỏ đến địa chỉ của một hàm, chúng ta cần sử dụng con trỏ hàm (Function pointer hoặc có thể gọi là Pointer to function).

Function pointers

Khi nhìn vào một hàm (function), ví dụ:

int foo()
{
	return 0;
}

Chúng ta có thể nói hàm này có định danh là foo, kiểu trả về là int, hàm foo không nhận đối số. Đó là những gì chúng ta thấy được trong quá trình biên soạn mã nguồn chương trình. Thuộc tính địa chỉ của hàm chỉ được sinh ra khi chương trình đã được chạy.

int foo() // code of foo start at memory address 0x01001492
{
	return 0;
}

int main()	
{
	int n = foo();
	
	return 0;
}

Như vậy, khi trong hàm main chạy đến dòng lệnh gọi hàm foo, hệ điều hành sẽ tìm đến địa chỉ của hàm foo trên bộ nhớ ảo và chuyển mã lệnh của hàm foo cho CPU tiếp tục xử lý. Để in ra địa chỉ của hàm foo, chúng ta có thể làm như sau:

int foo()
{
	return 0;
}

int main()
{
	cout << foo << endl;
	
	return 0;
}

Kết quả:

013D1492

Như các bạn thấy, khi muốn thực thi một hàm, chúng ta cần thêm cặp dấu ngoặc để truyền đối số vào cho hàm (nếu hàm không có tham số thì để trống). Nếu chúng ta không sử dụng cặp dấu ngoặc, sử dụng tên hàm trả về địa chỉ của hàm trên bộ nhớ ảo. Và địa chỉ này có thể được gán con một con trỏ có kiểu dữ liệu tương ứng (function pointer).

Function pointers syntax

Cú pháp của một con trỏ hàm có nhiều điểm khác biệt so với cách khai báo con trỏ thông thường.

<return_type> (*<name_of_pointer>)( <data_type_of_parameters> );

Mình lấy ví dụ, để trỏ đến hàm foo trong ví dụ trên, chúng ta cần khai báo con trỏ hàm như sau:

int (*pFoo) ();

Trong đó, int là kiểu trả về của hàm foo, pFoo là tên của con trỏ, và hàm foo không có tham số nên phần trong ngoặc mình bỏ trống. Một ví dụ khác, mình có hàm như bên dưới:

void swapValue(int &value1, int &value2) 
{
	int temp = value1;
	value1 = value2;
	value2 = temp;
}

Hàm swapValue có không có kiểu trả về, và nó nhận vào 2 tham số đều có kiểu tham chiếu int. Như vậy, mình có thể khai báo một con trỏ hàm dùng để trỏ đến hàm swapValue như sau:

void(*pSwap) (int &, int &);
Gán địa chỉ của hàm cho Function pointers

Sau khi đã có được con trỏ hàm được khai báo tương ứng với hàm, chúng ta có thể gán địa chỉ của hàm cho chúng:

void swapValue(int &value1, int &value2) 
{
	int temp = value1;
	value1 = value2;
	value2 = temp;
}

int main()
{
	void(*pSwap) (int &, int &) = swapValue;
	cout << pSwap << endl;
	cout << swapValue << endl;
	
	return 0;
}

Lưu ý, khi cần lấy địa chỉ của hàm, chúng ta chỉ sử dụng duy nhất tên hàm, không đặt thêm cặp dấu ngoặc vào.

Chỉ có con trỏ được khai báo có kiểu dữ liệu trả về và danh sách tham số phù hợp mới trỏ đến hàm được.

// function prototypes
int foo();
double goo();
int hoo(int x);
 
// function pointer assignments
int (*funcPtr1)() = foo; // okay
int (*funcPtr2)() = goo; // wrong -- return types don't match!
double (*funcPtr4)() = goo; // okay
funcPtr1 = hoo; // wrong -- fcnPtr1 has no parameters, but hoo() does
int (*funcPtr3)(int) = hoo; // okay
Sử dụng Function pointers

Sau khi đã nắm giữ được địa chỉ của hàm, con trỏ hàm có thể được sử dụng như hàm thông qua toán tử dereference. Ví dụ:

void swapValue(int &value1, int &value2)
{
	int temp = value1;
	value1 = value2;
	value2 = temp;
}

int main()
{
	void(*pSwap) (int &, int &) = swapValue;
	
	int a = 1, b = 5;
	cout << "Before: " << a << " " << b << endl;
	(*pSwap)(a, b);
	cout << "After:  " << a << " " << b << endl;

	return 0;
}

Lưu ý, tham số mặc định của hàm không áp dụng được cho con trỏ hàm, vì tham số mặc định được compiler xác định tại thời điểm compile chương trình, còn con trỏ hàm được sử dụng tại thời điểm chương trình đang chạy.

Sử dụng con trỏ hàm làm tham số

Một con trỏ hàm cũng là một biến con trỏ, do đó chúng ta có thể sử dụng con trỏ hàm là tham số của một hàm nào đó. Khi tham số của hàm là con trỏ hàm, chúng ta sẽ truyền đối số là địa chỉ của một hàm. Hàm được sử dụng làm đối số của hàm có thể gọi là callback function.

Mình lấy ví dụ về hàm selectionSort dùng để sắp xếp dữ liệu trong mảng số nguyên theo thứ tự tăng dần:

#include <algorithm> // use for std::swap

void selectionSort(int *arr, int length)
{
	for (int i_start = 0; i_start < length; i_start++)
	{
		int minIndex = i_start;

		for (int i_current = i_start + 1; i_current < length; i_current++)
		{
			if (arr[minIndex] > arr[i_current])
			{
				minIndex = i_current;
			}
		}

		swap(arr[i_start], arr[minIndex]); // std::swap
	}
}

//................

int main()
{
	int arr[] = { 1, 4, 2, 3, 6, 5, 8, 9, 7 };
	int length = sizeof(arr) / sizeof(int);

	cout << "Before sorted: ";
	printArray(arr, length);

	selectionSort(arr, length);

	cout << "After sorted:  ";
	printArray(arr, length);

	return 0;
}

Kết quả:

Before sorted: 1 4 2 3 6 5 8 9 7
After sorted:  1 2 3 4 5 6 7 8 9

Bây giờ đặt ra trường hợp chúng ta muốn có sắp xếp mảng một chiều sử dụng thuật toán selectionSort nhưng sắp xếp theo thứ tự giảm dần. Như vậy, chúng ta cần đến 2 hàm selectionSort để đáp ứng cho 2 trường hợp mình kể trên. Trong khi cả 2 hàm selectionSort này chỉ khác nhau về toán tử so sánh tại phép so sánh ở mệnh đề if trong vòng lặp.

Để giải quyết vấn đề này, đầu tiên mình cần có 2 hàm thực hiện công việc so sánh như sau:

bool ascending(int left, int right)
{
	return left > right;
}

bool descending(int left, int right)
{
	return left < right;
}

Hai hàm này chỉ có tác dụng thay thế phép so sánh trong mệnh đề if. Nếu chúng ta cần sắp xếp giá trị trong mảng theo thứ tự tăng dần, chúng ta sẽ thay thế hàm ascending vào mệnh đề if trong hàm selectionSort như sau:

void selectionSort(int *arr, int length)
{
	for (int i_start = 0; i_start < length; i_start++)
	{
		int minIndex = i_start;

		for (int i_current = i_start + 1; i_current < length; i_current++)
		{
			if (ascending(arr[minIndex], arr[i_current])) // replace comparison expression by ascending function
			{
				minIndex = i_current;
			}
		}

		swap(arr[i_start], arr[minIndex]); // std::swap
	}
}

Lúc này, chúng ta vẫn còn cần thêm một hàm selectionSort khác và thay thế biểu thức so sánh trong mệnh đề if bằng hàm descending để có thể sắp xếp mảng theo chiều giảm dần. Chúng ta cần thiết kế lại hàm selectionSort này sao cho người dùng có thể tùy chọn việc sắp xếp theo thứ tự tăng dần hay giảm dần theo từng thời điểm khác nhau. Chúng ta sẽ thêm vào tham số thứ 3 cho hàm selectionSort là một con trỏ hàm dùng để trỏ đến hàm ascending hoặc descending tùy vào lời gọi hàm selectionSort. Do hàm ascendingdescending có cấu trúc kiểu trả về và tham số hoàn toàn giống nhau, nên chúng ta có thể sử dụng chung một kiểu con trỏ hàm. Mình định nghĩa con trỏ hàm dùng làm tham số thứ 3 của hàm selectionSort như sau:

bool (*comparisonFunc)(int, int);

Bây giờ, chúng ta sẽ sửa lại hàm selectionSort thành phiên bản sử dụng con trỏ hàm:

bool ascending(int left, int right)
{
	return left > right;
}

bool descending(int left, int right)
{
	return left < right;
}

void selectionSort(int *arr, int length, bool (*comparisonFunc)(int, int))
{
	for (int i_start = 0; i_start < length; i_start++)
	{
		int minIndex = i_start;

		for (int i_current = i_start + 1; i_current < length; i_current++)
		{
			if (comparisonFunc(arr[minIndex], arr[i_current])) // use function pointer as ascending or descending function
			{
				minIndex = i_current;
			}
		}

		swap(arr[i_start], arr[minIndex]); // std::swap
	}
}

Lúc này, giả sử mình muốn sắp xếp mảng theo thứ tự giảm dần, mình sẽ sử dụng lời gọi hàm như sau:

int main()
{
	int arr[] = { 1, 4, 2, 3, 6, 5, 8, 9, 7 };
	int length = sizeof(arr) / sizeof(int);

	cout << "Before sorted: ";
	printArray(arr, length);

	selectionSort(arr, length, descending);

	cout << "After sorted:  ";
	printArray(arr, length);

	return 0;
}

Mình sử dụng địa chỉ của hàm descending làm đối số cho tham số thứ 3 của hàm selectionSort. Như vậy, hàm descending sẽ được sử dụng bên trong hàm selectionSort và mảng của chúng ta sẽ sắp xếp theo thứ tự giảm dần.

Before sorted: 1 4 2 3 6 5 8 9 7
After sorted:  9 8 7 6 5 4 3 2 1

Nếu muốn đổi ngược lại thứ tự của mảng khi sắp xếp, chúng ta chỉ cần thay đối số thứ 3 của hàm selectionSort là địa chỉ của hàm ascending:

int main()
{
	int arr[] = { 1, 4, 2, 3, 6, 5, 8, 9, 7 };
	int length = sizeof(arr) / sizeof(int);

	cout << "Before sorted: ";
	printArray(arr, length);

	selectionSort(arr, length, ascending);

	cout << "After sorted:  ";
	printArray(arr, length);

	return 0;
}

Với việc đặt thêm tham số thứ 3 của hàm selectionSort là 1 con trỏ hàm, chúng ta có thể thiết kế thêm nhiều tùy chọn cho điều kiện sắp xếp mảng một chiều khác nhau và vẫn có thể sử dụng cho hàm selectionSort. Ví dụ mình muốn thêm một kiểu sắp xếp có điều kiện khác là mọi số chẵn trong mảng sẽ đứng trước, các số lẻ trong mảng sẽ đứng sau, và phần chẵn hay phần lẻ đều được sắp xếp tăng dần, mình sẽ làm như sau:

bool evensFirst(int left, int right)
{
	//if left is even and right is odd, not need to swap
	if ((left % 2 == 0) && (right % 2 != 0))
		return false;

	//if left is odd and right is even, swap this couple
	if ((left % 2 != 0) && (right % 2 == 0))
		return true;

	return ascending(left, right);
}

Và mình chỉ cần sử dụng địa chỉ của hàm này làm đối số:

int main()
{
	int arr[] = { 1, 4, 2, 3, 6, 5, 8, 9, 7 };
	int length = sizeof(arr) / sizeof(int);

	cout << "Before sorted: ";
	printArray(arr, length);

	selectionSort(arr, length, evensFirst);

	cout << "After sorted:  ";
	printArray(arr, length);

	return 0;
}

Kết quả:

Before sorted: 1 4 2 3 6 5 8 9 7
After sorted:  2 4 6 8 1 3 5 7 9

Như các bạn thấy, sử dụng con trỏ hàm trong trường hợp này mang lại cho chúng ta một cách sử dụng hàm sắp xếp hiệu quả hơn.

Tham số mặc định của tham số con trỏ hàm

Chúng ta có thể cung cấp cho tham số con trỏ hàm một địa chỉ hàm cụ thể, và hàm đó sẽ được gọi mặc định nếu chúng ta không cung cấp đối số cho tham số con trỏ hàm. Ví dụ:

void selectionSort(int *arr, int length, bool (*comparisonFunc)(int, int) = ascending)
{
	for (int i_start = 0; i_start < length; i_start++)
	{
		int minIndex = i_start;

		for (int i_current = i_start + 1; i_current < length; i_current++)
		{
			if (comparisonFunc(arr[minIndex], arr[i_current])) // use function pointer as ascending or descending function
			{
				minIndex = i_current;
			}
		}

		swap(arr[i_start], arr[minIndex]); // std::swap
	}
}

Như vậy, lời gọi hàm selectionSort với 2 đối số sẽ được mặc định là sắp xếp mảng tăng dần.

std::function in C++11

Chuẩn C++11 cung cấp cho chúng ta một cách thay thế cho việc sử dụng con trỏ hàm bằng cách sử dụng kiểu dữ liệu function thuộc thư viện functional. Thư viện này cũng được định nghĩa trong namespace std nên chúng ta cần có dòng lệnh using namespace std; hoặc có thể khai báo là std::function.

Cú pháp khai báo biến kiểu std::function như sau:

std::function< <return_type>([list of parameters]) > varName;

Ví dụ:

std::function< bool(int, int) > comparisonFunc;

Mình lấy ví dụ về việc sử dụng kiểu dữ liệu std::function như sau:

#include <iostream>
#include <functional>
using namespace std;

void addOne(int &value)
{
	value++;
}

int main()
{
	function<void(int &)> func = addOne;
	
	int number = 5;
	func(number);

	cout << "New value: " << number << endl;

	return 0;
}

Như các bạn thấy, sử dụng kiểu dữ liệu std::function cũng tương tự như sử dụng con trỏ hàm, chỉ khác nhau về cách khai báo.


Tổng kết

Con trỏ hàm (function pointers) thường được sử dụng khi chúng ta có các hàm có cùng kiểu trả về và danh sách tham số. Đặt con trỏ hàm làm tham số của hàm cũng là một cách sử dụng con trỏ hàm khá phổ biến.

Vì con trỏ hàm có cú pháp khai báo khó nhớ hơn kiểu std::function, mình khuyến khích các bạn sử dụng kiểu std::function được định nghĩa bên trong thư viện functional của chuẩn C++11.

Bài tập cơ bản

1/ Sửa lại hàm selectionSort phiên bản sử dụng con trỏ hàm làm tham số thứ 3 sao cho phù hợp với cách định nghĩa của 2 hàm so sánh bên dưới:

bool ascending(int left, int right)
{
	return left < right;
}

bool descending(int left, int right)
{
	return left > right;
}

2/ Sử dụng con trỏ hàm để tạo hàm có thể thực hiện 4 phép toán cơ bản (+, -, *, /). Biết rằng, với mỗi phép toán chúng ta có một hàm có kiểu trả về float, mỗi hàm có 2 tham số kiểu float. Sau đó, viết hoàn thiện một chương trình đơn giản cho phép người dùng nhập vào toán tử (+, -, *, /) từ bàn phím và thực hiện phép toán với toán tử tương ứng thông qua hàm mà bạn đã định nghĩa.


Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.

Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn.

www.daynhauhoc.com

11 Likes

chỗ ascending với descending coi lại đi, ascending phải là return left < right; chứ

3 Likes

nên để ascending là left < right, vì mặc định hàm so sánh cho sắp xếp tăng dần là bé hơn.

http://en.cppreference.com/w/cpp/algorithm/sort

Sorts the elements in the range [first, last) in ascending order. The order of equal elements is not guaranteed to be preserved.

  1. Elements are compared using operator<.
1 Like

tại ascending là tăng dần, mà mảng tăng dần thì có phần tử bên trái luôn bé hơn (hoặc bằng) phần tử bên phải. Vì vậy hàm ascending thì nên return left < right (trái bé hơn phải) cho nó logic tí :joy:

để làm bài tập cũng được. Nói là ascending đáng lẽ phải là left < right, mà trong code trên thì left > right. Cho bài tập sửa lại left < right + sửa 1 dòng trong selectionSort cho ra đúng

A post was split to a new topic: Không hiểu đoạn code C++ nhỏ này. Mong mọi người giúp đỡ

1 Like

2 posts were split to a new topic: Sửa lỗi khởi tạo DSLK

đọc kĩ phần con trỏ và tham số hàm, không được nhảy cóc sẽ có câu trả lời

3 posts were split to a new topic: Trong java các khái niệm như delegate linq không?

1 Like

" Bộ nhớ ảo " là khái niệm mơ hồ và không tồn tại. Đã là bộ nhớ thì nó phải là cái gì đó RAM, ROM, Flash, …
Ảo ở đây là gì? Có thể ý niệm là volatile memory nhưng dĩ nhiên nó không ẢO. nó thật.

Bởi vì nó là abstraction mà. Hiểu như vậy thì hóa ra app cũng là ảo :slight_smile: vì chỉ có phần cứng mới là có thực.

Bộ nhớ volatile là RAM do cúp điện là mất tiêu trong vài giây, phải có nguồn điện (như pin) hoặc giữ trong nitơ lỏng.

1 Like

Dạ em khá chắc là em đã type đúng nhưng compiler vẫn báo lỗi ạ. mn giúp em hiểu với :frowning:

full code: https://yamcode.com/GauovzpIDE

/*Sử dụng con trỏ hàm để tạo hàm có thể thực hiện 4 phép toán cơ bản!*/
#include<iostream>
#include<cstdlib>
using namespace std;
float add(float a, float b) {
	return a + b;
}
float sub(float a, float b) {
	return a - b;
}
float mul(float a, float b) {
	return a * b;
}
float div(float a, float b) {
	return a / b;
}
void calculation(float a, float b, char opt) {
	switch (opt) {
		case '+': {
			cout << a << "+" << b << "=" << add(a, b) << endl;
			break;
		}
		case '-': {
			cout << a << "-" << b << "=" << sub(a, b) << endl;
			break;
		}
		case '*': {
			cout << a << "*" << b << "=" << mul(a, b) << endl;
			break;
		}
		case ':': {
			cout << a << ":" << b << "=" << div(a, b) << endl;
			break;
		}
	}
}
int main() {
	char opt;
	cout << "Input option: ";
	cin >> opt;
	float a, b;
	cout << "Input a: "; cin >> a;
	cout << "Input b: "; cin >> b;
	calculation(a, b, opt);
	return 0;
}

Bài này làm kiểu con trỏ hàm sao nhỉ? Em không hiểu đề cho lắm, ai giúp em với?

Thay vì 1 mớ switch case mình viết lại 1 chút

#include<iostream>
#include<cstdlib>
#include<functional>
using namespace std;
float add(float a, float b) {
	return a + b;
}
float sub(float a, float b) {
	return a - b;
}
float mul(float a, float b) {
	return a * b;
}
float divi(float a, float b) {
	return a / b;
}
function<float(int, int)> pCalculation;

int main() {
	float a, b;
	char opt;
	cout << "Please choose option:";
	cin >> opt;
	switch(opt){
	    case '+':
	    pCalculation = add;
	    break;
	    case '-':
	    pCalculation = sub;
	    break;
	    case '*':
	    pCalculation = mul;
	    break;
	    case '/':
	    pCalculation = divi;
	    break;
	    default:
	    cout << "Not supported function" << endl;
	    break;
	}
	cout << "Input a: "; cin >> a;
	cout << "Input b: "; cin >> b;
	cout << pCalculation(a, b)<<endl;
	return 0;
}
1 Like

Cứ tưởng hổng switch case thì phải như vầy chứ :stuck_out_tongue:

#include<iostream>

using namespace std;
float add(float a, float b) {
	return a + b;
}
float sub(float a, float b) {
	return a - b;
}
float mul(float a, float b) {
	return a * b;
}
float divi(float a, float b) {
	return a / b;
}

auto f(char opt){
    using X_t = float(*)(float, float);
    static const X_t x[] = {mul, add, sub, divi};
    return x[(++opt-42)/2];
}

int main() {
	float a, b;
	char opt;
	cin >> opt >> a >> b;
	cout << f(opt)(a, b)<<endl;
	return 0;
}
4 Likes

Cái hàm f của bác đỉnh quấ , bác giải thích qua hoặc cho e xin mấy khái niệm liên quan 2 dòng đầu của bác được không, em gg 1 hồi không ra luôn ^^

2 Likes

Thật ra cũng không rõ bạn cần tìm hiểu chỗ nào, nên thử liệt kê ra hết đây vậy: auto keyword, function pointers, type alias, list initialization, static keyword

Cơ mà code này 1 năm rồi nhìn lại thì code dùng make_arrayconstexpr chắc sẽ hay hơn:

constexpr auto f(char opt){
    return std::experimental::make_array(mul, add, sub, divi)[(++opt-42)/2];
}
3 Likes

make_array is removed in Library Fundamentals TS v3 because the deduction guide for std::array and std::to_array have been already in C++20.

C++20 có std::to_array rồi mà :unamused:

constexpr auto f(char opt){
    return std::to_array({imul, iadd, isub, idiv})[(opt - 41) / 2];
}

:relieved:

3 Likes

Ủa cho e hỏi sao máy em báo lỗi ở lệnh printArray(arr, length); vậy ạ?

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?