Date Редакция Категория comp Теги C / Cpp

Сначала рассмотрим указатели на обычные функции, а затем перейдем к функциям-членам класса.

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

Указатель на функцию -- это переменная, которая хранит адрес кода функции в памяти.

Примеры использования

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