Есть два способа определения функции. Первый способ:
класс-памяти тип-результата идентификатор ( список-типов-параметров ) { список-деклараций список-команд }
Класс-памяти может быть либо extern
, либо
static
. Если класс памяти пропущен, то подразумевается
extern
.
Тип-результата может быть любым, за исключением массива. Если тип-результата пропущен, то подразумевается int.
Идентификатор является именем функции.
Список-типов-параметров может быть void
или пустым,
означая, что функция не имеет параметров, либо разделённый запятыми список
деклараций объектов, включающий и типы, и имена параметров (идентификаторы).
Если указываются несколько аргументов одинакового типа, то тип каждого
аргумента указывается индивидуально.
тип id1, id2
в списке параметров не допускается.
Функция имеет переменное число аргументов, если
список-типов-параметров заканчивается ,...
.
Любой параметр объявленный как «массив типа» изменяется на «указатель на тип». Любой параметр декларированный как «функция» изменяется на «указатель на функцию».
Следующие примеры демонстрируют несколько определений функции:
int F( void )
Функция F
не имеет параметров и возвращает int
.
void G( int x )
Функция G
имеет один параметр, объект целого типа именуемый
x
, и не возвращает никакого значения.
void * H( long int len, long int wid )
Функция H
имеет два параметра, объекты длинного целого типа
len
и wid
, и возвращает указатель, который
указывает на объект неопределённого типа.
void I( char * format, ... )
Функция I
имеет один известный параметр, объект с именем
format
, который указывает символ (стринг). Функция также
принимает переменное число параметров после format
. Функция не
возвращает никакого результата.
Этот способ определения функции также используется в качестве прототипа для любых вызовов функции, происходящих позже в том же модуле. Если прототип находится в области видимости во время вызова функции, то перед присвоением её аргументы конвертируются в типы соответствующие параметрам. Если вызов функции происходит до её определения или из другого модуля, нужно указывать её прототип, чтобы обеспечить правильные преобразования типов аргументов. Если этого не сделать, то в результате будет произведено расширение типов по умолчанию и, следовательно, поведение будет неопределённым, если типы параметров функции не совпадают с расширенными аргументами.
Второй способ определения функции:
класс-памяти тип-результата идентификатор ( список-идентификаторов ) список-деклараций { список-деклараций список-команд }
Класс-памяти, тип-результата и идентификатор означают то
же, что и в первом способе. В этом способе список-идентификаторов
является (возможно пустым) списком идентификаторов, разделённых запятыми, без
указания информации о типе. После закрывающей круглой скобки и перед
открывающей фигурной даются декларации объектов по обычным правилам. Объект
типа int
явно декларировать необязательно.
В декларациях идентификаторов параметров допускается только один из
спецификаторов класса памяти — register
.
Прототип функции создаётся из определения, после расширения по умолчанию типа каждого из аргументов. Над всеми аргументами функции декларированной таким способом будет производится расширение типа по умолчанию. Результирующие типы должны совпадать с типами декларированных параметров после расширения. В противном случае поведение не определено.
Заметьте, что функции, декларированной таким способом, невозможно передать
объект типа float
. Аргумент типа float
будет
автоматически расширен до double
, и параметр также будет
расширен до double
(подразумевается, что он декларирован как
float
). По тем же причинам невозможно передать нерасширенными
объекты типа char
или short int
.
В соответствии со стандартом ANSI этот способ определения функции — устаревший и не должен использоваться. Он существует только по историческим причинам, в частности, для совместимости со старыми компиляторами C. Использование первого способа определения функции зачастую позволяет компилятору генерировать лучший код.
Следующие примеры — те же, что и для первого способа с соответствующими изменениями:
int F()
Функция F
не имеет параметров и возвращает целое.
void G( x )
Функция G
имеет один параметр, целое x
, и не
возвращает никого значения. Этот пример можно написать и так:
void G( x ) int x;
x
явно декларирован как целое.
void * H( len, wid ) long int len; long int wid;
Функция H
имеет два параметра, целые len
и
wid
, и возвращает указатель, который указывает на объект
неопределённого типа. Любой вызов этой функции должен гарантировать, что
передаются длинные целые, либо передавая объекты этого типа, либо явно
преобразуя их в этот тип.
Последний пример, с использованием эллипсиса (,...
), нельзя
представить напрямую вторым способом определения функции. Большинство
компиляторов позволяют работать с переменным списком аргументов в таком виде,
но при этом требуется знание механизма передачи аргументов функции, а он у
разных компиляторов может различаться.
Тело функции следует за её декларацией и открывающей фигурной скобкой. Оно состоит из двух частей, обе из которых необязательны.
Первая часть является списком деклараций всех объектов, которые необходимы в
функции. Эти объекты могут иметь любой тип и класс памяти. Объекты с классом
памяти register
или auto
имеют автоматическую
продолжительность хранения, что означает, что они создаются при вызове
функции и уничтожаются при завершении функции. (Значение объекта не
сохраняется между вызовами функции.) Объекты с классом памяти
extern
и static
имеют статическую
продолжительность хранения, что означает, что они создаются однажды, до
какого-либо вызова функции, и уничтожаются только тогда, когда заканчивается
программа. Любое значение помещённое в такой объект, будет сохраняться и
после возврата из функции, так, что при следующем вызове оно будет тем же
(если, конечно, над объектом не будет произведено какого-либо действия,
например, при помощи указателя, содержащего его адрес).
Если не выполнится явная команда return
, то функция не вернёт
управления, пока не встретится закрывающая фигурная скобка в конце её
определения. Возврат будет таким же, как и при использовании команды
return
без выражения. Если при этом функция определена как
возвращающая некое значение, а вызывающая команда попытается использовать
его, то поведение не определено. Используемое значение будет случайным.
Функция может вызвать сама себя напрямую (рекурсия) либо может вызвать другую функцию, которая в свою очередь вызовет её. Для любого объекта с автоматической продолжительностью хранения во время каждой рекурсии будет создаваться новый экземпляр, а объекты со статической продолжительностью хранения будут иметь только один, разделяемый между рекурсивными экземплярами функции.
Прототип функции похож на определение без тела. Непосредственно после правой круглой скобки в декларации функции ставится точка с запятой. Прототип описывает имя функции, типы параметров, которые она ожидает (имена необязательны) и тип возвращаемого значения. Эту информацию компилятор C может использовать для проверки и правильного преобразования типов аргументов во время вызова, а также для правильной обработки возвращаемого значения.
Если во время вызова функции не найден её прототип, то все аргументы
подвергаются расширению по умолчанию, и подразумевается, что тип
возвращаемого значения — int
. Если фактическое определение
функции не содержит параметров, совпадающих с расширенными типами, то
поведение не определено. Если возвращаемое значение — не
int,
и оно необходимо, то поведение не определено.
Прототип функции должен соответствовать определению. Типы каждого параметра и типы возвращаемых значений должны совпадать, в противном случае, поведение не определено.
Все библиотечные функции имеют прототипы в одном или нескольких заголовочных файлах. Они должны включаться всегда, когда используется описанная в них функция. Подробности см. «Watcom C Library Reference».
Если прототип (и определение) функции содержат список параметров, который
оканчивается ,...
, то функция имеет переменный список
аргументов или переменный список параметров, что значит, что число
аргументов функции может изменяться. (В качестве примера можно привести
библиотечную функцию printf
.) До переменной части должен быть
указан хотя бы один аргумент. Этот аргумент обычно описывает неким образом,
сколько ещё аргументов следует ожидать. Он может быть просто счётчиком, а
может включать в себя (как в printf
) описание числа аргументов и
их типы.
Все аргументы, которые соответствуют переменному списку аргументов, подвергаются расширению по умолчанию, так как во время компиляции невозможно определить, какие типы потребует функция.
Так как параметры представленные ,...
не имеют имён, то они
требуют специального обращения. Язык C предоставляет специальный тип и три
макро для обработки переменных списков аргументов. Для их использования нужно
включить заголовочный файл <stdarg.h>
.
Тип va_list
является реализационно-зависимым типом, который
используется для хранения информации о переменном списке. Внутри функции
должен быть декларирован объект этого типа. Он используется макро и функциями
для обработки списка.
va_start
имеет следующую форму:
void va_start( va_list пар_инф , посл_пар );
Макро заполняет объект пар_инф информацией описывающей переменный
список. Аргумент посл_пар является именем (идентификатором) последнего
параметра перед ,...
и должен быть объявлен без спецификатора
класса памяти register
.
Макро va_start
должно быть выполнено до любой обработки
переменной части списка параметров.
va_start
может исполняться более одного раза, но только после
того, как исполнится va_end
.
Макро va_arg
имеет следующую форму
тип va_arg( va_list пар_инф , тип );
пар_инф является тем же объектом, что и при вызове
va_start
. Тип — тип ожидаемого аргумента.
Ожидаемыми типами могут быть только те, которые получаются в результате
расширения аргументов по умолчанию (int
, long int
и
их беззнаковые варианты, double
и long double
), и
те, которые не подвергаются расширению (указатели, структуры и объединения).
Тип должен определяться программой. Макро va_arg
расширяется в
выражение, которое имеет тип и значение следующего параметра в переменном
списке.
В случае printf
ожидаемые типы параметров определяются из
«спецификаторов преобразований», таких как %s
, %d
и
т.д.
Первый вызов макро va_arg
(после исполнения
va_start
) возвращает значение параметра следующего после
посл_пар (указанного в va_start
). Каждый последующий
вызов va_arg
возвращает очередной параметр в списке. При каждом
вызове значение пар_инф изменяется (неким способом, зависящим то
реализации) для отображения процесса обработки списка параметров.
Если тип следующего параметра не совпадает с типом, или параметр не был указан, то поведение не определено.
Макро va_end
имеет следующую форму
void va_end( va_list пар_инф );
пар_инф — тот же объект, что и соответствующий в вызове
va_start
. Функция va_end
заканчивает обработку
переменного списка аргументов, и это нужно сделать до возврата из функции.
Если va_end
не вызвана до возврата из функции, то поведение не
определено.
Если va_end
вызвана без соответствующего вызова
va_start
, то поведение не определено.
После вызова va_end
и до возврата из функции можно опять вызвать
va_start
и заново обработать переменный список. При этом до
возврата из функции необходимо будет снова вызвать va_end
.
Следующая функция принимает в качестве параметров счётчик и произвольное количество чисел с плавающей точкой и возвращает среднее:
#include <stdarg.h> extern double Average( int count, ... ) /*************************************/ { double sum = 0; int i; va_list parminfo; if( count == 0 ) { return( 0.0 ); } va_start( parminfo, count ); for( i = 0; i < count; i++ ) { sum += va_arg( parminfo, double ); } va_end( parminfo ); return( sum / count ); }
main
Функция main
имеет специальное назначение в C. Это функция,
которая получает управление после старта программы. Функция main
имеет следующее определение:
extern int main( int argc, char * argv[] ) /****************************************/ { команды }
Объекты argc
и argv
имеют следующие свойства:
argc
является «счётчиком аргументов» (ARGument Count), или
числом параметров (включая имя программы) переданных программе, его значение
больше нуля,
argv
(ARGument Vector) — массив указателей на строки
содержащие параметры,
argv[0]
— имя программы, если оно доступно, иначе
— указатель на строку содержащую только нулевой символ,
argv[argc]
— указатель представляющий конец списка
аргументов,
argv[1]
до argv[argc-1]
— указатели на
стринги представляющие собой аргументы программы. Программа может изменять
эти стринги, и они существуют в течение всего времени исполнения программы. В
общем случае, стринги будут в смешанном регистре (строчные и прописные
буквы), но если система не может обрабатывать аргументы смешанного регистра,
то она приведёт их к строчным.
Трансляция аргументов программы в стринги, содержащиеся в argv
,
зависит от реализации, так как это обеспечивается операционной системой
(часто из командной строки, используемой при запуске программы),
В Watcom C/16 и C/32 каждая лексема в командной строке, не в кавычках,
отделённая пробелом, становится элементом argv
. Стринги в
кавычках сохраняются как один элемент без кавычек.
Например, командная строка
pgm 2+ 1 tokens "one token"
сделает argc
равным 5, а элементами
argv
будут стринги "pgm", "2+", "1",
"tokens"
и "one token"
.
Функция main
может быть декларирована без параметров:
extern int main( void ) /*********************/ { команды }
main
возвращает целое значение, обычно представляющее статус
завершения. Если не указано возвращаемое значение (с использованием команды
return
без выражения либо по достижении закрывающей функцию
фигурной скобки), то оно не определено.
Для прекращения программы в любой точке можно использовать библиотечную
функцию exit
. Значение её аргумента возвращается, как если бы
main
имела возвращаемое значение.