9.3. Указатель на функцию. Динамическое связывание
Указатель на функцию - переменная, которая содержит адрес некоторой функции. Соответственно, косвенное обращение по этому указателю представляет собой вызов функции.
Определение указателя на функцию имеет вид:
C
int(*pf)();// без контроля параметров вызоваint(*pf)(void);// без параметров, с контролем по прототипуint(*pf)(int,char*);// с контролем по прототипу
В соответствии с принципом контекстного определения типа данных эту конструкцию следует понимать так: pf - переменная, при косвенном обращении к которой получается функция с соответствующим прототипом, например int_F(int, char*), то есть pf содержит адрес функции или указатель на функцию. Следует обратить внимание на то, что в определении указателя присутствует прототип - указатель ссылается не на произвольную функцию, а только на одну из функций с заданной схемой формальных параметров и результата.
Перед началом работы с указателем его необходимо назначить на соответствующий объект, в данном случае - на функцию. В синтаксисе Си выражение вида &имя_функции имеет смысл - начальный адрес функции или указатель на функцию. Кроме того, по аналогии с именем массива использование имени функции без скобок также интерпретируется как указатель на эту функцию. Указатель может быть инициализирован и при определении.
Естественно, что функция, на которую формируется указатель, должна быть известна транслятору - определена или объявлена как внешняя. Синтаксис вызова функции по указателю совпадает с синтаксисом ее определения.
C
n =(*pf)(1)+(*pp)(n);// эквивалентно
n =INC(1)+DEC(n);
Как и обычный указатель, указатель на функцию имеет размерность адреса, т.е. на уровне архитектуры является машинным словом, содержащим адрес функции в соответствии с системой формирования адресов процессором (системой адресации памяти). Это позволяет, в свою очередь, опускаться до таких машинно-зависимых действий как вызов функции по заданному адресу памяти, преобразуя целую константу к указателю на функцию.
C
void(*pf)(void)=(void(*)(void))0x1000;// Константа 1000 – шестнадцатеричный адресvoidmain(){(*pf)();}// функции, которую вызывает main// void(*)(void) –абстрактный тип данных –// указатель на функцию
Указатель на функцию прежде всего имеет отношение к системному программированию. Достаточно сказать, что кроме Си в явном виде он встречается только в Паскале (процедурный тип), но во многих случаях входит в скрытые механизмы управления программной средой, известные как динамическое связывание функций. Динамическое и статическое связывание мы уже обсуждали в 5.6., но там это относилось к переменным. По отношению к функции термин динамическое связывание следует понимать как установление соответствия между именем функции и ее адресом. Если рассматривать этот вопрос по отношению обычным функциям на Си, то можно выделить три этапа связывания:
если определение функции и ее вызов находятся в одном модуле (файле), то транслятору ничего не мешает вычислить относительный адрес функций внутри объектного модуля во время трансляции – статическое связывание;
при вызове внешней функции, определение которой находится в другом файле (а программный код – в другом объектном модуле), адрес функции становится известен при компоновке программного файла из объектных модулей. Хотя процедура компоновки выполняется после трансляции, данное связывание также называется статическим;
набор внешних функций (объектный модуль целиком) может быть оформлен в виде динамически связываемой библиотеки – DLL (dynamic linking library). Она загружается в память одновременно с программным модулем и связывание имени внешней функции с ее адресом в адресном пространстве DLL происходит при загрузке. Это связывание уже считается динамическим. При наличии в каждом приложении отображаемого адресного пространства (виртуальной памяти) программный код DLL может одновременно использоваться (разделяться) несколькими приложениями.
Если на уровне среды программирования или непосредственно в Си-программах имеет место элемент динамического связывания, то с уверенностью можно сказать, что в том или ином виде (явно или неявно) используется указатель на функцию:
уже упомянутые нами DLL, а также все возможные виды динамической загрузки внутреннего (исполняемого) программного кода;
виртуальные функции в Си++ (см.12.4.) – при конструировании объекта производного класса в базовый класс помещается указатель на таблицу виртуальных функций для производного класса (массив указателей на функции);
функции, работающие с произвольными типами данных – для настройки на конкретный тип данных в качестве «довеска» получают указатель на функцию, которая «знает», как с ним работать. В результате основной алгоритм изолируется от типа данных, с которым работает.
Таблицы функций, вызов по имени #
Тип данных вида void (*pp[])() расшифровывается в соответствии с контекстным определением типа данных как массив указателей на функции с общим прототипом (схемой передачи параметров и результата) – последовательность операций в контексте – массив – указатель – вызов функции. Образно, хотя и не совсем точно этот тип можно назвать таблицей функций, вызов которых может производиться по номеру (индексу).
В самом простом примере наличие таблицы функций позволяет сделать вызов функции по заданному символьному имени регулярным (циклическим). В принципе, то же самое можно сделать с помощью обычного переключателя (switch), но для этого придется еа каждую новую функцию добавлять фрагмент программного кода. Здесь же достаточно внести в массивы символьное имя и ее адрес.
C
externdoublesin(double);externdoublecos(double);externdoubletan(double);char*names[]={"sin","cos","tan",NULL};// Массив имен (указатели на строки)double(*pf[])(double)={ sin, cos, tan};// Таблица функций (адреса функций)//-------------------------------------------------------------------------------------93-01.cpp//---- Вызов функции по имени из заданного спискаdoublecall_by_name(char*pn,double arg){for(int i=0; names[i]!=NULL; i++)if(strcmp(names[i],pn)==0){// Имя найдено -return((*pf[i])(arg));// вызов функции по i-му}// указателю в массиве pfreturn0.;}
В принципе, используя технику работы с двоичными файлами и динамические массивы, можно было бы выполнить динамическую загрузку функций из программных файлов. Однако эта задача является архитектурно зависимой и требует учета особенностей перемещения программного кода и защиты памяти программы в операционной системе.
Указатель на функцию как средство параметризации алгоритма #
Оригинальность и обособленность указателя на функцию заключается в том, что указуемым объектом является не переменная (компонента данных программы), а функция (компонента алгоритма). Но сущность указателя при этом не меняется: если обычный указатель позволяет параметризовать алгоритм обработки данных, то указатель на функцию позволяет параметризовать сам алгоритм. Это значит, что некоторая его часть может быть заранее неизвестна (не определена, произвольна) и будет подключаться к основному алгоритму только в момент его выполнения (динамическое связывание).
рис. 93-1. Параметризация алгоритма
Для реализации указанного принципа основная функция должна получать необходимый для ее работы «довесок» в виде формального параметра – указателя на функцию. В качестве примера приведем функцию вычисления определенного интеграла, в которой подынтегральная функция (в математическом смысле) передается в виде вычисляющей ее функции (в «программистском» смысле), которая передается формальным параметром - указателем на функцию.
«Функция, вычисляющая функцию, предается в виде указателя на функцию»
C
//------------------------------------------------------------------93-02.cpp//------Численное интегрирование произвольной функцииdoubleINTEG(double a,double b,int n,double(*pf)(double))// a,b - границы интегрирования, n - число точек// pf - подынтегральная функция{double s,h,x;for(s=0., x=a, h =(b-a)/n; x <=b; x+=h) s +=(*pf)(x)* h;return s;}externdoublesin(double);voidmain(){printf("sin(0..pi/2)=%lf\n",INTEG(0.,M_PI/2,40,sin));}
Другой частный пример подобного рода – сортировка одной и той же структуры данных по разным критериям. В массиве структур типа user для задания способа сравнения используется указатель на внешнюю функцию сравнения, которая в качестве параметров получает две ссылки на сравниваемые структуры.
C
//------------------------------------------------------93-03.cpp//------Сортировка массива структур по разным критериямstructuser{char name[20];int account;double time;} S[]={…};//-------- Вставка погружением// cmp - указатель на функцию сравнения двух struct uservoidsort(user A[],int(*cmp)(user&,user&)){for(int i=1;A[i].name[0]!=0;i++)for(int j=i; j>0&&(*cmp)(A[j],A[j-1])<0; j--){ user c=A[j];
A[j]=A[j-1];// Обмен элементов массива -
A[j-1]=c;}// структурированных переменных}//--------- функция сравнения по имениintcmp_name(user &u1, user &u2){returnstrcmp(u1.name,u2.name);}//--------- функция сравнения по accountintcmp_account(user &u1, user &u2){return u1.account - u2.account;}//--------- функция сравнения по timeintcmp_time(user &u1, user &u2){return u1.time - u2.time;}voidmain(){sort(S,cmp_name);sort(S,cmp_account);sort(S,cmp_time);}
Этот пример можно обобщить на случай использования данных любого типа. В Си отсутствует понятие произвольный (неопределенный) тип, но понятие указатель на неопределенный (произвольный) тип существует – это void*. Однако прямое использование этого указателя невозможно. Какой же здесь выход?
Если существуют несколько идентичных структур данных, отличающихся типом хранимых данных, то один и тот же алгоритм будет отличаться только отдельными операциями, касающимися переменных этого типа. Например, при сортировке массива указателей на строки сравнение реализуется выражением вида strcmp(p[i],p[i-1]), а в массиве указателей на целые используется непосредственное сравнение p[i]<p[i-1]. Если эту операцию вынести за пределы алгоритма, реализовать отдельной функцией, а указатель на нее передавать в качестве параметра, то мы получим универсальную функцию сортировки массивов указателей на переменные любого типа данных. Можно назвать ее условно (хотя и не совсем точно) итератором.
Типичными итераторами являются:
итератор обхода (foreach), выполняющий для каждой переменной в структуре данных указанную функцию;
итераторы проверки и поиска по условию (firstthat), возвращающие указатель на первую (последнюю, очередную) переменную, которая удовлетворяет условию, проверяемому в функции;
итераторы, работающие на упорядоченных данных, использующие сравнение: сортировка, поиск минимального, двоичный поиск, включение с сохранением порядка.
Рис. 93-2. Структура итератора
Итератор являет собой пример универсализации алгоритма, основанный на разделении «компетенций» между различенными его частями:
структура данных, обрабатываемая итератором, содержит в своих элементах указатели на переменные произвольного (неизвестного для итератора) типа void*, но одинакового в каждом экземпляре структуры данных. Структура данных «знает», где хранятся переменные, но «не знает», как их обработать;
итератор получает в качестве параметров указатель на структуру данных и указатель на функцию обработки. Эта функция «знает», как обработать переменные, но «не знает», где их взять;
итератор выполняет алгоритм обработки структуры данных в соответствии со своим назначением: foreach - обходит все переменные, firstthat - обходит и проверяет все переменные, итератор сортировки -сортирует указатели на хранимые объекты (или соответствующие элементы структуры данных, например, элементы списка);
действие, которое надлежит выполнить над хранимыми объектами произвольного типа, реализуется вызовом внешней функции через указатель. Итератор извлекает из структуры данных указатели void* на хранимые данные и передает их внешней функции.
прототип (схема формальных параметров и результата) внешней функции различна для разных типов итераторов. Для foreach – указатель имеет вид void (pf)(void), для firstthat – int (pf)(void), для итераторов, использующих сравнение – int (cmp)(void,void*).
C
//------------------------------------------------------93-04.cpp//----- Итераторы foreach, firstthat и поиска минимального для спискаstructlist{ list *next;// Указатель на следующийvoid*pdata;};// Указатель на данные//----- Итератор: для каждого элемента спискаvoidForEach(list *pv,void(*pf)(void*)){for(; pv !=NULL; pv = pv->next)(*pf)(pv->pdata);}//----- Итератор: поиск первого в списке по условиюvoid*FirstThat(list *pv,int(*pf)(void*)){for(; pv !=NULL; pv = pv->next)if((*pf)(pv->pdata))return pv ->pdata;returnNULL;}//----- Итератор: поиск минимального в спискеvoid*FindMin(list *pv,int(*pf)(void*,void*)){ list *pmin;for( pmin=pv; pv !=NULL; pv = pv->next)if((*pf)(pv->pdata ,pmin->pdata)<0) pmin=pv;return pmin;}//----- Примеры использования итератора ------------------//----- Функция вывода строкиvoidprint(void*p){puts((char*)p);}//----- Функция проверки : длины строки >5intbigstr(void*p){returnstrlen((char*)p )>5;}//----- Функция сравнения строк по длинеintscmp(void*p1,void*p2){returnstrlen((char*)p1)-strlen((char*)p2);}//----- Вызов итераторов для статического списка,// содержащего указатели на строки
list a1={NULL,"aaaa"}, a2={&a1,"bbbbbb"}, a3={&a2,"ccccc"},*PH=&a3;//----- Итератор сортировки для массива указателейvoidSort(void**pp,int(*pf)(void*,void*)){int i,k;dofor(k=0,i=1; pp[i]!=NULL; i++)if((*pf)(pp[i-1],pp[i])>=0)// вызов функции сравнения{void*q;// перестановка указателей
k++; q = pp[i-1]; pp[i-1]= pp[i]; pp[i]= q;}while(k);}// Пример вызова итератора сортировки для массива// указателей на целые переменныеintcmp_int(void*p1,void*p2){return*(int*)p1-*(int*)p2;}int b1=5, b2=6, b3=3, b4=2;void*PP[]={&b1,&b2,&b3,&b4,NULL};voidmain(){char*pp;ForEach(PH,print);
pp =(char*)FirstThat(PH,bigstr);if(pp !=NULL)puts(pp);
pp =(char*)FindMin(PH,scmp);if(pp !=NULL)puts(pp);Sort(PP,cmp_int);for(int i=0; PP[i]!=NULL;i++)printf("%d ",*(int*)PP[i]);puts("");}
Лабораторный практикум #
Преобразовать функцию сортировки с использованием массивов (b), списков (6.3), деревьев (8.4, 8.5) в итератор. Проверить его работу на двух структурах данных содержащих указатели на различные типы (например, целые и строки). Массив преобразовать в массив указателей.
Головоломка #
Результат здесь очевиден, потому что не может быть другим. Будет выведена строка "I'm foo" или значение 6. Значительно труднее объяснить:
такое P - функция, указатель на функцию? Если функция, то где у нее определения формальных параметров и результата и что она делает, если указатель - то где обращение по нему;
где находится операция, по которой на самом деле производится вызов функции foo и inc.