Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
Указатели при вызове функций. До сих пор мы рассматривали функцию как минимальный исполняемый модуль программы, обмен данными с которым происходит через набор параметров и с помощью значений, возвращаемых функцией в точку вызова. Теперь перейдем к вопросу о том, почему в языке Си функция введена как один из производных типов. Необходимость в таком типе связана, например, с задачами, в которых функция (или ее адрес) должна выступать в качестве параметра другой функции или в качестве значения, возвращаемого другой функцией. Обратимся к уже рассмотренному ранее выражению «вызов функции» (точнее, к более общему варианту вызова): обозначение_функции (список_аргументов) где обозначение_функции (только в частном случае это идентификатор) должно иметь тип «указатель на функцию, возвращающую значение конкретного типа». В соответствии с синтаксисом языка указатель на функцию - это выражение или переменная, используемые для представления адреса функции. По определению, указатель на функцию содержит адрес первого байта или первого слова выполняемого кода функции (арифметические операции над указателями на функции запрещены). Самый употребительный указатель на функцию - это ее имя (идентификатор). Именно так указатель на функцию вводится в ее определении: тип имя_функции (спецификация_параметров) тело_функции Прототип тип имя_функции (спецификация_параметров); также описывает имя функции именно как указатель на функцию, возвращающую значение конкретного типа. Имя_функции в ее определении и в ее прототипе - указатель- константа. Он навсегда связан с определяемой функцией и не может быть «настроен» на что-либо иное, чем ее адрес. Для идентификатора имя_функции термин «указатель» обычно не используют, а говорят об имени функции. Указатель на функцию как переменная вводится отдельно от определения и прототипа какой-либо функции. Для этих целей используется конструкция: тип (*имя_указателя) (спецификация_параметров); где тип - определяет тип возвращаемого функцией значения; имя_ указателя - идентификатор, произвольно выбранный программистом; спецификация_параметров - определяет состав и типы параметров функции. Например, запись 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 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); } } Результаты выполнения программы:
The End! В функции menu( ) определен массив menu_items[ ] указателей на функции. В качестве инициализирующих значений в списке использованы имена функций f1( ) и f2( ): |