search

Указатели на функции

Указатель на функцию ссылается на входную точку этой функции. Указатель на функцию— это довольное сложное, но очень мощное средство C++. Несмотря на то что функция не является переменной, она, тем не менее, занимает физическую область памяти, некоторый адрес которой можно присвоить указателю. Адрес, присваиваемый указателю, является входной точкой функции. (Именно этот адрес используется при вызове функции.) Если некоторый указатель ссылается на функцию, то ее (функцию) можно вызвать с помощью этого указателя.

Указатели на функции также позволяют передавать функции в качестве аргументов другим функциям. Адрес функции можно получить, используя имя функции без круглых скобок и аргументов. (Этот процесс подобен получению адреса массива, когда также используется только его имя без индекса.) Если присвоить адрес функции указателю, то эту функцию можно вызвать через указатель. Например, рассмотрим следующую программу. Она содержит две функции, vline() и hline(), которые рисуют на экране вертикальные и горизонтальные линии заданной длины.


#include <iostream>
using namespace std;
void vline(int i), hline(int i);
int main()
{
void (*p)(int i);
p = vline; // указатель на функцию vline()
(*p)(4); // вызов функции vline()
p = hline; // указатель на функцию hline()
(*p)(3); // вызов функции hline()
return 0;
}
void hline(int i)
{
for( ; i; i--) cout << "-";
cout << "\n";
}
void vline(int i)
{
for( ; i; i--) cout << "|\n";
}
Вот как выглядят результаты выполнения этой программы.
I
I
I
I
- - -

Рассмотрим эту программу в деталях. В первой строке тела функции main() объявляется переменная р как указатель на функцию, которая принимает один целочисленный аргумент и не возвращает никакого значения. Это объявление не определяет, какая функция имеется в виду. Оно лишь создает указатель, который можно использовать для адресации любой функции этого типа. Необходимость круглых скобок, в которые заключен указатель *р, следует из С++-правил предшествования.

В следующей строке указателю р присваивается адрес функции vline(). Затем выполняется вызов функции vline() с аргументом 4. После этого указателю р присваивается адрес функции hline(), и с помощью этого указателя реализуется ее вызов.

В этой программе при вызове функций посредством указателя используется следующий формат:


(*p) (4);

Однако функцию, адресуемую указателем р, можно вызвать с использованием более простого синтаксиса:


p (4);

Единственная причина, по которой чаще используется первый вариант вызова функции, состоит в том, что всем, кто станет разбирать вашу программу, станет ясно, что здесь реализован вызов функции через указатель р, а не вызов функции с именем р. Во всем остальном эти варианты эквивалентны.

Несмотря на то что в предыдущем примере указатель на функцию используется только ради иллюстрации, зачастую такое его применение играет очень важную роль. Указатель на функцию позволяет передавать ее адрес другой функции. В качестве показательного примера можно привести функцию qsort() из стандартной С++-библиотеки. Функция qsort() — это функция быстрой сортировки, основанная на алгоритме Quicksort, который упорядочивает содержимое массива. Вот как выглядит ее прототип.


void qsort(void * start, size_t length, size_t size, int
(*compare) (const void *, const void *));

Функция qsort() — это функция сортировки из стандартной С++-библиотеки.

Прототип функции qsort() "прописан" в заголовке <cstdlib>, в котором также определен тип size_t (как тип unsigned int). Чтобы использовать функцию qsort(), необходимо передать ей указатель на начало массива объектов, который вы хотите отсортировать (параметр start), длину этого массива (параметр length), размер в байтах каждого элемента (параметр size) и указатель на функцию сравнения элементов массива (параметр *compare).

Функция сравнения, используемая функцией qsort(), сопоставляя два элемента массива, должна возвратить отрицательное значение, если ее первый аргумент указывает на значение, которое меньше второго, нуль, если эти аргументы равны, и положительное значение, если первый аргумент указывает на значение, которое больше второго.

Чтобы понять, как можно использовать функцию qsort(), рассмотрим следующую программу.


#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int comp(const void *a, const void *b);
int main()
{
char str[] = "Указатели на функции дают гибкость.";
qsort(str, strlen(str), 1, comp);
cout << "Отсортированная строка: " << str;
return 0;
}
int comp(const void *a, const void *b)
{
return * (char *) a - * (char *) b;
}
Вот как выглядят результаты выполнения этой программы.
Отсортированная строка: Уаааабгдезииииккклнностттуфцью

Эта программа сортирует строку str в возрастающем порядке. Поскольку функции qsort() передается вся необходимая ей информация, включая указатель на функцию сравнения, ее можно использовать для сортировки данных любого типа. Например, следующая программа сортирует массив целых чисел. Для гарантии переносимости при определении размера целочисленного значения в ней используется оператор sizeof.


#include <iostream>
#include <cstdlib>
using namespace std;
int comp(const void *a, const void *b);
int main()
{
int num[] = {10, 4, 3, 6, 5, 7, 8};
int i;
qsort(num, 7, sizeof(int), comp);
for(i=0; i < 7; i++)
cout << num[i] << ' ';
return 0;
}
int comp(const void *a, const void *b)
{
return * (int *) a - * (int *) b;
}

He стану утверждать, что указатели на функции не так просты для понимания, но практика поможет и "с ними найти общий язык". В отношении указателей на функции необходимо рассмотреть еще один аспект, связанный с перегруженными функциями.

Как найти адрес перегруженной функции

Получить адрес перегруженной функции немного сложнее, чем найти адрес обычной "одиночной" функции. Если же существует несколько версий перегруженной функции, то должен существовать механизм, который бы определял, адрес какой именно версии мы получаем. При получении адреса перегруженной функции именно способ объявления указателя определяет, адрес какой ее версии будет получен. По сути, объявление указателя в этом случае сравнивается с соответствующими объявлениями указателей перегруженных функций. Функция, объявление которой обнаружит совпадение, и будет той функцией, адрес которой мы получили.

В следующем примере программы содержится две версии функции space(). Первая версия выводит на экран count пробелов, а вторая— count символов, переданных в качестве аргумента ch. В функции main() объявляются два указателя на функции. Первый задан как указатель на функцию с одним целочисленным параметром, а второй — как указатель на функцию с двумя параметрами.


/* Использование указателей на перегруженные функции.
*/
#include <iostream>
using namespace std;
// Вывод на экран count пробелов.
void space(int count)
{
for( ; count; count--) cout << ' ';
}
// Вывод на экран count символов, переданных в ch.
void space(int count, char ch)
{
for( ; count; count--) cout << ch;
}
int main()
{
/* Создание указателя на void-функцию с одним int-параметром.
*/
void (*fp1) (int);
/* Создание указателя на void-функцию с одним int-параметром и
одним параметром типа char. */
void (*fp2)(int, char);
fp1 = space; // получаем адрес функции space(int)
fp2 = space; // получаем адрес функции space(int, char)
fp1(22); // Выводим 22 пробела (этот вызов аналогичен вызову
(* fp1) (22)) .
cout << "|\n";
fp2(30, 'х'); // Выводим 30 символов "х" (этот вызов
аналогичен вызову (*fp2) (30, 'x').
cout << "|\n";
return 0;
}
Вот как выглядят результаты выполнения этой программы.
I
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ I

Как отмечено в комментариях, компилятор способен определить, адрес какой перегруженной функции он получает, на основе того, как объявлены указатели fp1 и fp2.

Итак, когда адрес перегруженной функции присваивается указателю на функцию, то именно это объявление указателя служит основой для определения того, адрес какой функции был присвоен. При этом объявление указателя на функцию должно соответствовать одной (и только одной) из перегруженных функций. В противном случае в программу вносится неоднозначность, которая вызовет ошибку компиляции.