Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
int (* menu_items[ ]) ( ) = {f1, f2}; Такое определение массива указателей на функции по меньшей мере не очень наглядно. Упростить подобные определения можно с помощью вспомогательных обозначений (имен), вводимых спецификатором typedef. Например, то же самое определение массива указателей можно ввести так: typedef int (*Menu_action) (void); Menu_action menu_items [ ] = {f1, f2}; Здесь typedef вводит обозначение Menu_action для типа «указатель на функции с пустым списком параметров, возвращающие значения типа int». Библиотечные функции с указателями на функции в параметрах. Эти функции играют важную роль при решении задач на языке Си. В стандартную библиотеку компилятора с языка Си включены, по крайней мере, следующие функции: qsort ( ) - функция быстрой сортировки массива; search ( ) - функция поиска в упорядоченном массиве элемента с заданными свойствами. У каждой из этих функций один из параметров - указатель на функцию. Для функции быстрой сортировки нужен указатель на функцию, позволяющую задать правила упорядочения (сравнения) элементов. В функции поиска параметр - указатель на функцию позволяет задать функцию, с помощью которой программист должен сформулировать требования к искомому элементу массива. Опыт работы на языке Си показал, что даже не новичок в области программирования испытывает серьезные неудобства, разбирая синтаксис определения конструкций, включающих указатели на функции. Например, не каждому сразу становится понятным такое определение прототипа библиотечной функции: void qsort(void * base, size_t nelem, size_t width, int (*fcmp)(const void * p1, const void * p2)); Это прототип функции быстрой сортировки, входящей в стандартную библиотеку функций. Прототип находится в заголовочном файле stdlib.h. Функция qsort( ) сортирует содержимое таблицы (массива) однотипных элементов, неоднократно вызывая функцию сравнения, подготовленную пользователем. Для вызова функции сравнения ее адрес должен заместить указатель fcmp, специфицированный как параметр. При использовании qsort( ) программист должен подготовить таблицу сортируемых элементов в виде одномерного массива фиксированной длины и написать функцию, позволяющую сравнивать два любых элемента сортируемой таблицы. Остановимся на параметрах функции qsort( ): base - указатель на начало таблицы (массива) сортируемых элементов (адрес нулевого элемента массива); nelem - количество сортируемых элементов в таблице (целая величина, не большая размера массива) - сортируются первые nelem элементов от начала массива; width - размер элемента таблицы (целая величина, определяющая в байтах размер одного элемента массива); fcmp - указатель на функцию сравнения, получающую в качестве параметров два указателя p1, p2 на элементы таблицы и возвращающую в зависимости от результата сравнения целое число: если 7 8p1 < *p2, функция fcmp ( ) возвращает отрицательное целое < 0; если *p1 == *p2, fcmp ( ) возвращает 0; если *p1 > *p2, fcmp ( ) возвращает положительное целое > 0. При сравнении символ «меньше, чем» (<) означает, что после сортировки левый элемент отношения *p1 должен оказаться в таблице перед правым элементом *p2, то есть значение *p1 должно иметь меньшее значение индекса в массиве, чем *p2. Аналогично (но обратно) определяется расположение элементов при выполнении соотношения «больше, чем» (>). В следующей программе функция qsort( ) используется для упорядочения массива указателей на строки разной длины. Упорядочение должно быть выполнено таким образом, чтобы последовательный перебор массива указателей позволял получать строки в алфавитном порядке. Сами строки в процессе сортировки не меняют своих положений. Изменяются только значения указателей в массиве. "Six - 6", "Seven - 7", "Eight - 8" }; /* Размер таблицы: */ int n = sizeof(pc)/sizeof(pc[0]); int i; printf("\n До сортировки:"); for (i = 0; i < n; i++) printf("\npc [%d] = %p -> %s", i,pc[i],pc[i]); /* Вызов функции упорядочения: */ qsort((void *) pc,/* Адрес начала сортируемой таблицы */ n,/* Число элементов сортируемой таблицы */ sizeof(pc[0]), /* Размер одного элемента */ compare /* Имя функции сравнения (указатель) */ ); printf("\n\n После сортировки:"); for (i = 0; i < n; i++) printf("\npc [%d] = %p -> %s", i,pc[i],pc[i]); } Результаты выполнения программы: до сортировки:
после сортировки:
Вывод адресов, то есть значений указателей pc[i], выполняется в шестнадцатеричном виде с помощью спецификации преобразования %p. Обратите внимание на значения указателей pc[i]. До сортировки разность между pc[1] и pc[0] равна длине строки «One - 1» и т. д. После упорядочения pc[0] получит значение, равное исходному значению pc[7], и т. д. Для выполнения сравнения строк (а не элементов массива pc[ ]) в функции compare( ) использована библиотечная функция strcmp( ), прототип которой в заголовочном файле string.h имеет вид: int strcmp(const char *s1, const char *s2); Функция strcmp( ) сравнивает строки, связанные с указателями s1 и s2. Сравнение выполняется посимвольно, начиная с начальных символов строк и до тех пор, пока не встретятся несовпадающие символы либо не закончится одна из строк. Прототип функции strcmp( ) требует, чтобы параметры имели тип (const char *). Входные параметры функции compare( ) имеют тип (const void *), как предусматривает определение функции qsort( ). Необходимые преобразования для наглядности выполнены в два этапа. В теле функции compare( ) определены два вспомогательных указателя типа (unsigned long *), которым присваиваются значения адресов элементов сортируемой таблицы (элементов массива pc[ ]) указателей. В свою очередь, функция strcmp( ) получает разыменования этих указателей, то есть адреса символьных строк. Таким образом, выполняется сравнение не элементов массива char * pc[ ], а тех строк, адреса которых являются значениями pc[i]. Однако функция qsort( ) работает с массивом pc[ ] и меняет местами только значения его элементов. Последовательный перебор массива pc[ ] позволяет в дальнейшем получить строки в алфавитном порядке, что иллюстрирует результат выполнения программы. Так как pc[i] - указатель на некоторую строку, то по спецификации преобразования %s выводится сама строка. Если не использовать вспомогательных указателей pa, pb, то функцию сравнения строк можно вызвать из тела функции com- pare( ) таким оператором: return strcmp((char *)(*(unsigned long *)a), (char *)(*(unsigned long *)b)); где каждый указатель (void *) вначале преобразуется к типу (unsigned long *). Последующее разыменование «достает» из не скольких смежных байтов значение соответствующего указателя pc[i], затем преобразование (char *) формирует такой указатель на строку, который нужен функции strcmp( ). Функции с переменным количеством аргументов В языке Си допустимы функции, количество аргументов у которых при компиляции функции не фиксировано. Кроме того, могут быть неизвестными и типы аргументов. Количество и типы аргументов становятся известными только в момент вызова функции, когда явно задан их список. При определении и описании таких функций спецификация параметров заканчивается запятой и многоточием. Формат прототипа функции с переменным количеством аргументов: тип имя (спецификация_явных_параметров, ...); где тип - тип возвращаемого функцией значения; имя - имя функции; спецификация_явных_параметров - список спецификаций параметров, количество и типы которых фиксированы и известны в момент компиляции. Эти параметры можно назвать обязательными. Каждая функция с переменным количеством аргументов должна иметь хотя бы один обязательный параметр. После списка явных (обязательных) параметров ставится запятая, а затем многоточие, извещающее компилятор, что дальнейший контроль соответствия количества и типов аргументов при обработке вызова функции проводить не нужно. Сложность в том, что у переменного списка аргументов нет даже имени, поэтому не понятно, как найти его начало и конец. Каждая функция с переменным списком аргументов должна иметь механизм определения их количества и их типов. Принципиально различных подходов к созданию этого механизма два. Первый подход предполагает добавление в конец списка реально использованных (необязательных) аргументов специального аргумента-индикатора с уникальным значением, которое будет сигнализировать об окончании списка. При таком подходе в теле функции параметры последовательно перебираются, и их значения сравниваются с заранее известным концевым признаком. Второй подход предусматривает передачу в функцию сведений о реальном количестве аргументов. Эти сведения о реальном количестве используемых аргументов можно передавать в функцию с помощью одного из явно задаваемых (обязательных) параметров. В обоих подходах - и при задании концевого признака, и при указании числа реально используемых аргументов - переход от одного аргумента к другому выполняется с помощью указателей, то есть с использованием адресной арифметики. Проиллюстрируем сказанное примерами. Доступ к адресам параметров из списка. Следующая программа включает функцию с изменяемым списком параметров, первый из которых (единственный обязательный) определяет число действительно используемых при вызове необязательных аргументов. #include /*Функция суммирует значения своих аргументов типа int */ long summa(int k,...) /* k - число суммируемых аргументов */ { int *pick = &k; /* Настроили указатель на параметр k*/ long total = 0; for(; k; k--) total += *(++piok); return total; } void main() { printf("\n summa(2, 6, 4) = %d",summa(2,6,4)); printf("\n summa(6, 1, 2, 3, 4, 5, 6) = %d", summa(6,1,2,3,4,5,6)); } Результат выполнения программы: summa(2, 6, 4) = 10 summa(6, 1, 2, 3, 4, 5, 6) = 21 Для доступа к списку аргументов используется указатель pick типа int *. Вначале ему присваивается адрес явно заданного параметра k, то есть он устанавливается на начало списка аргументов в памяти. Затем в цикле указатель pirk перемещается по адресам следующих аргументов, соответствующих неявным параметрам. С помощью разыменования *(++pkk) выполняется выборка их значений. Параметром цикла суммирования служит значение k, которое уменьшается на 1 после каждой итерации и, наконец, становится нулевым. Особенность функции - возможность работы только с целочисленными аргументами, так как указатель pick после обработки значения очередного параметра «перемещается вперед» на величину sizeof(int) и должен быть всегда установлен на начало следующего аргумента. Недостаток функции - возможность ошибочного задания неверного количества реально используемых аргументов. Следующий пример содержит функцию для вычисления произведения переменного количества аргументов. Признаком окончания списка аргументов служит аргумент с нулевым значением. #include /* Функция вычисляет произведение аргументов:*/ double prod(double arg, ...) { double aa = 1.0; /* Формируемое произведение*/ double *prt = &arg; /* Настроили указатель на параметр arg */ if (*prt == 0.0) return 0.0; for (; *prt; prt++) aa *= *prt; return aa; } void main() { double prod(double,...);/* Прототип функции */ printf("\n prod(2e0, 4e0, 3e0, 0e0) = %e", prod(2e0,4e0,3e0,0e0)); printf("\n prod(1.5, 2.0, 3.0, 0.0) = %f", prod(1.5,2.0,3.0,0.0)); printf("\n prod(1.4, 3.0, 0.0, 16.0, 84.3, 0.0)=%f", prod(1.4,3.0,0.0,16.0,84.3,0.0)); printf( "\n prod(0e0) = %e",prod(0e0)); } Результат выполнения программы: prod(2e0, 4e0, 3e0, 0e0) = 2.400000е+01 prod(1.5, 2.0, 3.0, 0.0) = 9.000000 prod(1.4, 3.0, 0.0, 16.0, 84.3, 0.0) = 4.200000 prod(0e0) = 0.000000е+00 В функции prod( ) перемещение указателя prt по списку аргументов выполняется всегда за счет изменения prt на величину sizeof(double). Поэтому все параметры при обращении к функции prod( ) должны иметь тип double. В вызовах функции проиллюстрированы некоторые варианты задания аргументов. Обратите внимание на вариант с нулевым значением аргумента в середине списка. Аргументы вслед за этим значением игнорируются. Недостаток функции - неопределенность действий при отсутствии в списке аргумента с нулевым значением. Чтобы функция с переменным количеством аргументов могла воспринимать аргументы различных типов, необходимо в качестве исходных данных каким-то образом передавать ей информацию о типах параметров. Для однотипных параметров возможно, например, такое решение - передавать с помощью дополнительного обязательного параметра признак типа аргумента. Определим функцию, выбирающую минимальное из значений аргументов, которые могут быть двух типов: или только long, или только int. Признак типа аргумента будем передавать как значение первого обязательного параметра. Второй обязательный аргумент указывает количество параметров, из значений которых выбирается минимальное. В следующей программе предложен один из вариантов решения сформулированной задачи: #include void main() { /* Прототип функции: */ long minimum(char, int , ...); printf("\n\tminimum('l',3,10L,20L,30L) = %ld", minimum('l',3,10L,20L,30L)); printf("\n\tminimum('i',4,11, 2, 3, 4) = %ld", minimum('i',4,11,2,3,4)); printf( "\n\tminimum('k', 2, 0, 64) = %ld", minimum('k',2,0,64)); } /* Определение функции с переменным списком параметров */ long minimum(char z, int k,...) { if (z == 'i') { int *pi = &k + 1; /* Настроились на первый необязательный параметр */ int min = *pi; /* Значение первого необязательного параметра */ for(; k; k--, pi++) min = min > *pi ? *pi : min; return (long)min; } if (z == 'l') { long *pl = (long*)(&k+1); long min = *pl; /* Значение первого необязательного параметра */ for(; k; k--, pl++) min = min > *pl ? *pl : min; return (long)min; } printf("\nOiBu6Ka! Неверно задан 1-й параметр:"); return 2222L; } Результат выполнения программы: minimum('l', 3, 10L, 20L, 30L) = 10 minimum('i', 4, 11, 2, 3, 4) = 2 Ошибка! Неверно задан 1-й параметр: minimum('k',2,0,64)=2222 В приведенных примерах функций с изменяемыми списками аргументов перебор параметров выполнялся с использованием адресной арифметики и явным применением указателей нужных типов. К проиллюстрированному способу перехода от одного параметра к другому нужно относиться с осторожностью. Дело в том, что порядок размещения параметров в памяти ЭВМ зависит от реализации компилятора. В компиляторах имеются опции, позволяющие изменять последовательность размещения параметров. Стандартная для языка Си на современном ПК с процессором фирмы Intel последовательность: меньшее значение адреса у первого параметра функции, а остальные размещены подряд в соответствии с увеличением адресов. Противоположный порядок обработки и размещения будет у функций, определенных и описанных с модификатором pascal. Этот модификатор и его антипод - модификатор cdecl являются дополнительными ключевыми словами, определенными для ряда компиляторов. Не останавливаясь подробно на возможностях, предоставляемых модификатором pascal, отметим два факта. Во-первых, применение модификатора pascal необходимо в тех случаях, когда функция, написанная на языке Си, будет вызываться из программы, подготовленной на Паскале. Во-вторых, функция с модификатором pascal не может иметь переменного списка аргументов, то есть в ее определении и в ее прототипе нельзя использовать многоточие. |