Основы программирования на С++ для начинающих

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

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

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

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

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

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

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

Таким образом запись pfunc = area также совершенно корректная. С этого момента мы можем использовать указатель pfunc для вызова функции на которую он указывает. Для этого мы записываем значение на которое указывает указатель *pfunc (операция * в таком контексте называется разыменованием указателя).

Скобки вокруг разыменованного значения pfunc в записи (*pfunc)( r ) нужны из соображений приоритетов раскрытия операций в выражении. Выполнение этого примера:

указатель на функцию, с++, программирование для начинающих, c++

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

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

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

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

указатель на функцию, с++, программирование для начинающих, c++

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

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

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

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

Кто не знает о typedef читайте тут.

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

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

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

указатель на функцию, с++, программирование для начинающих, c++

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

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

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

Смотрите также урок Указатели на объекты

18 thoughts on “Указатели на функции

  1. Лишняя точка с запятой в первом листинге после функции double area(double R).

    “некоторый адрес во внутреннем представлении компьютера”
    Просто “некоторый адрес”, к чему эта добавка про “внутреннее представление”? – она что-то добавляет к сути?

    “нового в рассматриваемом примерt”
    опечатка.

    “можно передать указатель на функцию в качестве параметра другой, охватывающей функции, ”
    Что за охватывающая функция? Что она охватывает?

    “Кто не знает о typedef – читайте тут.”
    Ссылка замечательная, но лучше написать про это тут (это же совсем не много).

    Последний пример унылый. ИМХО по теме указателей на функции надо было сказать про это вцелом (как в первом примере статьи), потом привести пример с стандартной функцией qsort, которая принимает указатель на функцию сравнения элементов. Ну и завершить все чем-то вроде функции поиска корня методом половинного деления (это очень простой пример и в нем точно также как в qsort удобно передавать функцию в качестве параметра).

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

    Ну это ИМХО.

    1. > Просто “некоторый адрес”, к чему эта добавка про “внутреннее представление”? – она что-то добавляет к сути?

      Конечно добавляет. Потому как, с точки зрения типизации C, а особенно C++, синтаксически “просто адресов” или “некоторых адресов” не существует вообще (разве что исключая void*). Все указатели типизированные (и тем радикально различаются друг от друга). И только во внутреннем машинном представлении указатель на функцию-метод класса, скажем, и char* – уже ничем не различаются, они просто неразличимы.

      1. > Конечно добавляет. Потому как, с точки зрения типизации C, а особенно C++, синтаксически “просто адресов” или “некоторых адресов” не существует вообще (разве что исключая void*). Все указатели типизированные … И только во внутреннем машинном представлении указатель на функцию-метод класса, скажем, и char* – уже ничем не различаются, они просто неразличимы.

        Адрес – он и в Африке адрес, независимо на чем ты пишешь.

        #include

        int main() {
        int val = 12345;
        int *intp = &val; // типа типизированный указатель
        char *charp; // еще один типизированный указатель
        charp = (char*)intp;

        std::cout << (int) *charp << " " << *(int*)charp << std::endl;
        }

        Тут показано что указатель на int можно скастовать в указатель на char.
        Ну а потом можно обратиться к данным по этому адресу. В первом случае разыменовывается указатель char*, поэтому выбирается первый байт по адресу переменной val. Во втором случае указатель char* кастуется в int* и разыменовывается, поэтому берутся уже все 4 байта.

        Вывод: 57 12345

        В любом случае с адресами в Си можно работать как угодно. Ты можешь брать данные по любому адресу, считывать их и записывать по ним что угодно.

        Например ты мог бы дописать в конец этой программы что-то типа:
        *charp = 56;

        std::cout << val << std::endl;

        то-есть по адресу переменной val можно заменить первый байт, работая с ним через указатель на char. А можно и не первый байт, ведь в Си все хорошо с адресной арифметикой.

    2. > Последний пример унылый. ИМХО по теме указателей на функции надо было

      А вот по этому поводу: не “надо было” – а надо было взять и написать как свой текст, так и свои примеры. А не тоскливо рассказывать “как надо бы было бы”.

      1. Я и пишу “свои тексты” :). К критике относись спокойней. Особенно при том, что уроки у тебя не получаются.

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

        “Контекст наших обсуждений” – это “указатели на функции” и в нем нам вообще не интересно что там происходит с разделяемыми библиотеками.

        Тем более, что достоверность этой инфы может вызывать сомнения. Ты говоришь, что программы на С++ используют “разделяемые библиотеки” языка Си. Термин “разделяемая библиотека” предполагает, что библиотека одновременно используется несколькими процессами.

        Дак вот, библиотеки включаются внутрь каждого исполняемого файла, они нифига не “разделяемые”. Как разделяемые они может быть используются при отладке, но релизы собираются с опцией –static всегда. Иначе твоя программа не запустится на компьютере, если на нем не установлен компилятор той же версии, которую ты использовал при сборке программы и не будут прописаны соответствующие пути в системную переменную PATH. Ну или ты можешь, конечно, запускать все программы, написанные на С++ из одного каталога, в корень которого поместить соответствующие .dll/.so, но так никто не делает

      2. int powr(int op1, int op2)
        {
        int ret = 1;
        while (op2–) ret *= op1;
        return ret;
        }

        Это типа возведение целую в степень. Мало того, что это дико неэффективно и может быть заменено использованием стандартной функции. Так оно еще и упадет при отрицательном значении op2.

        PS. Если писать статью про указатели на функции в С++, то ИМХО стоит рассказать про std::functional. С одой стороны в заголовке заявлено С++, а не Си. С другой – funtional почти со всех точек зрения лучше и удобней. Например я через него могу лямбду передавать (да и любой другой callable объект), а через указатель на функцию – не могу.

        Но вместо этого автор написал какую-то недостоверную информацию про “разделяемые библиотеки”.

      3. Совершенно согласен со следующим коментатором, уроки у тебя НЕ получаются.

  2. int main()
    {
    //…
    return 0; // уже 4 года эта строчка лишняя. Если программа не завершилась аварийно, она *автоматически* вернет нулевой код ошибки. Т.е. ты тут выполняешь работу, которую и без тебя выполняет компилятор

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

    В этой ситуации ни в коем случае нельзя поступать так, как ты пишешь. Все твои старания дико далеки от “разработки через тестирование”. Посмотри на google test framework или на boost unit test framework или на 100500 аналогов.

    >>
    for (int i = 0; i > за счёт умного компилятора C++, который по контексту понимает, что должны использоваться адреса функций

    Пол статьи ты рассказываешь про “умный компилятор”. Реально там ничего особо умного нет – если за именем функции нет круглой скобки, то стопудово это не вызов функции, а указатель на нее. Никаких извратов типа &function я нигде кроме твоей статьи не видел.

    1. Может и так, но мне как новичку стало понятно что просто название функции это по сути адрес функции, так как то понятнее в плане абстрактного понимания

  3. Как же трудно у меня все обстоит с этими указателями:(
    Интересный урок и заставляет много думать. Наверное это хорошо, но некоторые моменты для меня так и остались непонятными. Особенно с массивом указателей на функции и может кто-нибудь ответить мне на счет typedef.
    typedef int(*fint_t)(int, int);
    fint_t foper[] =
    {
    summ, diff, mult, divd, bals, powr
    };
    Здесь имеется ввиду, что указатель на функцию превращается в тип данных?
    Объясните пожалуйста…

    1. typedef не вводит нового типа, он только определяет короткий синоним для длинного написания “указатель на функцию вот такого типа”.
      А дальше определяется массив таких указателей на функции (с именем foper) и он тут же явно инициализируется. Поскольку из инициализации выводится размер массива, то при его описании мы можем его не указывать ([]).

  4. Понятно, что sizeof возвращающий длину в байтах, но вот условие:
    i < sizeof(tests) / sizeof(tests[0])
    никак не могу понять. Поясните пжл.

    1. Что может быть проще?
      Так получается число элементов массива.
      Если массив 1-байтных элементов (char[]) содержит 10 элементов, то это 10/1=10.
      Если массив 100-байтных элементов (структур)содержит 10 элементов, то это 1000/100= … и опять же 10.

      1. Дайте побольше задачек по темам на этом сайте…спасибо за уроки!

    2. Значит так , sizeof(tests) возвращает объем памяти(называйте как хотите) какой: 1)Используется при работе массива , если он был описан вот так
      [code] void (*tests[])(void) = {test1 , test2 , test3 , test4}[/code] – здесь будет 4 елемента. 2)А вот если использовать другое объявление: [code]void (*tests[5])(void) = {test1 , test2 , test3 , test4}[/code]- здесь как ни крути , а памяти выделено на 5 элементов , по сему [code]sizeof(tests) / sizeof(tests[0])[/code]
      равно 5 , а не 4 как можно было подумать. По этому лучше в данном случае не указывать размер массива.Так как в цикле мы попытаемся на пустом месте вызвать функцию и получим ошибку выполнения

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *