Указатели на функции.
Прежде чем вводить указатель на функцию, напомним, что каждая функция
характеризуется типом возвращаемого значения, именем и сигнатурой. Напомним, что
сигнатура определяется количеством, порядком следования и типами параметров.
Иногда говорят, что сигнатурой функции называется список типов ее
параметров.
А теперь путём последовательности утверждений придем к обсуждению темы
данного раздела урока.
1. При использовании имени функции без последующих скобок и параметров имя
функции выступает в качестве указателя на эту функцию, и его значением служит
адрес размещения функции в памяти.
2. Это значение адреса может быть присвоено некоторому указателю, и затем уже
этот новый указатель можно применять для вызова функции.
3. В определении нового указателя должен быть тот же тип, что и возвращаемое
функцией значение, и та же сигнатура.
4. Указатель на функцию определяется следующим образом:
тип_функции (*имя_указателя)(спецификация_параметров);
|
Например: int (*func1Ptr) (char); - определение указателя func1Ptr на
функцию с параметром типа char, возвращающую значение типа int.
Примечание: Будьте внимательны!!! Если
приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е.
в виде int *fun (char); то компилятор воспримет ее как прототип некой функции с
именем fun и параметром типа char, возвращающей значение указателя типа int *.
Второй пример: char * (*func2Ptr) (char * ,int); - определение
указателя func2Ptr на функцию с параметрами типа указатель на char и типа int,
возвращающую значение типа указатель на char.
Иллюстрируем на практике.
В определении указателя на функцию тип возвращаемого значения и сигнатура
(типы, количество и последовательность параметров) должны совпадать с
соответствующими типами и сигнатурами тех функций, адреса которых предполагается
присваивать вводимому указателю при инициализации или с помощью оператора
присваивания. В качестве простейшей иллюстрации сказанного приведем программу с
указателем на функцию:
#include <iostream>
using namespace std;
void f1(void) // Определение f1.
{
cout << "Load f1()\n";
}
void f2(void) // Определение f2.
{
cout << "Load f1()\n";
}
void main()
{
void (*ptr)(void); // ptr - указатель на функцию.
ptr = f2; // Присваивается адрес f2().
(*ptr)(); // Вызов f2() по ее адресу.
ptr = f1; // Присваивается адрес f1().
(*ptr)(); // Вызов f1() по ее адресу.
ptr(); // Вызов эквивалентен (*ptr)();
}
Результат выполнения программы:
Load f2()
Load f1()
Load f1()
Press any key to continue
|
Здесь значением имени_указателя служит адрес функции, а с помощью операции
разыменования * обеспечивается обращение по адресу к этой функции. Однако будет
ошибкой записать вызов функции без скобок в виде *ptr();. Дело в том, что
операция () имеет более высокий приоритет, нежели операция обращения по адресу
*. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка
обратиться к функции ptr(). И уже к результату будет отнесена операция
разыменования, что будет воспринято как синтаксическая ошибка.
При определении указатель на функцию может быть инициализирован. В качестве
инициализирующего значения должен использоваться адрес функции, тип и сигнатура
которой соответствуют определяемому указателю.
При присваивании указателей на функции также необходимо соблюдать
соответствие типов возвращаемых значений функций и сигнатур для указателей
правой и левой частей оператора присваивания. То же справедливо и при
последующем вызове функций с помощью указателей, т.е. типы и количество
фактических параметров, используемых при обращении к функции по адресу, должны
соответствовать формальным параметрам вызываемой функции. Например, только
некоторые из следующих операторов будут допустимы:
char f1(char) {...} // Определение функции.
char f2(int) {...} // Определение функции.
void f3(float) {...} // Определение функции.
int* f4(char *){...} // Определение функции.
char (*pt1)(int); // Указатель на функцию.
char (*pt2)(int); // Указатель на функцию.
void (*ptr3)(float) = f3; // Инициализированный указатель.
void main()
{
pt1 = f1; // Ошибка - несоответствие сигнатур.
pt2 = f3; // Ошибка - несоответствие типов (значений и сигнатур).
pt1 = f4; // Ошибка - несоответствие типов.
pt1 = f2; // Правильно.
pt2 = pt1; // Правильно.
char с = (*pt1)(44); // Правильно.
с = (*pt2)('\t'); // Ошибка - несоответствие сигнатур.
}
|
Следующая программа отражает гибкость механизма вызовов функций с помощью
указателей.
#include <iostream>
using namespace std;
// Функции одного типа с одинаковыми сигнатурами:
int add(int n, int m) { return n + m; }
int division(int n, int m) { return n/m; }
int mult(int n, int m) { return n * m; }
int subt(int n, int m) { return n - m; }
void main()
{
int (*par)(int, int); // Указатель на функцию.
int a = 6, b = 2;
char c = '+';
while (c != ' ')
{
cout << "\n Arguments: a = " << a <<", b = " << b;
cout << ". Result for c = \'" << c << "\':";
switch (c)
{
case '+':
par = add;
c = '/';
break;
case '-':
par = subt;
c = ' ';
break;
case '*':
par = mult;
c = '-';
break;
case '/':
par = division;
c = '*';
break;
}
cout << (a = (*par)(a,b))<<"\n"; //Вызов по адресу.
}
}
Результаты выполнения программы:
Arguments: a = 6, b = 2. Result for c = '+':8
Arguments: a = 8, b = 2. Result for c = '/':4
Arguments: a = 4, b = 2. Result for c = '*':8
Arguments: a = 8, b = 2. Result for c = '-':6
Press any key to continue
|
Цикл продолжается, пока значением переменной c не станет пробел. В каждой
итерации указатель par получает адрес одной из функций, и изменяется значение c.
По результатам программы легко проследить порядок выполнения ее операторов.
Массивы указателей на функции.
Указатели на функции могут быть объединены в массивы. Например, float
(*ptrArray[4]) (char) ; - описание массива с именем ptrArray из четырех
указателей на функции, каждая из которых имеет параметр типа char и возвращает
значение типа float. Чтобы обратиться, например, к третьей из этих функций,
потребуется такой оператор:
float а = (*ptrArray[2])('f');
|
Как обычно, индексация массива начинается с 0, и поэтому третий элемент
массива имеет индекс 2.
Массивы указателей на функции удобно использовать при разработке всевозможных
меню, точнее программ, управление которыми выполняется с помощью меню. Для этого
действия, предлагаемые на выбор будущему пользователю программы, оформляются в
виде функций, адреса которых помещаются в массив указателей на функции.
Пользователю предлагается выбрать из меню нужный ему пункт (в простейшем случае
он вводит номер выбираемого пункта) и по номеру пункта, как по индексу, из
массива выбирается соответствующий адрес функции. Обращение к функции по этому
адресу обеспечивает выполнение требуемых действий. Самую общую схему реализации
такого подхода иллюстрирует следующая программа для "обработки файлов":
#include <iostream>
using namespace std;
/* Определение функций для обработки меню
(функции фиктивны т. е. реальных действий не выполняют):*/
void act1 (char* name)
{
cout <<"Create file..." << name;
}
void act2 (char* name)
{
cout << "Delete file... " << name;
}
void act3 (char* name)
{
cout << "Read file... " << name;
}
void act4 (char* name)
{
cout << "Mode file... " << name;
}
void act5 (char* name)
{
cout << "Close file... " << name;
}
void main()
{
// Создание и инициализация массива указателей
void (*MenuAct[5])(char*) = {act1, act2, act3, act4, act5};
int number; // Номер выбранного пункта меню.
char FileName[30]; // Строка для имени файла.
// Реализация меню
cout << "\n 1 - Create";
cout << "\n 2 - Delete";
cout << "\n 3 - Read";
cout << "\n 4 - Mode";
cout << "\n 5 - Close";
while (1) // Бесконечный цикл.
{
while (1)
{ // Цикл продолжается до ввода правильного номера.
cout << "\n\nSelect action: ";
cin >> number;
if (number>>= 1 && number <= 5) break;
cout << "\nError number!";
}
if (number != 5)
{
cout << "Enter file name: ";
cin >> FileName; // Читать имя файла.
}
else break;
// Вызов функции по указателю на нее:
(*MenuAct[number-1])(FileName);
} // Конец бесконечного цикла.
}
|
Пункты меню повторяются, пока не будет введен номер 5 -
закрытие.