Сначала рассмотрим указатели на обычные функции, а затем перейдем к функциям-членам класса.
Указатели на функции
Указатель на функцию -- это переменная, которая хранит адрес кода функции в памяти.
Примеры использования
1. Функции как аргументы других функций
Функции сортировки qsort
void qsort (void* base, size_t num, size_t size,
int (*compar)(const void*,const void*));
необходимо передать указатель на функцию compar
, задающую порядок сортировки (по возрастанию, по убыванию и т. п.).
2. Callback'и
или, если угодно, функции обратного вызова. Они вызываются как реакция программы на то или иное действие. Например, в коде графического интерфейса может встретится функция, создающая "кнопку"
void createButton(int x, int y, const char *text, function callback_func);
для чего нужно передать ей: координаты кнопки, текст надписи на ней, и функцию (тот самый callback), вызываемую при нажатии на кнопку.
Синтаксис
Рассмотрим следующий пример:
void (*func)(int);
Здесь func
-- указатель на функцию, принимающую один аргумент (int
) и возвращающую void
.
Подробнее? Извольте.
Представьте, что вы объявляете функцию func
, принимающую целочисленный аргумент и ничего не возвращающую:
void func(int);
Указатель на эту функцию обозначается как *func. Поскольку скобки имеют высший приоритет в сравнении со звездочкой, то запись
void *func(int);
вместо ожидаемого, даст объявление функции, возвращающей указатель на void. Чтобы *func сработало раньше, нужно его также взять в скобки. В итоге получим:
void (*func)(int);
Разобрать более сложные случаи поможет правило чтения по спирали.
Тем не менее, даже досконально разобравшись в синтаксисе указателей на функции, лучше использовать подобную запись как можно реже. Вместо этого рекомендуется использовать typedef
:
typedef void (*func)(int);
Инициализация
Чтобы инициализировать указатель на функцию, ему нужно присвоить адрес реальной функции. Ниже, указателю присваивается значение адреса функции my_int_func
:
#include <stdio.h>
void my_int_func(int x)
{
printf("%d\n", x);
}
int main()
{
typedef void (*func)(int);
/* the ampersand is actually optional */
func = &my_int_func;
return 0;
}
Вызов
Чтобы вызвать функцию my_int_func
, на которую указывает указатель func
, вы должны разыменовать этот указатель (*func)
(помним про скобки, изменяющие приоритет операторов!). Это работает, но в действительности все проще: компилятор трактует вызов указателя как вызов функции, на которую тот указывает, а разыменование происходит автоматически:
#include <stdio.h>
void my_int_func(int x)
{
printf("%d\n", x);
}
int main()
{
typedef void (*func)(int);
func = &my_int_func;
/* можно подробно */
(*func)(2);
/* но можно и проще */
func(2);
return 0;
}
Указатели на функции-члены класса
Постепенно подходим к основной теме статьи. Предыдущие примеры работали как в С, так и в С++. Теперь мы переходим к использованию классов и, следовательно, С++.
В примере ниже вводится тип данных function
и глобальная переменная этого типа. Методы класса A
служат для выбора нужной функции и ее вызова. Заметьте, что речь все еще идет об отдельной функции, а не о члене класса.
#include <iostream>
typedef void (*function)(void); // тип данных для функции
function switchFunc; // создадим переменную этого типа
// Функции, соответствующие типу function
void myFunc() { std::cout << "myFunc()" << std::endl; }
void yourFunc() { std::cout << "yourFunc()" << std::endl; }
class A
{
public:
void setFunc(function func);
void call();
};
// Устанавливает, какая функция будет вызываться
void A::setFunc(function func)
{
switchFunc = func;
}
// Вызывает выбранную функцию
void A::call()
{
std::cout << "Call ";
switchFunc();
}
int main()
{
A a;
a.setFunc(myFunc);
a.call();
a.setFunc(yourFunc);
a.call();
}
Результат выполнения:
Call myFunc()
Call yourFunc()
Как видно, использование указателей на функции в качестве аргументов методов класса ничего нового не привносит.
Метод класса, отличается от обычной функции, в частности, тем, что неявно содержит среди своих аргументов указатель на объект того класса, которому он принадлежит (this
).
В результате тип "указатель на функцию-член" должен отличаться от типа "указатель на функцию". Чем же?
Допустим, у нас есть функция:
void myFunc();
Если это -- обычная функция или статический метод класса, то ее тип нам уже известен:
typedef void (*function)(void);
А если это нестатический метод класса (к примеру, класса A
), то ее тип запишется так:
typedef void (A::*function)(void);
То есть, обращаться с указателями на статические методы можно также, как с указателями на обычные функции. Действительно, статический метод не связан с каким либо определенным объектом класса, не содержит среди своих аргументов указателя на конкретный объект, и по сути является обычной функцией, по каким-то соображениям добавленной в класс.
Далее мы будем рассматривать только нестатические методы, и использовать термины "функция-член класса" и "метод" как синонимы.
Изменим предыдущий пример, внося тип указатель-на-функцию и переменную этого типа внутрь класса:
#include <iostream>
class A
{
typedef void (A::*function)(void);
function switchFunc;
public:
void setFunc(function func);
void call();
void myFunc();
void yourFunc();
};
void A::myFunc() { std::cout << "myFunc()" << std::endl; }
void A::yourFunc() { std::cout << "yourFunc()" << std::endl; }
void A::setFunc(function func)
{
switchFunc = func;
}
void A::call()
{
std::cout << "Call ";
(this->*switchFunc)();
}
int main()
{
A a;
a.setFunc(&A::myFunc);
a.call();
a.setFunc(&A::yourFunc);
a.call();
}
Бросается в глаза различие в вызове метода и функции
switchFunc(); // для функции
(this->*switchFunc)(); // для метода
Действительно, нестатический метод класса относится к конкретному экземпляру класса (объекту) и потому вызывается через this->
, а так как switchFunc
-- это указатель, то понадобилось его разыменование.
Зачем нужны скобки, я уже говорил.
Обращает на себя внимание и вызов функции, соответствующей типу function
typedef void (A::*function)(void); // тип
&A::myFunc // вызов функции
Неожиданного здесь нет: раз мы задали такой тип -- указатель на функцию-член класса A
-- то должны и вызывать функции этого типа, добавляя к имени функции имя класса, не забыв про указатель.
Напомню, что ->*
используется, когда стоящий слева аргумент представляет собой указатель на объект (как в нашем случае this
), а .*
-- когда он является ссылкой на объект. Пример обращения к полям и методам через .*
приведен ниже:
#include <iostream>
class A
{
public:
int x;
void func(int y) { std::cout << "y = " << y << std::endl; }
typedef int A::*pointer_to_member;
typedef void (A::*pointer_to_function) (int);
};
int main()
{
A a;
A::pointer_to_member ptrToMember = &A::x;
A::pointer_to_function ptrToFunction = &A::func;
a.*ptrToMember = 10;
std::cout << "x = " << a.*ptrToMember << std::endl;
(a.*ptrToFunction) (20);
}
Борьба с ошибками
Возьмите себе за правило: как только нужно создать указатель на функцию -- использовать typedef
. Всегда.
Для упрощения громоздких вызовов вроде
(this->*switchFunc)();
можно использовать макросы. Например, такой:
#define CALL_MEMBER_FN(ptrToObject,ptrToMember) ((ptrToObject)->*(ptrToMember))
Тогда указанный выше вызов запишется как
CALL_MEMBER_FN(this,switchFunc)();
Это тот случай, когда использование макросов полезно.
Комментарии
comments powered by Disqus