Указатели на функции
Прежде чем вводить указатель на функцию, напомним, что каждая функция характеризуется типом возвращаемого значения, именем и сигнатурой. Напомним, что сигнатура определяется количеством, порядком следования и типами параметров. Иногда говорят, что сигнатурой функции называется список типов ее параметров. При использовании имени функции без последующих скобок и параметров, имя функции выступает в качестве указателя на эту функцию, и его значением служит адрес размещения функции в памяти. Это значение адреса может быть присвоено другому указателю, затем уже этот новый указатель можно применять для вызова функции. Однако в определении нового указателя должен быть тот же тип, что и возвращаемое функцией значение, и та же сигнатура. Указатель на функцию определяется следующим образом:
тип_функции (*имя_указателя)(список_формальных_параметров);
Например:
int (*funclPtr) (char); - определение указателя funclPtr на функцию с параметром типа char, возвращающую значение типа int.
Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int *fun (char); то компилятор воспримет ее как прототип функции с именем fun и параметром типа char, возвращающей значение указателя типа int
Второй пример:
char * (*func2Ptr) (char * ,int); - определение указателя func2Ptr на функцию с параметрами типа указатель на char и типа int, возвращающую значение типа указатель на char.
В определении указателя на функцию тип возвращаемого значения и сигнатура (типы, количество и последовательность параметров) должны совпадать с соответствующими типами и сигнатурами тех функций, адреса которых предполагается присваивать вводимому указателю при инициализации или с помощью оператора присваивания. В качестве простейшей иллюстрации сказанного приведем программу с указателем на функцию:
//Программа 6.13
#include "stdafx.h"
#include <iostream>
void f1(void) { // Определение f1
std::cout<< "\nExecute f1()";
}
void f2(void) { // Определение f2
std::cout<<"\nExecute f2()";
}
void main(){
void (*ptr)(void); // ptr - указатель на функцию
ptr = f2; // Присваивается адрес f2()
(*ptr)(); // Вызов f2() по ее адресу
ptr = f1; // Присваивается адрес f1()
(*ptr) (); // Вызов f1() no ее адресу
ptr(); // Вызов эквивалентен (*ptr)();
}
Результат выполнения программы:
Выполняется f2 () Выполняется f1() Выполняется f1()
В программе описан указатель ptr на функцию, и ему последовательно присваиваются адреса функций f2 и f1. Заслуживает внимания форма вызова функции с помощью указателя на функцию:
(*имя_указателя)(список_фактических_параметров);
Здесь значением имени_указателя служит адрес функции, а с помощью операции разыменования * обеспечивается обращение по адресу к этой функции. Однако будет ошибкой записать вызов функции без скобок в виде *ptr(). Дело в том, что операция () имеет более высокий приоритет, нежели операция обращения по адресу *. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка обратиться к функции ptr(). И уже к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка.
При определении указатель на функцию может быть инициализирован. В качестве инициализирующего значения должен использоваться адрес функции, тип и сигнатура которой соответствуют определяемому указателю.
При присваивании указателей на функции, также необходимо соблюдать соответствие типов возвращаемых значений функций и сигнатур для указателей правой и левой частей оператора присваивания. То же справедливо и при последующем вызове функций с помощью указателей, т.е. типы и количество фактических параметров, используемых при обращении к функции по адресу, должны соответствовать формальным параметрам вызываемой функции. Например, только некоторые из следующих операторов будут допустимы:
char f1(char) { . . . } // Определение функции
char f2(int) {...} // Определение функции
void f3(float) {...} // Определение функции
int* f4(char *){...} // Определение функции
char (*ptl)(int); // Указатель на функцию
char (*pt2)(int); // Указатель на функцию
void (*ptr3)(float) = f3; // Инициализированный указатель
void main(){
ptl = f1; // Ошибка - несоответствие сигнатур
pt2 = f3; // Ошибка - несоответствие типов
// (значений и сигнатур)
ptl = f4; // Ошибка - несоответствие типов
ptl = f2; // Правильно
pt2 = ptl; // Правильно
char с = (*ptl)(44); // Правильно
с = (*pt2)('\t'); // Ошибка - несоответствие сиг-натур
}
Указатели на функции могут быть объединены в массивы. Например,
float (*ptrArray)(char)[4]; - описание массива с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа float. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор:
float а = (*ptrArray[2])('f');
Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2.
Приведенное описание массива ptrArray указателей на функции не особенно наглядное и ясное. Для функций с большим количеством параметров и сложным описанием возвращаемых значений оно может стать и вовсе непонятным. Ситуация особенно усложняется, когда такое описание типа должно входить в более сложные описания, например, когда указатели функций и их массивы используются в качестве параметров других функций. Для удобства последующих применений и сокращения производных описаний рекомендуется с помощью спецификатора typedef вводить имя типа указателя на функции:
typedef unsigned int UINT;
UINT a = 3;
typedef float (*PTF)(float);
typedef char *(*PTC)(char);
typedef void (*PTFONC)(PTF, int, float);
Здесь ptf - имя типа "указатель на функцию с параметром типа float, возвращающую значение типа float". ptc - имя типа "указатель на, функцию с параметром типа char, возвращающую указатель на тип char”. ptfunc - имя типа "указатель на функцию, возвращающую пустое значение (типа void)". Параметрами для этой функции служат: ptf - указатель на функцию float имя (float), выражение типа int и выражение типа float. (В определение имени типа ptfunc вошел только что определенный тип с именем ptf.)
Введя имена типов указателей на функции, проще описать соответствующие указатели, массивы и другие производные типы:
PTF ptfloat1, ptfloat2[5]; //Указатель и массив указателей
//на функции float имя(float)
PTC ptchar; // Указатель на функцию char *(char)
PTFUNC ptfunc[8]; // Массив указателей на функции
Опыт работы на языках Си и Си++ показал, что даже не новичок в области программирования испытывает серьезные неудобства, разбирая синтаксис определения конструкций, включающих указатели на функции. Например, не каждому сразу становится понятным такое определение прототипа функции [6,11]:
void qsort(void *base, size_t nelem, size_t width,
int (*fcmp)(const void *pl, const void *p2));
Это прототип функции быстрой сортировки, входящей в стандартную для системы UNIX и для языка ANSI Си библиотеку функций. Прототип находится в заголовочном файле stdlib.h. Разберем элементы прототипа и напишем программу, использующую указанную функцию. Функция qsort() сортирует содержимое таблицы однотипных элементов, постоянно вызывая функцию сравнения, подготовленную пользователем. Для вызова функции сравнения ее адрес должен заместить указатель fcmp, специфицированный как формальный параметр. Итак, для использования qsort() программист должен подготовить таблицу сортируемых элементов в виде одномерного массива фиксированной длины и написать функцию, позволяющую сравнивать два любых элемента сортируемой таблицы. Остановимся на параметрах функции qsort ():
base - указатель на начало таблицы сортируемых элементов (адрес 0-го элемента массива);
nelem - количество элементов в таблице (целая величина, не больше размера массива);
width - размер элемента таблицы (целая величина, определяющая в байтах размер одного элемента массива);
fcmp - указатель на функцию сравнения, получающую в качестве параметров два указателя p1, р2 на элементы таблицы и возвращающую в зависимости от результата сравнения целое число:
если *p1 < *р2, femp возвращает целое < 0;
если *p1 = *p2, fcmp возвращает 0;
если *p1 > *p2, fcmp возвращает целое > 0.
При сравнении символ "меньше чем" (<) означает, что после сортировки левый элемент отношения *pl должен оказаться в таблице перед правым элементом *р2, т.е. значение *pi должно иметь меньшее значение индекса в массиве, нежели *р2. Аналогично (но обратно) определяется расположение элементов при выполнении соотношения "больше чем" (>).
В следующей программе функция qsort() используется для упорядочения массива указателей на строки разной длины. Упорядочение должно быть выполнено таким образом, чтобы последовательный перебор массива указателей позволял получать строки в алфавитном порядке. Сами строки в процессе сортировки не меняют своих положений. Изменяются только значения указателей в массиве.
//Программа 6.14
#include "stdafx.h"
#include <iostream>
#include <stdlib.h> // Для функции qsort()
#include <string.h> // Для сравнения строк- .strcmpO
// Определение функции для сравнения:
int sravni(const void *a, const void *b) {
unsigned long *pa = (unsigned long *)a,
*pb = (unsigned long *)b;
return strcmp((char *)*pa, (char *)*pb);
}
void main() {
char *pc[] = {"Ivanov", "Cidorov", "Petrov", "Antonov"};
int i, n = sizeof(pc)/sizeof(pc[0]);
std::cout << "\n before sorter:\n";
for(int i = 0; i < n; i++) std::cout<<"\t"<< pc[i];
qsort((void *)pc, // Адрес начала сортируемой "Таблицы
n, // Количество элементов сортируемой таблицы
sizeof(pc[0]), // Размер одного элементе
sravni); // Имя функции сравнения (указатель)
std::cout << "\n After sorter:\n";
for(i = 0; i < n; i++) std::cout<<"\t"<< pc[i];
getchar();
}
Результат выполнения программы:
До сортировки:
Иванов Сидоров Петров Антонов
После сортировки:
Антонов Иванов Петров Сидоров
Для выполнения сравнения строк (а не элементов массива рс[ ]) в функции sravni() использована библиотечная функция strcmp(), прототип которой в заголовочном файле string. h имеет вид
int strcmp(const char *sl, const char *s2) ;
Функция strcmp() выполняет беззнаковое сравнение строк, связанных с указателями s1 и s2. Сравнение выполняется без учета регистров набора букв латинского алфавита. Функция выполняет сравнение посимвольно, начиная с начальных символов строк и до тех пор, пока не встретятся несовпадающие символы либо не закончится одна из строк.
Прототип функции strcmp() требует, чтобы параметры имели тип (const char *). Входные параметры функции sravni() имеют тип (const void *), как предусматривает определение функции qsort(). Необходимые преобразования для наглядности выполнены в два этапа. В теле функции sravni() определены два вспомогательных указателя типа (unsigned long *), которым присваиваются значения адресов элементов сортируемой таблицы (элементов массива рс[ ]) указателей). В свою очередь, функция strcmp() получает адреса символьных строк. Таким образом, выполняется сравнение не элементов массива char* рс[ ], а тех строк, адреса которых являются значениями pc[i]. Однако функция qsort() работает с массивом рс[] и меняет местами только значения его элементов. Последовательный перебор массива рс[] позволяет в дальнейшем получить строки в алфавитном порядке, что иллюстрирует результат выполнения программы. Так как pc[i] - указатель на некоторую строку, то его разыменование в операции вывода « в поток cout выполняется автоматически.
Если не использовать вспомогательных указателей ра, рb, то функцию сравнения строк можно вызвать из тела функции sravni() таким оператором:
return strcmp((char *)(*(unsigned long *)a), (char *)(*(unsigned long *)b));
Здесь каждый родовой указатель (void *) вначале преобразуется к типу (unsigned long *). Последующее разыменование "достает" из четырех смежных байтов значение соответствующего указателя pc[i]. И уж затем преобразование (char *) формирует указатель на строку, который нужен функции strcmp().
С помощью данного примера можно также пояснить технологию callback функций. В данном случае функция sravni является callback-функцией, которая будет вызыана функцией qsort при наступлении некоторого события (в данном случае при возникновении необходимости сравнить два элемента массива) внутри qsort.
Аналогично, при реализации оконных приложений windows, мы обязаны написать в соответствии с некоторым шаблоном функцию, указатель на которую необходимо передать в операционую систему при создании ею окна нашего приложения. Когда возникает событие (клик мышкой, нажатие клавиши на клавиатуре и т.д.) операционная система вызывает нашу функцию (именно поэтому мы можем называть её callback функцией), чтобы она обработала это событие в соответствии с алгоритмом, который мы сами реализовали.
Если проводить аналогии между двумя этими примерами, то qsort - это операционая система windows, sravni - это callback функция нашего оконого приложения. Требование qsort к семантике функции sravni - это шаблон callback функции нашего оконого приложения, в соответствии с которым она должна быть реализована, чтобы появилась возможность передать указатель на неё операционной системе.
Перегрузка функций
Перегрузкой функций называется ситуация, когда в программе имеются несколько объявлений функций с одним и тем же именем, но разным количиством или типом формальных параметров. В этомм случае компилятор по типу и количеству фактических параметров в вызове функции определяет, какую из объявлений функции использовать.
Например, пусть у нас имеются два объявления двух функций с одтнаковыми именами, но разным типом формальных параметров:
void MyPrint(double i){std::cout<<”double ” <<i;} //первое объявление функции
void MyPrint(int i){std::cout<<”int ”<<i;} //второе объявление функции
и один вызов MyPrint(1). В этом случае мы можем говорить, что происходит перегрузка функции MyPrint. Так как фактический параметр является константой типа int, то компилятор свяжет этот вызов со вторым объявлением функции MyPrint.
Ситуация может быть более запутанной, если типы фактических параметров в вызове функции не совпадают ни с одним из формальных параметров в объявлении функции. Например, будет ли компилироваться вызов MyPrint(‘1’)? В этом вызове фактический параметр это константа типа char. Объявления функции MyPrint с таким формальным параметром нет. Тем не менее, вызов произойдёт, так как компилятор будет руководствоваться следующими правилами:
1. Пытается найти полное совпадение типов.
2. Если точного совпадения типов не найдено, то Си++ пытается найти совпадение путём дальнейшего стандартного неявного преобразования типов, определённых для фундаментальных типов данных.
3. Если нет стандартного преобразования типов, то компилятор пытается найти преобразование типа, определенного программистом.
В даном случае при вызове MyPrint(‘1’) сработает второе правило, фактический параметр типа char будет неявно преобразован к типу int и будет вызвана вторая функция MyPrint.
Проектные задания
1. Набрать и отладить программу 6.1, выполнить её в пошаговом режиме, в функции print задать значение по умолчанию для параметра vilue.
2. Набрать и отладить программу 6.2, объяснить, как функция с переменным количеством параметров обменивается данными при её вызове.
3. Набрать и отладить программу 6.3, объяснить чем способ передачи данных в функцию в программе 6.3 отличается от программы 6.2
4. Набрать и отладить программу 6.4, нарисовать её алгоритм, рассказать об быстрой сортировки
5. Написать на языке Си++ несколько примеров подставляемых функций
6. Набрать и отладить программу 6.5, объяснить чем отличается способ передачи данных в функцию по указателю от передачи данных по значению.
7. Набрать и отладить программу 6.6 и 6.7, написать свою функцию, вычисляющую сумму элементов массива.
8. Напишите функцию, принимающую в качестве параметров указатели (или ссылки) на две строки и возвращающую указатель на новую строку, являющуюся результатом слияния этих строк.
9. Приведите примеры передачи функции как параметра динамического и статического массивов памяти.
10. Напишите функцию, принимающую указатель (или ссылку) на матрицу в качестве параметра и возвращающую указатель на копию этой матрицы, повернутую по часовой стрелке.
11. Напишите функцию, принимающую указатель (или ссылку) на матрицу в качестве параметра и возвращающую указатель на копию этой матрицы, повернутую против часовой стрелки.
12. Напишите функцию, принимающую указатель (или ссылку) на матрицу в качестве параметра и возвращающую указатель на копию этой матрицы, транспонированную относительно неглавной диагонали.
13. Напишите функцию, принимающую указатель (или ссылку) на матрицу в качестве параметра и возвращающую указатель на копию этой матрицы, повернутую зеркально вниз.
14. Напишите функцию, принимающую указатель (или ссылку) на матрицу в качестве параметра и возвращающую указатель на копию этой матрицы, повернутую зеркально вправо
Дата добавления: 2020-12-11; просмотров: 376;