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

Уже было отмечено, что указатели могут указывать на самые разнообразные типы объектов в программе на языке C++. Точнее, на все и на любые виды объектов, являющиеся данными в программе.

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

#include <iostream>

#include <math.h>

using namespace std;

 

// площадь круга радиуса R

double area(double R)

{

const double PI = 3.1415;

return PI * R * R;

}

 

int main()

{

setlocale(LC_ALL, "rus");

double r = 1.0;

cout << "для круга радиуса " << r << " площадь = "

             << area(r) << endl;

double(*pfunc)(double);    // переменная указатель на функцию

pfunc = area;

 

cout << "для круга радиуса " << r << " площадь = "

             << (*pfunc)(r) << endl;

return 0;

}

Здесь с функцией area() всё ясно: она вычисляет площадь круга того радиуса, который предан ей, как параметр. Но позже мы объявляем переменную указатель на функцию:

double (*pfunc)( double );

В момент этого объявления указатель pfunc представляет собой ничего более, как некоторый адрес во внутреннем представлении компьютера (4 байта в 32-бит операционной системе, 8 байт в 64-бит операционной системе). Это в точности тот же внутренний вид, который имеет, скажем, указатель на целочисленную переменную int*. Но этот указатель имеет свой строго определённый тип: указатель на функцию, принимающую один параметр типа double, и возвращающую значение типа double. Но вот на какую конкретно функцию указывает указатель, в точке его определения — не важно: значение указателя не определено.

А вот следующим оператором присвоения мы привязываем указатель на функцию к конкретной функции area(). Абсолютно правильным было бы записать присвоение указателю адреса функции: pfunc = &area. Но компилятор C++ настолько умён, что и упоминание имени функции в операторе присвоения интерпретирует как её адрес. Таким образом запись pfunc = area также совершенно корректная. С этого момента мы можем использовать указатель pfunc для вызова функции на которую он указывает. Для этого мы записываем значение на которое указывает указатель *pfunc (операция * в таком контексте называется разыменованием указателя). Скобки вокруг разыменованного значения pfunc в записи (*pfunc)( r ) нужны из соображений приоритетов раскрытия операций в выражении. Выполнение этого примера:

Пока использование указателей на функции не принесло ничего принципиально нового в рассматриваемом примере, кроме некоторых замысловатостей синтаксиса. Но в этом примере мы пока только узнали каким образом можно определить и использовать указатели на функции. А теперь мы можем перейти к вопросу зачем это нужно и как мы можем это использовать.

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

В этой ситуации мы можем поступить так (ex2.cc):

#include <iostream>

using namespace std;

 

void test1(void)

{

cout << "выполняется тест №1" << endl;

// ...

}

 

void test2(void)

{

cout << "выполняется тест №2" << endl;

// ...

}

 

void test3(void)

{

cout << "выполняется тест №3" << endl;

// ...

}

 

void(*tests[])(void) =  // массив указателей на функции

{  

test1, test2, test3 // последовательность тестов

// ... и много других тестов

};

 

int main()

{

setlocale(LC_ALL, "rus");

 

for (int i = 0; i < sizeof(tests) / sizeof(tests[0]); i++)

{

tests[i]();

}

return 0;

}

И вот что мы имеем на исполнении:

В этом листинге tests[ ] — это массив указателей на функции без параметров и без возвращаемых значений. Красивое решение, не правда ли? Мы можем не задумываясь добавлять новые функции массива вызываемых тестов, и они все последовательно будут вызываться при выполнении.

Мы можем пойти ещё дальше: если указатель на функцию представляет функцию в выражениях, можно передать указатель на функцию в качестве параметра другой, охватывающей функции, и последняя будет выполнять в своём теле переданную параметром функцию, не зная какое действие при этом выполняется.

Для иллюстрации всего сказанного создадим программу самого примитивного калькулятора, выполняющего арифметические действия над целочисленными операндами (ex3.cc):

#include <iostream>

#include <stdlib.h>

using namespace std;

 

int calculate(int op1, int op2, int(*func)(int, int))

{

return func(op1, op2);

}

 

/* сложение, вычитание, умножение, деление,

остаток, возведение в степень: */

int summ(int op1, int op2) { return op1 + op2; }

int diff(int op1, int op2) { return op1 - op2; }

int mult(int op1, int op2) { return op1 * op2; }

int divd(int op1, int op2) { return op1 / op2; }

int bals(int op1, int op2) { return op1 % op2; }

int powr(int op1, int op2)

{

int ret = 1;

while (op2--) ret *= op1;

return ret;

}

 

typedef int(*fint_t)(int, int); // fint_t - указатель на функцию

// функции 6-ти арифметических операций

fint_t foper[] = // массив указателей на функции

{

summ, diff, mult, divd, bals, powr

};

 

int main()

{

setlocale(LC_ALL, "rus");

char coper[] = { '+', '-', '*', '/', '%', '^' };

int noper = sizeof(coper) / sizeof(coper[0]);

do

{

char buf[120];

char *str = buf;

char *endptr;

char oper;

 

cout << "выражение для вычисления (<op1><знак><op2>): " << flush;

cin >> buf;

 

int op1, op2;

 

op1 = strtod(str, &endptr);

oper = *endptr++;

op2 = strtod(str = endptr, &endptr);

 

int i;

for (i = 0; i < noper; i++)

{

if (oper == coper[i]) {

cout << op1 << ' ' << oper << ' ' << op2 << " = "

<< calculate(op1, op2, foper[i]) << endl;

break;

}

}

if (i == noper)

cout << "неверный знак операции: " << oper << endl;

} while (true);

 

return 0;

}

Здесь собственно вычисление выполняет функция calculate(). Но она ничего не «знает» о выполняемых действиях и о арифметических операциях вообще: она применяет к 2-м первым своим параметрам одну из 6-ти функций, которую ей передали 3-м параметром для выполнения.

В этом коде функция strtod() — стандартная функция библиотеки языка C (ANSI C, стандарта POSIX), которая извлекает десятичное число из строки, полученной со стандартного потока ввода. В контексте наших обсуждений это интересно тем, что:

  • программа на C++ может использовать всё множество библиотечных вызовов языка C;
  • программа C++ использует на этапе выполнения разделяемые библиотеки языка C (.so или .dll), и при отсутствии стандартной библиотеки C, программы на C++ становятся неработоспособными.

Но вернёмся к работе показанного калькулятора (его предельная упрощённость связана с тем, что мы не занимаемся проблемами ввода, его формата и не обрабатываем ошибки ввода пользователем — в реальных программах так делать нельзя):

Наблюдательный читатель мог бы заметить, что функция calculate() при всём своём желании и не могла бы выполнить ни одно из требуемых арифметических действий, так как выполняющие эти действия функции sum()dif()mul() и div() описаны позже функции calculate() и не видимы в коде функции calculate().

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