Главная страница
Навигация по странице:

  • Массивы указателей на функции.

  • Указатели на функции как параметры

  • Указатель на функцию как возвращаемое функцией значение.

  • Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник


    Скачать 1.57 Mb.
    НазваниеВ., Фомин С. С. Курс программирования на языке Си Учебник
    АнкорКурс на Си
    Дата18.02.2023
    Размер1.57 Mb.
    Формат файлаdocx
    Имя файлаПодбельский. Курс программирования на Си.docx
    ТипУчебник
    #943863
    страница16 из 42
    1   ...   12   13   14   15   16   17   18   19   ...   42

    Указатели при вызове функций. До сих пор мы рассматривали функцию как минимальный исполняемый модуль программы, об­мен данными с которым происходит через набор параметров и с помощью значений, возвращаемых функцией в точку вызова. Теперь перейдем к вопросу о том, почему в языке Си функция введена как один из производных типов. Необходимость в таком типе связана, например, с задачами, в которых функция (или ее адрес) должна выступать в качестве параметра другой функции или в качестве значения, возвращаемого другой функцией. Обратимся к уже рас­смотренному ранее выражению «вызов функции» (точнее, к более общему варианту вызова):

    обозначение_функции (список_аргументов)

    где обозначение_функции (только в частном случае это идентифи­катор) должно иметь тип «указатель на функцию, возвращающую значение конкретного типа».

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

    Самый употребительный указатель на функцию - это ее имя (идентификатор). Именно так указатель на функцию вводится в ее определении:

    тип имя_функции (спецификация_параметров) тело_функции

    Прототип

    тип имя_функции (спецификация_параметров);

    также описывает имя функции именно как указатель на функцию, возвращающую значение конкретного типа.

    Имя_функции в ее определении и в ее прототипе - указатель- константа. Он навсегда связан с определяемой функцией и не может быть «настроен» на что-либо иное, чем ее адрес. Для идентифика­тора имя_функции термин «указатель» обычно не используют, а го­ворят об имени функции.

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

    тип (*имя_указателя) (спецификация_параметров);

    где тип - определяет тип возвращаемого функцией значения; имя_ указателя - идентификатор, произвольно выбранный программис­том; спецификация_параметров - определяет состав и типы пара­метров функции.

    Например, запись

    int (*point) (void);

    определяет указатель-переменную с именем point на функции без параметров, возвращающие значения типа int.

    Важнейшим элементом в определении указателя на функции яв­ляются круглые скобки. Если записать

    int * funct (void);

    то это будет не определением указателя, а прототипом функции без параметров с именем funct, возвращающей значения типа int*.

    В отличие от имени функции (например, funct), указатель point является переменной, то есть ему можно присваивать значения других указателей, определяющих адреса функций программы. Принципиальное требование - тип указателя-переменной должен полностью соответствовать типу функции, адрес которой ему при­сваивается.

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

    point=funct; /* Ошибка - несоответствие типов */

    будет ошибочной, так как типы возвращаемых значений для point и funct различны.

    Неконстантный указатель на функцию, настроенный на адрес конкретной функции, может быть использован для вызова этой функции. Таким образом, при обращении к функции перед круглы­ми скобками со списком аргументов можно помещать: имя_функ- ции (то есть константный указатель); указатель-переменную того же типа, значение которого равно адресу функции; выражение разы­менования такого же указателя с таким же значением. Следующая программа иллюстрирует три способа вызова функций.

    #include

    void f1 (void) {

    printf ("\n Выполняется f1( )");

    }

    void f2 (void) {

    printf ("\n Выполняется f2( )");

    }

    void main ()

    {

    void (*point) (void);

    /*point - указатель-переменная на функцию */ f2 (); /* Явный вызов функции f2()*/ point=f2; /* Настройка указателя на f2()*/ (*point)(); /* Вызов f2() по ее адресу

    с разыменованием указателя */ point=f1; /* Настройка указателя на f1()*/ (*point)(); /* Вызов f1() по ее адресу

    с разыменованием указателя */

    point (); /* Вызов f1() без явного разыменования указателя */ }

    Результат выполнения программы:

    Выполняется f2( )

    Выполняется f2( )

    Выполняется f1( )

    Выполняется f1( )

    Все варианты обращения к функциям с использованием указа­телей-констант (имен функций) и указателей-переменных (некон­стантных указателей на функции) в программе продемонстрирова­ны и снабжены комментариями. Приведем общий вид двух опера­ций вызова функций с помощью неконстантных указателей:

    (*имя_указателя) (список_аргументов)

    имя_указателя (список_аргументов)

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

    *point (); /* Ошибочный вызов */

    Это полностью соответствует синтаксису языка. Операция '( )' - «круглые скобки» имеет более высокий приоритет, чем операция разыменования '*'. В этом ошибочном вызове вначале выполнится вызов point( ), а уж к результату будет применена операция разыме­нования.

    При определении указателя на функции он может быть инициа­лизирован. В качестве инициализирующего выражения должен ис­пользоваться адрес функции того же типа, что и тип определяемого указателя, например:

    int fic (char); /* Прототип функции */

    int (*pfic) (char)=fic;

    /* pfic - указатель на функцию */

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

    тип (*имя_массива [размер] ) (спецификация_параметров);

    где тип определяет тип возвращаемых функциями значений; имя_ массива - произвольный идентификатор; размер - количество эле­ментов в массиве; спецификация_параметров - определяет состав и типы параметров функций.

    Пример:

    int (*parray [4]) (char);

    здесь parray - массив указателей на функции, каждому из которых можно присвоить адрес определенной выше функции int fic (char) и адрес любой функции с прототипом вида

    int имя-функции (char);

    Массив в соответствии с синтаксисом языка является производ­ным типом наряду с указателями и функциями. Массив функций создать нельзя, однако, как мы показали, можно определить мас­сив указателей на функции. Тем самым появляется возможность создавать «таблицы переходов» (jump tables), или «таблицы пере­дачи управления». С помощью таблицы переходов удобно органи­зовывать ветвления с возвратом по результатам анализа некоторых условий. Для этого все ветви обработки (например, N+1 штук) оформляются в виде однотипных функций (с одинаковым типом возвращаемого значения и одинаковой спецификацией парамет­ров). Определяется массив указателей из N+1 элементов, каждому элементу которого присваивается адрес конкретной функции об­работки. Затем формируются условия, на основе которых долж­на выбираться та или иная функция (ветвь) обработки. Вводится индекс, значение которого должно находиться в пределах от 0 до N включительно, где (N+1) - количество ветвей обработки. Каждо­му условию ставится в соответствие конкретное значение индекса. По конкретному значению индекса выполняются обращение к эле­менту массива указателей на функции и вызов соответствующей функции обработки:

    имя_массива [индекс] (список_аргументов);

    (*имя массива [индекс]) (список_аргументов);

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

    #include /* Для функции exit ( ) */ #include #define N 2

    void act0(char * name) {

    printf("%s: Работа завершена!\n",name); exit(0);

    }

    void act1(char * name)

    { printf("%s: работа 1\n",name);

    }

    void act2(char * name) {

    printf("%s: работа 2\n",name);

    }

    void main()

    { /* Массив указателей на функции: */

    void (* pact[ ])(char *)={act0,act1,act2};

    char string[12];

    int number;

    printf('Дп^Вводито имя: ");

    scanf("%s",string);

    printf("Вводите номера работ от 0 до %d:\n",N); while(1)

    {

    scanf("%d",&number);

    /* Ветвление по условию */ pact[number](string);

    }

    }

    Пример выполнения программы:

    Вводите имя: Peter

    Вводите номера работы от 0 до 2:



    Peter: работа 1



    Peter: работа 1



    Peter: работа 2

    0

    Peter: Работа завершена!

    В программе для упрощения нет защиты от неверно введенных данных, то есть возможен выход индекса за пределы, определенные для массива pact[ ] указателей на функции. При такой ситуации результат непредсказуем.

    Указатели на функции как параметры позволяют создавать функ­ции, реализующие тот или иной метод обработки другой функции, которая заранее не определена. Например, можно определить функ­цию для вычисления определенного интеграла. Подынтегральная функция может быть передана в функцию вычисления интеграла с помощью параметра-указателя. Пример функции для вычисления определенного интеграла с помощью формулы прямоугольников:

    double rectangle

    (double (* pf)(double), double a, double b) {

    int N=20;

    int i;

    double h,s=0.0;

    h=(b-a)/N;

    for (i=0; i
    s+=pf(a+h/2+i*h);

    return h*s;

    }

    Параметры функции rectangle( ): pf - указатель на функцию с па­раметром типа double, возвращающую значение типа double. Это указатель на функцию, вычисляющую значение подынтегральной функции при заданном значении аргумента. Параметры a, b - преде­лы интегрирования. Число интервалов разбиения отрезка интегри­рования фиксировано: N=20. Пусть текст функции под именем rect.c сохранен в каталоге пользователя.

    Предположим, что функция rectangle( ) должна быть использована для вычисления приближенных значений интегралов (Абрамов С. А., Зима Е. В. Начала информатики. - М.: Наука, 1989. - С. 83):

    2 1/2

    f и и 4 cos2 х dx.

    J,(x+1)- J

    Программа для решения этой задачи может иметь следующий вид:

    #include

    #include

    #include "rect.c" /* Включение определения функции rectangle( ) */ double ratio(double x) /* Подынтегральная функция */ {

    double z; /* Вспомогательная переменная */ z=x*x+1;

    return x/(z*z);

    }

    double cos4_2(double v) /* Подынтегральная функция */ {

    double w; /* Вспомогательная переменная */ w=cos(v);

    return 4*w*w;

    }

    void main() {

    double a,b,c;

    a=-1.0;

    b=2.0;

    c=rectangle(ratio,a,b);

    printf("\n Первый интеграл: %f",c);

    printf("\n Второй интеграл: %f",

    rectangle(cos4_2,0.0,0.5));

    }

    Результат выполнения программы:

    Первый интеграл: 0,149847

    Второй интеграл: 1.841559

    Комментарии к тексту программы могут быть следующими. Ди­ректива #include "rect.c" включает на этапе препроцессорной обра­ботки в программу определение функции rectangle( ). Предполага­ется, как упомянуто выше, что текст этого определения находится в файле rect.c. В языке Си, как говорилось в главе 3, существует соглашение об обозначении имен включаемых файлов. Если имя файла заключено в угловые скобки '< >', то считается, что это один из файлов стандартной библиотеки компилятора. Например, файл <math.h> содержит средства связи с библиотечными математиче­скими функциями. Файл, название которого помещено в кавычках " ", воспринимается как файл из текущего каталога. Именно там в этом примере препроцессор начинает разыскивать файл rect.c.

    Определения функций ratio( ) и cos4_2( ), позволяющих вычис­лять значения подынтегральных выражений по заданному значению аргумента, ничем не примечательны. Их прототипы соответствуют требованиям спецификации первого параметра функции rectangle( ):

    double имя (double)

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

    Указатель на функцию как возвращаемое функцией значение. При организации меню в тех случаях, когда количество вариантов и соответствующее количество действий определяются не в точке исполнения, а в «промежуточной» функции, удобно возвращать из этой промежуточной функции адрес той конкретной функции, ко­торая должна быть выполнена. Этот адрес можно возвращать в виде значения указателя на функцию.

    Рассмотрим программу, демонстрирующую особенности такой организации меню.

    В программе определены три функции: первые две функции f1( ) и f2( ) с прототипом вида

    int f (void);

    (пустой список параметров, возвращается значение типа int) и третья функция menu( ) с прототипом

    int (*menu(void)) (void);

    которая возвращает значение указателя на функции с пустыми спис­ками параметров, возвращающие значения типа int.

    При выполнении функции menu( ) пользователю дается возмож­ность выбора из двух пунктов меню. Пунктам меню соответству­ют определенные выше функции f1( ) и f2( ), указатель на одну из которых является возвращаемым значением. При неверном выборе номера пункта возвращаемое значение становится равным NULL.

    В основной программе определен указатель r, который может принимать значения адресов функций f1( ) и f2( ). В бесконечном цикле выполняются обращения к функции menu( ), и если результат равен NULL, то программа печатает «The End» и завершает выпол­нение. В противном случае вызов

    t=(*r) ( );

    обеспечивает исполнение той из функций f1( ) или f2( ), адрес ко­торой является значением указателя r.

    Текст программы1:

    #include

    int f1(void)

    {

    printf(" The first actions: "); return 1;

    }

    int f2(void)

    {

    printf(" The second actions: "); return 2;

    }

    int (* menu(void))(void)

    {

    int choice; /* Номер пункта меню */

    /* Массив указателей на функции: */ int (* menu_items[])() = {f1, f2};

    printf("\n Pick the menu item (1 or 2): "); scanf("%d",&choice);

    if (choice < 3 && choice > 0)

    return menu_items[choice - 1];

    else

    return NULL;

    }

    1 Исходный вариант программы предложен С. М. Лавреновым.

    void main()

    {

    int (*r)(void); /* Указатель на функции */

    int t;

    while (1)

    { /* Обращение к меню: */

    r=menu();

    if (r == NULL)

    { printf("\nThe End!"); return;

    }

    /* Вызов выбранной функции */ t=(*r)();

    printf("\tt= %d",t);

    }

    }

    Результаты выполнения программы:

    Pick

    the

    menu

    item

    (1

    or

    2):

    2



    The

    second actions:

    t=

    2










    Pick

    the

    menu

    item

    (1

    or

    2):

    1



    The

    first

    actions:

    t=1













    Pick

    the

    menu

    item

    (1

    or

    2):

    24



    The End!

    В функции menu( ) определен массив menu_items[ ] указателей на функции. В качестве инициализирующих значений в списке ис­пользованы имена функций f1( ) и f2( ):
    1   ...   12   13   14   15   16   17   18   19   ...   42


    написать администратору сайта