Как вызвать функцию в c
В эмбеддинг-разработке может встретиться следующая задача: имея адрес функции в виде обычного числа, надо вызвать функцию по данному адресу. В простейшем случае предполагается, что функция не имеет аргументов, и ничего не возвращает. Организация вызова функци может выглядеть следующим образом:
void jump_to_funct(uint32_t stack, uint32_t func)
Что здесь происходит? Перед прототипом функции jump_to_funct прописан атрибут naked , благодаря которому компилятор для данной функции не будет создавать пролог и эпилог. То есть, код данной функции будет содержать только команды, которые получатся в результате компиляции строки 4 и 5.
В функцию jump_to_func передаются два числа. Первое число — это адрес вершины стека. Второе число — это адрес функции, т. е. адрес первой машинной команды вызываемой функции.
На строке 4 происходит вызов инстрикт-функции __set_MSP(stack) . Данная функция установит указатель стека на значение, переданное в переменной stack . Пременная stack содержит адрес в виде беззнакового целочислоительного числа.
В строке 5 происходит вызов функции, расположенной по адресу func , причем этот адрес так же задается просто в виде беззнакового целочислительного числа.
По идее, если нужно сделать простой вызов функции без смены адреса вершины стека, строку 4 можно не писать.
Проинициализировать значения stack и func можно следующим образом:
uint32_t *boot_from = (uint32_t*)MAIN_RUN_FROM;
if (((boot_from[0] >> 24) != (SRAM_BASE >> 24)) &&
((boot_from[0] >> 24) != (CCMDATARAM_BASE >> 24)))
return send_str(«SRAM ERROR\r»);
if (((boot_from[1] >> 24) != (FLASH_BASE >> 24)) && (boot_from[1] > MAIN_RUN_FROM))
return send_str(«FLASH ERROR\r»);
В данном случае предполагается, что данный код вызывается до функции main(). По-сути, данный код вызывает функцию main() по указанному адресу. Полный код похожего проекта можно посмотреть на GitHub:
- Стандарт языка программирования Си
- Передача параметра в функцию по указателю в C стиле
- Передача параметров в функцию по указателю (C стиль) и по ссылке (C++ стиль)
- Передача структуры в функцию и изменение значений элементов структуры
- В чем разница объявления строки как массива и как указателя
- Использование очень больших чисел
- getch() в Linux
- Аргументы функции main()
- Как побайтно считать файл
- Реализация циклического сдвига ROR и ROL
- Динамическая загрузка библиотек в Linux
- Функция fmemopen() — открытие набора байт как файла в памяти с получением файлового дескриптора
- Подмена дефайнов (#define)
- Успешной отладки, шутка
- Как описывать функции с аргументами по-умолчанию в C/C++
- Форматированный вывод через функцию printf
- Создание и удаления двухмерного и трехмерного динамического массива
- Как передать в функцию двумерный массив, размер которого известен
- Как передать в функцию динамический двумерный массив
- Как правильно читать объявления в Си
- Вычисление pbkdf2 на языке C
- Определение разрядности платформы 32 или 64 бит
- Функции getuid() и geteuid()
- Указатели и символьные строки в языке C
- Практическое применение LD_PRELOAD или замещение функций в Linux
- Проблемы C-подобных языков. Где находиться типу: справа или слева? (Теория)
- Откуда в языке Си появился синтаксис указателей, и для чего он предназначался изначально
- Как в языке Си вызвать функцию, для которой известен адрес вызова в виде числа
- Вызов функции по известному адресу в языке Си — абстрактный тип данных указателя на функцию
- Структуры в языке Си. Определения структур в сравнении с языком C++
- Структуры в языке Си. Указатели на структуры
- Структуры в языке Си. Массивы структур
- Операция запятая «,» в языке Си и Си++
- Самый быстрый и оптимальный способ копирования строк в Cи и C++
- Стоит запомнить: цикл for в языке Си/Си++ — это цикл с предусловием
- Как происходит компиляция C/C++ кода. Единица трансляции
Вызов функции в си
Эта статья — перевод этой статьи. Некоторые детали в ней устарели, некоторые являются специфичными для окружения, но в целом полно излагают поведение при вызове функции. Данный материал позволит глубже понять механизм вызова, специфичные ошибки, особенности вызова функций с переменным числом параметров и возможные методы оптимизации. Кроме того, она будет полезна, если вы захотите вызвать функцию или написать собственную на ассемблере.
Ниже описано поведение стека при вызове функции в си. Детали соответствуют действительности для компилятора gcc на платформе intel pentium. Существует множество способов и соглашений создать и заполнить фрейм: разные процессоры, операционные системы и компиляторы могут делать это по-разному.
Вызов функции
Р ассмотрим функцию со следующим прототипом
int foo (int arg1, int arg2, int arg3);
Функция имеет 2 локальных переменных. Далее будем считать, что sizeof(int) равен 4 байтам. Пусть функция main вызывает функцию foo. Регистр ESP является указателем стека (хранит адрес вершины стека), регистр EBP — указателем базы. Регистр EBP используется для хранения адреса предыдущего фрейма. Аргументы, которые main передаёт foo, а также локальные переменные могут быть получены путём сдвига относительно указателя базы.
Если вызываемая функция работает с регистрами EAX, ECX и EDX, то вызывающая функция должна их сохранить каким-то образом на стеке перед вызовом подпрограммы. С другой стороны, вызываемая функция должна восстановить значения этих регистров. Если вызываемая функция изменяет состояние регистров EAX, ECX и EDX, то она должна сначала сохранить предыдущее состояние на стеке, а перед выходом восстановить их прежние значения.
Параметры, передаваемые функции foo кладутся на стек справа налево: сначала самый последний аргумент, в конце самый первый. Локальные переменные, также как и временные, хранятся на стеке.
Возвращаемое значение, если оно меньше 4 байт, сохраняется в регистре EAX. Если возвращаемое значение больше четырёх байт, то вызывающая функция передаёт дополнительный первый параметр вызываемой функции. Этот параметр — адрес, по которому должно быть сохранено возвращаемое значение. На языке си вызов
x = foo(a, b, c);
будет трансформирован в
foo(&x, a, b, c);
Заметьте, это произойдёт только если возвращаемое значение больше 4 байт.
Рассмотрим пошагово процесс вызова и понаблюдаем за изменением стека.
Действия вызывающей функции перед вызовом
В нашем примере вызывающая функция main, а вызываемая foo. Перед вызовом функции main использует регистры ESP и EBP для работы с собственным фреймом.
1. Функция main кладёт на стек содержимое регистров EAX, ECX и EDX. Это необязательное действие, которое происходит только если состояние регистров должно быть сохранено.
2. Функция main кладёт на стек аргументы, начиная с последнего. Например, если вызов имеет вид
a = foo(12, 15, 18);
на ассемблере это может выглядеть как:
push dword 18 push dword 15 push dword 12
3. main запрашивает вызов подпрограммы call
call foo
Во время исполнения инструкции call, на стек кладётся содержимое регистра EIP (Istruction pointer). Смысл вот в чём: так как EIP указывает на следующую инструкцию после в main, то на вершине стека окажется адрес возврата. После вызова call следующий цикл выполнения начнётся с метки foo.
На рисунке 2 показано содержимое фрейма после того, как отработала функция call. Красная линия на этом и следующих рисунках отделяет содержимое стека, которое было создано вызовом функции от остального содержимого. После вызова функции вершина стека вновь окажется на этой позиции.
Действия вызываемой функции после вызова
К огда функция foo получает управление, она должна сделать 3 вещи: установить собственный фрейм, выделить место под локальное хранилище, по мере надобности сохранить состояние регистров EBX, ESI и EDI.
Итак, foo сначала устанавливает собственный фрейм. EBP ещё указывает на положение фрейма функции main. Это значение должно быть сохранено, поэтому оно кладётся на стек. Содержимое ESP перемещается в EBP. Это позволит обращаться к аргументам функции как к сдвигу относительно EBP и освободит указатель стека ESP для дальнейших действий. Поэтому, почти все функции си начинаются с инструкций
push ebp mov ebp, esp
На рисунке 3 изображён стек после этих действий. Обратите внимание, что первый аргумент имеет адрес EBP + 8, так как адрес возврата и EBP функии main каждый занимают по 4 байта.
На следующем шаге функция foo должна выделить на стеке место под локальные переменные. Также она должна выделить место под временное хранилище. Дело в том, что функция может иметь сложные выражения, которые будут сохранять промежуточные значения для дальнейшего использования в других сложных выражениях. Пусть, к примеру, кроме двух локальных переменных типа int нашей функции требуется ещё 12 байт для временного хранилища. Необходимые 20 байт могут быть выделены просто вычитанием из EBP двадцати (напомню, вершина стека растёт с убыванием адреса)
sub esp, 20
К локальным переменным и временному хранилищу теперь можно обращаться с помощью сдвига относительно EBP. В заключение, foo должна сохранить содержимое регистров EBX, ESI и EDI на стеке , если они используются.
Теперь может быть выполнено тело функции. При этом новые данные могут помещаться и сниматься со стека. Поэтому указатель стека ESP может изменяться, но EBP остаётся фиксированным, и к первому аргументу можно обратиться [EBP + 8] независимо от того, сколько операций push и pop было вызвано. Выполнение функции foo в свою очередь также может привести к вызову других функций или к рекурсивному вызову. Тем не менее, поскольку EBP восстанавливается после возвращения из этих вложенных вызовов, ссылки на аргументы, локальные переменные и временное хранилище всё также может быть произведено с помощью сдвига относительно EBP.
Действия вызываемой функции перед возвратом
П еред тем как вернуть управление вызывающей функции, foo должна определиться с тем, как возвращать значение, о чём мы уже упоминали. Результат будет помещён в регистр EAX, либо станет дополнительным параметром функции, а сама функция не будет возвращать значения.
Во-вторых, foo должна восстановить значение регистров EBX, ESI и EDI. В начале вызова мы поместили значения регистров на стек (так как наша функция их изменяла), а теперь можем снять эти значения оттуда, но только в том случае, если ESP хранит верное значение, то есть количество операция push и pop должно быть сбалансировано.
После этих двух шагов локальные переменные и временное хранилище больше не нужны и фрейм может быть снят с помощью инструкций
mov esp, ebp pop ebp
После этого адрес возврата может быть снят со стека и помещён в регистр EIP. Так как эти действия часто встречаются в программах на си, то процессоры i386 имеют специальные инструкции, которые делают то же самое, что и приведённые выше
leave ret
Действия вызывающей функции после возвращения
П осле того, как управление перешло к вызывающеё функции (в нашем примере это main), аргументы, переданные функции обычно уже не нужны. Мы можем снять со стека все три значения сразу, просто прибавив 12 (3 раза по 4 байта) к указателю стека.
add esp, 12
Функция main после этого должна сохранить возвращаемое значение, которое хранится в регистре EAX в предназначенном для этого месте. Например, если возвращаемое значение присваивается переменной, то содержимое регистра EAX может быть размещено по адресу этой переменной.
В конце концов, если содержимое регистров EAX, ECX и EDX было сохранено на стеке, то оно может быть восстановлено. Таким образом, стек опять находится в том состоянии, в котором был до вызова функции.
ru-Cyrl 18- tutorial Sypachev S.S. 1989-04-14 sypachev_s_s@mail.ru Stepan Sypachev students
Всё ещё не понятно? – пиши вопросы на ящик
Как вызвать функцию из строки?
Есть много функций с одинаковым именем и разной цифрой в конце этого имени, то есть function1, function2, function3 и т.д. То есть, в определенном месте программы у меня есть счетчик, по которому я вызываю уже нужную из этих функцию, но как-то не хочется это делать через if или switch. Так вот, если сложить вместе имя функции function и счетчик, то можно ли как-то вызвать по этому имени функцию?
Отслеживать
задан 15 дек 2014 в 0:20
533 3 3 золотых знака 8 8 серебряных знаков 20 20 бронзовых знаков
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Если вам хочется такого, вы скорее всего неправильно построили архитектуру своей программы. Трюки, которые проходят в PHP или Javascript’е, не нужны в жёстко типизированных языках, в них другие выразительные средства.
Если всё же вам непременно хочется идти этим путём, заведите массив или карту.
typedef double (*function_t)(double); function_t[] functions = < function0, function1, function2, function3, function4 >; unsigned int number_of_functions = sizeof(functions) / sizeof(functions[0]); double call_by_index(unsigned int index, double argument) < if (index >= number_of_functions) < // сообщите об ошибке, например, можете выбросить исключение // или вернуть NaN >return functions[index](argument); >
Но, повторюсь, это неправильный путь. Если вы расскажете, что именно вы хотите запрограммировать, возможно, мы подскажем дизайн получше.
Отслеживать
ответ дан 15 дек 2014 в 1:41
206k 28 28 золотых знаков 293 293 серебряных знака 526 526 бронзовых знаков
у меня на этот случай припасена задача. Суть задачи — на вход поступают данные вида «имя_команды параметры». где имя_команды — это несколько символов, а параметры — это просто строка с параметрами. Задача — исполнять команды. Эта задача очень хорошо решается, если есть легкий способ вызвать функцию по ее имени. куча if’ов — плохой подход. В С++ хорошо решается с помощью map, который содержит указатели на функции-обработчики. В java — только if/case (мне простых методов не известно).
15 дек 2014 в 9:27
@kovadim: Почему if-case? В Java тоже есть аналогичные std::map() контейнеры. И как вывод можно хранить пары
15 дек 2014 в 11:01
а туда можно добавить ссылки на методы?
15 дек 2014 в 11:02
Java 8 поддерживает лямбды, так что в виде лямбды.
15 дек 2014 в 11:10
@VladD, если правильно понимаю, это появилось в Java 8, а раньше так программировать было нельзя. Помню, меня ужасно раздражало, что для имитации подобного поведения надо было для каждой функции делать свой класс (имена функций в классах одинаковые), а все эти классы наследовали общий интефейс. — И все это словоблудие вместо какого-нибудь (*f)() .
15 дек 2014 в 12:05
Почитайте о разделяемых библиотеках (.so), посмотрите man dlopen (в винде тоже есть что-то похожее, свое, см. в сторону .dll).
Вот «для затравки» совсем небольшой ленивый примерчик:
avp@avp-xub11:lib$ cat t1.c #include #include #include #include int main (int ac, char *av[]) < if (ac 100) < // а это кривая проверка "самовызова" printf("self-call: %s\n", (char *)ac); return 0; >void *my = dlopen(0, RTLD_NOW); if (!my) < printf("dlopen: %s\n", dlerror()); exit(1); >int i; for (i = 1; av[i]; i++) < void (*f)() = dlsym(my, av[i]); if (!f) printf("function [%s] not found\n", av[i]); else f(av[i]), puts(""); >return puts("End") == EOF; > avp@avp-xub11:lib$ gcc t1.c -ldl -rdynamic avp@avp-xub11:lib$ ./a.out f1 puts printf main function [f1] not found puts printf self-call: main End avp@avp-xub11:lib$
Программа открывает саму себя (загрузочный модуль) как разделяемую библиотеку, а также наследует уже подключенные загрузчиком другие .so и вызывает функции, имена которых передаются в аргументах main() .
Только учтите, правильность передачи параметров никто не проверяет. Вы должны знать, какие параметры принимает функция и обязаны сами вызывать ее правильно.
Определение и вызов функций
Мощность языка СИ во многом определяется легкостью и гибкостью в определении и использовании функций в СИ-программах. В отличие от других языков программирования высокого уровня в языке СИ нет деления на процедуры, подпрограммы и функции, здесь вся программа строится только из функций.
Функция — это совокупность объявлений и операторов, обычно предназначенная для решения определенной задачи. Каждая функция должна иметь имя, которое используется для ее объявления, определения и вызова. В любой программе на СИ должна быть функция с именем main (главная функция), именно с этой функции, в каком бы месте программы она не находилась, начинается выполнение программы.
При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое (одно !) значение. Это возвращаемое значение и есть результат выполнения функции, который при выполнении программы подставляется в точку вызова функции, где бы этот вызов ни встретился. Допускается также использовать функции не имеющие аргументов и функции не возвращающие никаких значений. Действие таких функций может состоять, например, в изменении значений некоторых переменных, выводе на печать некоторых текстов и т.п..
С использованием функций в языке СИ связаны три понятия — определение функции (описание действий, выполняемых функцией), объявление функции (задание формы обращения к функции) и вызов функции.
Определение функции задает тип возвращаемого значения, имя функции, типы и число формальных параметров, а также объявления переменных и операторы, называемые телом функции, и определяющие действие функции. В определении функции также может быть задан класс памяти.
int rus (unsigned char r) < if (r>='А' && cВ данном примере определена функция с именем rus, имеющая один параметр с именем r и типом unsigned char. Функция возвращает целое значение, равное 1, если параметр функции является буквой русского алфавита, или 0 в противном случае.
В языке СИ нет требования, чтобы определение функции обязательно предшествовало ее вызову. Определения используемых функций могут следовать за определением функции main, перед ним, или находится в другом файле.
Однако для того, чтобы компилятор мог осуществить проверку соответствия типов передаваемых фактических параметров типам формальных параметров до вызова функции нужно поместить объявление (прототип) функции.
Объявление функции имеет такой же вид, что и определение функции, с той лишь разницей, что тело функции отсутствует, и имена формальных параметров тоже могут быть опущены. Для функции, определенной в последнем примере, прототип может иметь вид
int rus (unsigned char r); или rus (unsigned char);
В программах на языке СИ широко используются, так называемые, библиотечные функции, т.е. функции предварительно разработанные и записанные в библиотеки. Прототипы библиотечных функций находятся в специальных заголовочных файлах, поставляемых вместе с библиотеками в составе систем программирования, и включаются в программу с помощью директивы #include.
Если объявление функции не задано, то по умолчанию строится прототип функции на основе анализа первой ссылки на функцию, будь то вызов функции или определение. Однако такой прототип не всегда согласуется с последующим определением или вызовом функции. Рекомендуется всегда задавать прототип функции. Это позволит компилятору либо выдавать диагностические сообщения, при неправильном использовании функции, либо корректным образом регулировать несоответствие аргументов устанавливаемое при выполнении программы.
Объявление параметров функции при ее определении может быть выполнено в так называемом "старом стиле", при котором в скобках после имени функции следуют только имена параметров, а после скобок объявления типов параметров. Например, функция rus из предыдущего примера может быть определена следующим образом:
int rus (r) unsigned char r; < . /* тело функции */ . >В соответствии с синтаксисом языка СИ определение функции имеет следующую форму:
[спецификатор-класса-памяти] [спецификатор-типа] имя-функции ([список-формальных-параметров])
Необязательный спецификатор-класса-памяти задает класс памяти функции, который может быть static или extern. Подробно классы памяти будут рассмотрены в следующем разделе.
Спецификатор-типа функции задает тип возвращаемого значения и может задавать любой тип. Если спецификатор-типа не задан, то предполагается, что функция возвращает значение типа int.
Функция не может возвращать массив или функцию, но может возвращать указатель на любой тип, в том числе и на массив и на функцию. Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлении этой функции.
Функция возвращает значение если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и возвращается в точку вызова функции в качестве результата. Если оператор return не содержит выражения или выполнение функции завершается после выполнения последнего ее оператора (без выполнения оператора return), то возвращаемое значение не определено. Для функций, не использующих возвращаемое значение, должен быть использован тип void, указывающий на отсутствие возвращаемого значения. Если функция определена как функция, возвращающая некоторое значение, а в операторе return при выходе из нее отсутствует выражение, то поведение вызывающей функции после передачи ей управления может быть непредсказуемым.
Список-формальных-параметров - это последовательность объявлений формальных параметров, разделенная запятыми. Формальные параметры - это переменные, используемые внутри тела функции и получающие значение при вызове функции путем копирования в них значений соответствующих фактических параметров. Список-формальных-параметров может заканчиваться запятой (,) или запятой с многоточием (. ), это означает, что число аргументов функции переменно. Однако предполагается, что функция имеет, по крайней мере, столько обязательных аргументов, сколько формальных параметров задано перед последней запятой в списке параметров. Такой функции может быть передано большее число аргументов, но над дополнительными аргументами не проводится контроль типов.
Если функция не использует параметров, то наличие круглых скобок обязательно, а вместо списка параметров рекомендуется указать слово void.
Порядок и типы формальных параметров должны быть одинаковыми в определении функции и во всех ее объявлениях. Типы фактических параметров при вызове функции должны быть совместимы с типами соответствующих формальных параметров. Тип формального параметра может быть любым основным типом, структурой, объединением, перечислением, указателем или массивом. Если тип формального параметра не указан, то этому параметру присваивается тип int.
Для формального параметра можно задавать класс памяти register, при этом для величин типа int спецификатор типа можно опустить.
Идентификаторы формальных параметров используются в теле функции в качестве ссылок на переданные значения. Эти идентификаторы не могут быть переопределены в блоке, образующем тело функции, но могут быть переопределены во внутреннем блоке внутри тела функции.
При передаче параметров в функцию, если необходимо, выполняются обычные арифметические преобразования для каждого формального параметра и каждого фактического параметра независимо. После преобразования формальный параметр не может быть короче чем int, т.е. объявление формального параметра с типом char равносильно его объявлению с типом int. А параметры, представляющие собой действительные числа, имеют тип double.
Преобразованный тип каждого формального параметра определяет, как интерпретируются аргументы, помещаемые при вызове функции в стек. Несоответствие типов фактических аргументов и формальных параметров может быть причиной неверной интерпретации.
Тело функции - это составной оператор, содержащий операторы, определяющие действие функции.
Все переменные, объявленные в теле функции без указания класса памяти, имеют класс памяти auto, т.е. они являются локальными. При вызове функции локальным переменным отводится память в стеке и производится их инициализация. Управление передается первому оператору тела функции и начинается выполнение функции, которое продолжается до тех пор, пока не встретится оператор return или последний оператор тела функции. Управление при этом возвращается в точку, следующую за точкой вызова, а локальные переменные становятся недоступными. При новом вызове функции для локальных переменных память распределяется вновь, и поэтому старые значения локальных переменных теряются.
Параметры функции передаются по значению и могут рассматриваться как локальные переменные, для которых выделяется память при вызове функции и производится инициализация значениями фактических параметров. При выходе из функции значения этих переменных теряются. Поскольку передача параметров происходит по значению, в теле функции нельзя изменить значения переменных в вызывающей функции, являющихся фактическими параметрами. Однако, если в качестве параметра передать указатель на некоторую переменную, то используя операцию разадресации можно изменить значение этой переменной.
/* Неправильное использование параметров */ void change (int x, int y)
В данной функции значения переменных x и y, являющихся формальными параметрами, меняются местами, но поскольку эти переменные существуют только внутри функции change, значения фактических параметров, используемых при вызове функции, останутся неизменными. Для того чтобы менялись местами значения фактических аргументов можно использовать функцию приведенную в следующем примере.
/* Правильное использование параметров */ void change (int *x, int *y)
При вызове такой функции в качестве фактических параметров должны быть использованы не значения переменных, а их адреса
Если требуется вызвать функцию до ее определения в рассматриваемом файле, или определение функции находится в другом исходном файле, то вызов функции следует предварять объявлением этой функции. Объявление (прототип) функции имеет следующий формат:
[спецификатор-класса-памяти] [спецификатор-типа] имя-функции ([список-формальных-параметров]) [,список-имен-функций];
В отличие от определения функции, в прототипе за заголовком сразу же следует точка с запятой, а тело функции отсутствует. Если несколько разных функций возвращают значения одинакового типа и имеют одинаковые списки формальных параметров, то эти функции можно объявить в одном прототипе, указав имя одной из функций в качестве имени-функции, а все другие поместить в список-имен-функций, причем каждая функция должна сопровождаться списком формальных параметров. Правила использования остальных элементов формата такие же, как при определении функции. Имена формальных параметров при объявлении функции можно не указывать, а если они указаны, то их область действия распространяется только до конца объявления.
Прототип - это явное объявление функции, которое предшествует определению функции. Тип возвращаемого значения при объявлении функции должен соответствовать типу возвращаемого значения в определении функции.
Если прототип функции не задан, а встретился вызов функции, то строится неявный прототип из анализа формы вызова функции. Тип возвращаемого значения создаваемого прототипа int, а список типов и числа параметров функции формируется на основании типов и числа фактических параметров используемых при данном вызове.
Таким образом, прототип функции необходимо задавать в следующих случаях:
1. Функция возвращает значение типа, отличного от int.
2. Требуется проинициализировать некоторый указатель на функцию до того, как эта функция будет определена.
Наличие в прототипе полного списка типов аргументов параметров позволяет выполнить проверку соответствия типов фактических параметров при вызове функции типам формальных параметров, и, если необходимо, выполнить соответствующие преобразования.
В прототипе можно указать, что число параметров функции переменно, или что функция не имеет параметров.
Если прототип задан с классом памяти static, то и определение функции должно иметь класс памяти static. Если спецификатор класса памяти не указан, то подразумевается класс памяти extern.
Вызов функции имеет следующий формат:
Поскольку синтаксически имя функции является адресом начала тела функции, в качестве обращения к функции может быть использовано адресное-выражение (в том числе и имя функции или разадресация указателя на функцию), имеющее значение адреса функции.
Список-выражений представляет собой список фактических параметров, передаваемых в функцию. Этот список может быть и пустым, но наличие круглых скобок обязательно.
Фактический параметр может быть величиной любого основного типа, структурой, объединением, перечислением или указателем на объект любого типа. Массив и функция не могут быть использованы в качестве фактических параметров, но можно использовать указатели на эти объекты.
Выполнение вызова функции происходит следующим образом:
1. Вычисляются выражения в списке выражений и подвергаются обычным арифметическим преобразованиям. Затем, если известен прототип функции, тип полученного фактического аргумента сравнивается с типом соответствующего формального параметра. Если они не совпадают, то либо производится преобразование типов, либо формируется сообщение об ошибке. Число выражений в списке выражений должно совпадать с числом формальных параметров, если только функция не имеет переменного числа параметров. В последнем случае проверке подлежат только обязательные параметры. Если в прототипе функции указано, что ей не требуются параметры, а при вызове они указаны, формируется сообщение об ошибке.
2. Происходит присваивание значений фактических параметров соответствующим формальным параметрам.
3. Управление передается на первый оператор функции.
4. Выполнение оператора return в теле функции возвращает управление и возможно, значение в вызывающую функцию. При отсутствии оператора return управление возвращается после выполнения последнего оператора тела функции, а возвращаемое значение не определено.
Адресное выражение, стоящее перед скобками определяет адрес вызываемой функции. Это значит что функция может быть вызвана через указатель на функцию.
int (*fun)(int x, int *y);
Здесь объявлена переменная fun как указатель на функцию с двумя параметрами: типа int и указателем на int. Сама функция должна возвращать значение типа int. Круглые скобки, содержащие имя указателя fun и признак указателя *, обязательны, иначе запись
int *fun (intx,int *y);
будет интерпретироваться как объявление функции fun возвращающей указатель на int.
Вызов функции возможен только после инициализации значения указателя fun и имеет вид:
В этом выражении для получения адреса функции, на которую ссылается указатель fun используется операция разадресации * .
Указатель на функцию может быть передан в качестве параметра функции. При этом разадресация происходит во время вызова функции, на которую ссылается указатель на функцию. Присвоить значение указателю на функцию можно в операторе присваивания, употребив имя функции без списка параметров.
double (*fun1)(int x, int y); double fun2(int k, int l); fun1=fun2; /* инициализация указателя на функцию */ (*fun1)(2,7); /* обращение к функции */В рассмотренном примере указатель на функцию fun1 описан как указатель на функцию с двумя параметрами, возвращающую значение типа double, и также описана функция fun2. В противном случае, т.е. когда указателю на функцию присваивается функция описанная иначе чем указатель, произойдет ошибка.
Рассмотрим пример использования указателя на функцию в качестве параметра функции вычисляющей производную от функции cos(x).
double proiz(double x, double dx, double (*f)(double x) ); double fun(double z); int main() < double x; /* точка вычисления производной */ double dx; /* приращение */ double z; /* значение производной */ scanf("%f,%f",&x,&dx); /* ввод значений x и dx */ z=proiz(x,dx,fun); /* вызов функции */ printf("%f",z); /* печать значения производной */ return 0; >double proiz(double x,double dx, double (*f)(double z) ) < /* функция вычисляющая производную */ double xk,xk1,pr; xk=fun(x); xk1=fun(x+dx); pr=(xk1/xk-1e0)*xk/dx; return pr; >double fun( double z) < /* функция от которой вычисляется производная */ return (cos(z)); >Для вычисления производной от какой-либо другой функции можно изменить тело функции fun или использовать при вызове функции proiz имя другой функции. В частности, для вычисления производной от функции cos(x) можно вызвать функцию proiz в форме
а для вычисления производной от функции sin(x) в форме
Любая функция в программе на языке СИ может быть вызвана рекурсивно, т.е. она может вызывать саму себя. Компилятор допускает любое число рекурсивных вызовов. При каждом вызове для формальных параметров и переменных с классом памяти auto и register выделяется новая область памяти, так что их значения из предыдущих вызовов не теряются, но в каждый момент времени доступны только значения текущего вызова.
Переменные, объявленные с классом памяти static, не требуют выделения новой области памяти при каждом рекурсивном вызове функции и их значения доступны в течение всего времени выполнения программы.
Классический пример рекурсии - это математическое определение факториала n! :
n! = 1 при n=0; n*(n-1)! при n>1 .Функция, вычисляющая факториал, будет иметь следующий вид:
long fakt(int n)
Хотя компилятор языка СИ не ограничивает число рекурсивных вызовов функций, это число ограничивается ресурсом памяти компьютера и при слишком большом числе рекурсивных вызовов может произойти переполнение стека.
1.5.2. Вызов функции с переменным числом параметров
При вызове функции с переменным числом параметров в вызове этой функции задается любое требуемое число аргументов. В объявлении и определении такой функции переменное число аргументов задается многоточием в конце списка формальных параметров или списка типов аргументов.
Все аргументы, заданные в вызове функции, размещаются в стеке. Количество формальных параметров, объявленных для функции, определяется числом аргументов, которые берутся из стека и присваиваются формальным параметрам. Программист отвечает за правильность выбора дополнительных аргументов из стека и определение числа аргументов, находящихся в стеке.
Примерами функций с переменным числом параметров являются функции из библиотеки функций языка СИ, осуществляющие операции ввода-вывода информации (printf,scanf и т.п.). Подробно эти функции рассмотрены во третьей части книги.
Программист может разрабатывать свои функции с переменным числом параметров. Для обеспечения удобного способа доступа к аргументам функции с переменным числом параметров имеются три макроопределения (макросы) va_start, va_arg, va_end, находящиеся в заголовочном файле stdarg.h. Эти макросы указывают на то, что функция, разработанная пользователем, имеет некоторое число обязательных аргументов, за которыми следует переменное число необязательных аргументов. Обязательные аргументы доступны через свои имена как при вызове обычной функции. Для извлечения необязательных аргументов используются макросы va_start, va_arg, va_end в следующем порядке.
Макрос va_start предназначен для установки аргумента arg_ptr на начало списка необязательных параметров и имеет вид функции с двумя параметрами:
Параметр prav_param должен быть последним обязательным параметром вызываемой функции, а указатель arg_prt должен быть объявлен с предопределением в списке переменных типа va_list в виде:
Макрос va_start должен быть использован до первого использования макроса va_arg.
Макрокоманда va_arg обеспечивает доступ к текущему параметру вызываемой функции и тоже имеет вид функции с двумя параметрами
Эта макрокоманда извлекает значение типа type по адресу, заданному указателем arg_ptr, увеличивает значение указателя arg_ptr на длину использованного параметра (длина type) и таким образом параметр arg_ptr будет указывать на следующий параметр вызываемой функции. Макрокоманда va_arg используется столько раз, сколько необходимо для извлечения всех параметров вызываемой функции.
Макрос va_end используется по окончании обработки всех параметров функции и устанавливает указатель списка необязательных параметров на ноль (NULL).
Рассмотрим применение этих макросов для обработки параметров функции вычисляющей среднее значение произвольной последовательности целых чисел. Поскольку функция имеет переменное число параметров будем считать концом списка значение равное -1. Поскольку в списке должен быть хотя бы один элемент, у функции будет один обязательный параметр.
#includeint main() < int n; int sred_znach(int. ); n=sred_znach(2,3,4,-1); /* вызов с четырьмя параметрами */ printf("n=%d",n); n=sred_znach(5,6,7,8,9,-1); /* вызов с шестью параметрами */ printf("n=%d",n); return (0); >int sred_znach(int x. ); < int i=0, j=0, sum=0; va_list uk_arg; va_start(uk_arg,x); /* установка указателя uk_arg на */ /* первый необязятельный параметр */ if (x!=-1) sum=x; /* проверка на пустоту списка */ else return (0); j++; while ( (i=va_arg(uk_arg,int))!=-1) /* выборка очередного */ < /* параметра и проверка */ sum+=i; /* на конец списка */ j++; >va_end(uk_arg); /* закрытие списка параметров */ return (sum/j); > 1.5.3. Передача параметров функции main
Функция main, с которой начинается выполнение СИ-программы, может быть определена с параметрами, которые передаются из внешнего окружения, например, из командной строки. Во внешнем окружении действуют свои правила представления данных, а точнее, все данные представляются в виде строк символов. Для передачи этих строк в функцию main используются два параметра, первый параметр служит для передачи числа передаваемых строк, второй для передачи самих строк. Общепринятые (но не обязательные) имена этих параметров argc и argv. Параметр argc имеет тип int, его значение формируется из анализа командной строки и равно количеству слов в командной строке, включая и имя вызываемой программы (под словом понимается любой текст не содержащий символа пробел). Параметр argv это массив указателей на строки, каждая из которых содержит одно слово из командной строки. Если слово должно содержать символ пробел, то при записи его в командную строку оно должно быть заключено в кавычки.
Функция main может иметь и третий параметр, который принято называть argp, и который служит для передачи в функцию main параметров операционной системы (среды) в которой выполняется СИ-программа.
Заголовок функции main имеет вид:
int main (int argc, char *argv[], char *argp[])
Если, например, командная строка СИ-программы имеет вид:
A:\>cprog working 'C program' 1
то аргументы argc, argv, argp представляются в памяти как показано в схеме на рис.1.
argc [ 4 ] argv [ ]--> [ ]--> [A:\cprog.exe\0] [ ]--> [working\0] [ ]--> [C program\0] [ ]--> [1\0] [NULL] argp [ ]--> [ ]--> [path=A:\;C:\\0] [ ]--> [lib=D:\LIB\0] [ ]--> [include=D:\INCLUDE\0] [ ]--> [conspec=C:\COMMAND.COM\] [NULL] Рис.1. Схема размещения параметров командной строкиОперационная система поддерживает передачу значений для параметров argc, argv, argp, а на пользователе лежит ответственность за передачу и использование фактических аргументов функции main.
Следующий пример представляет программу печати фактических аргументов, передаваемых в функцию main из операционной системы и параметров операционной системы.
Пример: int main ( int argc, char *argv[], char *argp[]) < int i=0; printf ("\n Имя программы %s", argv[0]); for (i=1; i>=argc; i++) printf ("\n аргумент %d равен %s", argv[i]); printf ("\n Параметры операционной системы:"); while (*argp) < printf ("\n %s",*argp); argp++; >return (0); >Доступ к параметрам операционной системы можно также получить при помощи библиотечной функции geteuv, ее прототип имеет следующий вид:
char *geteuv (const char *varname);
Аргумент этой функции задает имя параметра среды, указатель на значение которой выдаст функция geteuv. Если указанный параметр не определен в среде в данный момент, то возвращаемое значение NULL.
Используя указатель, полученный функцией geteuv, можно только прочитать значение параметра операционной системы, но нельзя его изменить. Для изменения значения параметра системы предназначена функция puteuv.
Компилятор языка СИ строит СИ-программу таким образом, что вначале работы программы выполняется некоторая инициализация, включающая, кроме всего прочего, обработку аргументов, передаваемых функции main, и передачу ей значений параметров среды. Эти действия выполняются библиотечными функциями _setargv и _seteuv, которые всегда помещаются компилятором перед функцией main.
Если СИ-программа не использует передачу аргументов и значений параметров операционной системы, то целесообразно запретить использование библиотечных функций _setargv и _seteuv поместив в СИ-программу перед функцией main функции с такими же именами, но не выполняющие никаких действий (заглушки). Начало программы в этом случае будет иметь вид:
_setargv() < return ; /* пустая функция */ >-seteuv() < return ; /* пустая функция */ >int main() < /* главная функция без аргументов */ . . renurn (0); >В приведенной программе при вызове библиотечных функций _setargv и _seteuv будут использованы функции помещенные в программу пользователем и не выполняющие никаких действий. Это заметно снизит размер получаемого exe-файла.