Блог → Как программировать грамотно на языке Си. Часть 3

Мощность языка программирования Си, во многом объясняется лёгкостью и гибкостью в определении и использовании функций в программах. В отличие от других языков программирования высокого уровня, в Си нет деления на процедуры, подпрограммы и функции, в языке Си программа строится только из функций. Что такое функция в Си? Это независимая совокупность объявлений и операторов, обычно предназначенная для выполнения определенной задачи. Каждая функция должна иметь имя, которое используется для вызова функции. Имя одной из функций - main, которая должна присутствовать в каждой программе, зарезервировано. В программе могут содержаться и другие функции, причем функция main необязательно должна быть первой, хотя с нее всегда начинается выполнение программы.

При вызове функции ей могут быть переданы входные данные, а происходит это посредством передачи функции набора аргументов. Функция может возвращать значение, причём это возвращаемое значение и есть основной результат выполнения функции, который при выполнении программы подставляется на место вызова функции, где бы этот вызов ни встретился в программе. Однако могут быть определены функции, которые не имеют никаких параметров и не возвращают никакого значения. Тем не менее действие таких функций может состоять в изменении внешних или статических переменных или выполнении каких-либо других действий, не связанных с данными.

С использованием функций в языке Си связаны три понятия: определение функции, объявление функции и вызов функции. Определение функции задает имя функции, типы и число ее формальных параметров, объявления и операторы, которые определяют действие функции. Последовательность объявлений и операторов называется телом функции. В определении функции также может быть задан тип значения, возвращаемого функцией (тип возврата), и класс памяти.

Ниже я привожу пример определения функции, проверяющей, является ли заданный символ русской буквой:

int rus (unsigned char с)
{
if (с >= 'А' && c <= 'ё')
return 1;
else
return 0;
}


Имя функции в этом примере - rus. Функция имеет один параметр - исследуемый символ "c", который имеет тип unsigned char. Функция возвращает целое значение 1, если символ является русской буквой, и 0, если нет.

В языке Си нет требования, чтобы определение функции обязательно предшествовало вызову функции. Определения используемых функций могут следовать за определением функции main или могут находиться вообще в другом файле. Однако для того чтобы компилятор мог выполнить проверку соответствия типов передаваемых аргументов типам формальных параметров в определении функции, до вызова функции нужно поместить объявление (прототип) функции.

Прототип функции имеет такой же формат, что и определение функции, с той лишь разницей, что не имеет тела функции и что заголовок функции заканчивается точкой с запятой. Прототип функции задаёт имя функции, типы и число формальных параметров, тип значения, возвращаемого функцией, и класс памяти. Формальные параметры могут иметь имена, но эти имена компилятору не нужны. К примеру, прототип функции, определяющей, является ли символ русской буквой, может иметь следующий вид:

int rus (unsigned char);


В этом прототипе объявлено, что функция rus возвращает целое значение (int) и имеет один параметр типа unsigned char. Компилятор использует прототип функции для сравнения типов фактических аргументов в вызове функции с типами формальных параметров. Прототипы библиотечных функций таких, как функции getchar и printf, находятся в файлах включения, поставляемых в составе системы программирования и включаемых в программу с помощью директив препроцессора #include.

Если объявление функции не задано, то по умолчанию строится прототип на основании информации из первой ссылки на функцию, будь то вызов функции или определение. Однако такой прототип может неадекватно представлять последующий вызов или определение функции, поэтому крайне рекомендуется всегда задавать прототип функции. Задание прототипов позволяет компилятору либо выдавать диагностические сообщения, либо корректным образом регулировать несоответствие аргументов, которое в противном случае не может быть установлено до выполнения программы.

Такая форма заголовка функции в определении или объявлении функции, когда объявление типов параметров находится в круглых скобках вслед за именем функции, соответствует проекту стандарта ANSI языка Си. В большинстве публикаций используется объявление параметров в так называемом "старом стиле", когда типы параметров объявляются перед телом функции, как в примере ниже:

print_table (табл, счетчик)
int *tabl; /* Указатель на первый элемент массива */
int cnt; /* Число элементов */
{

}


Старый стиль удобен, когда для пояснения назначения параметров используются комментарии. Аналог прототипа функции, представленный в старом стиле, будем называть "синтаксической моделью". Такая синтаксическая модель не может быть включена в программу в качестве прототипа функции, но мы будем ею пользоваться при описании функции, для удобства пояснения.

Определение функции

Определение функции задает имя, формальные параметры и тело функции. Оно может также определять тип возвращаемого значения и класс памяти функции. Определение функции имеет следующий формат:

[спецификатор-класса-памяти] [спецификатор-типа]
описатель ([список-формальных-параметров])
тело-функции


Необязательный спецификатор-класса-памяти задаёт класс памяти функции, который может быть или static, или extern. Если он не задан, то по умолчанию он считается extern. Подробную информацию о классах памяти, я скорее всего, приведу чуть позже, в следующих заметках цикла.

Необязательный спецификатор-типа и обязательный описатель задают тип возвращаемого функцией значения и имя функции. Спецификатор типа может задавать любой основной тип, тип структуры или смеси. Если спецификатор типа не задан, то предполагается, что функция возвращает значение типа int. Описатель функции может быть задан со звёздочкой, что означает, что функция возвращает указатель. Функции не могут возвращать массив или функцию, но могут возвращать указатели на любой тип, включая массивы и функции. Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлениях этой функции, сделанных где-либо в программе.

Функция возвращает значение, если ее выполнение заканчивается выполнением оператора return в теле функции следующего формата:

return выражение;


Выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения в определении (объявлении) функции, и управление возвращается в точку вызова. Если оператор return не задан или не содержит выражения, функция не возвращает никакого значения. В этом случае спецификатор типа возвращаемого значения должен быть задан ключевым словом void, означающим отсутствие возвращаемого значения. Если функция объявлена со спецификатором типа возвращаемого значения, а фактически значение не возвращается, поведение вызывающей функции после возврата управления из такой функции может быть непредсказуемым.

Список-формальных-параметров - это последовательность объявлений формальных параметров, разделенных запятыми. Вообще формальные параметры - это переменные, которые принимают значения, передаваемые функции во время вызова. Полное объявление формальных параметров функции задается в круглых скобках, следующих за именем функции в определении функции. Элемент списка формальных параметров имеет два формата:

[register] спецификатор-типа [описатель]
[…]


Список формальных параметров может заканчиваться запятой и многоточием. Это означает, что число аргументов функции переменно, однако предполагается, что функция имеет, по крайней мере, столько аргументов, сколько формальных параметров задано перед последней запятой. Функции может быть передано больше аргументов, чем задано до запятой и многоточия. Над такими аргументами не производится контроль типов.

В качестве расширения проекта стандарта ANSI языка Си, реализация допускает использование запятой "," вместо запятой и многоточия ",…" в конце списка формальных параметров для задания переменного числа аргументов. Если функции не передаются аргументы, то вместо списка формальных параметров необходимо задавать ключевое слово void.

Порядок и типы формальных параметров должны быть одни и те же в определении функции и во всех объявлениях (если они заданы). Типы фактических аргументов в вызовах функции должны быть совместимы с типами соответствующих формальных параметров. Формальный параметр может быть любого основного типа, структурой, смесью, перечислением, указателем или массивом.

Только класс памяти register можно задать для формального параметра. Если формальный параметр представлен в списке, но не объявлен, то предполагается, что параметр имеет тип int. Спецификатор типа может быть опущен, если задан класс памяти register для величины типа int. Идентификаторы формальных параметров используются в теле функции в качестве ссылок на передаваемые величины. Эти идентификаторы не могут быть переопределены в блоке, объемлющем тело функции, но могут быть переопределены во внутренних блоках.

Если необходимо, то компилятор выполняет обычные арифметические преобразования для каждого формального параметра и каждого фактического аргумента независимо. После преобразования нет формального параметра, короче чем int, и нет формального параметра с типом float (они преобразуются к типу double). Это значит, что объявить формальный параметр как char все равно, что объявить его как int.

Если используются ключевые слова near, far, huge, то компилятор выполняет преобразования аргументов-указателей функции, который зависят от размера указателей и наличия или отсутствия списка типов аргументов. Так как параметры x и y, принимаемые функцией, являются копиями передаваемых параметров, она обменивает значение этих параметров, не оказывая влияние на значение аргументов, и эта Функция, не выполняет своей задачи. Правильная работа функции обеспечивается лишь при таком её определении:

/* Правильная функция обмена */
void обмен (int *ук_x, int *ук_y)
{
int врем = *ук_x;
*ук_x = *ук_y;
*ук_y = врем;
}


При вызове функции ей должны передаваться в качестве аргументов адреса переменных х и у, значения которых нужно обменять.

Объявление функции (прототип)

Если нужно вызвать функцию до её определения в этом файле, или определение функции находится в другом исходном файле, вызов функции следует предварить объявлением функции, т.е. задать прототип функции, который позволит компилятору выполнить проверку аргументов и возвращаемого значения. Прототип функции имеет следующий формат:

[спецификатор-класса-памяти] [спецификатор-типа]
описатель( [список-формальных-параметров])
[,список-описателей];


В отличие от формата определения функции в формате прототипа, за заголовком функции следует точка с запятой (т.е. прототип функции не имеет тела), и заголовок функции может заканчиваться списком-описателей. Список описателей представляет имена других функций, возвращающих значение того же типа, который задан спецификатором типа для первого описателя функции, или других переменных такого же типа.

Правила использования остальных элементов формата прототипа те же, что и в определении функции. Имена параметров в списке формальных параметров указывать нет необходимости. Такие имена имеют область действия только до конца объявления, и никакой смысловой нагрузки не несут.

Прототип - это явное объявление функции, которое предшествует определению функции, при этом тип возврата, заданный в прототипе, должен согласовываться с типом возврата, заданным в определении. Если прототип функции не задан, а встретился вызов функции, компилятор создает неявный прототип, исходя из информации, доступной из вызова функции. Тип возврата создаваемого прототипа функции int. На основании типов и числа фактических аргументов создается список объявлений формальных параметров.

Таким образом, прототипы функций нужно задавать в следующих случаях:
1. функция возвращает значение типа, отличного от int (хотя хороший стиль программирования требует задания прототипа и для случая int);
2. указатель на функцию должен быть инициализирован до того, как функция будет определена, это обеспечивается заданием прототипа функции.

Наличие в прототипе полного списка типов параметров позволяет компилятору выполнить проверку соответствия типов аргументов в вызове функции или её определении типам, указанным в прототипе, или выполнить соответствующее преобразование. В прототипе может быть отражен и тот факт, что число аргументов переменно или что аргументы вообще не передаются. Если прототип задан с классом памяти static, в определении функции также должен быть задан спецификатор класса памяти static. Если задан спецификатор класса памяти extern или спецификатор опущен, то функция имеет класс памяти extern.

Вызовы функций

Вызов функции имеет следующий формат:

выражение ([список-выражений])


Выражение вычисляется как адрес функции. Список-выражений представляет собой список фактических аргументов, передаваемых в функцию, и он может быть пустым. Фактический аргумент может быть любой величиной основного типа, структурой, перечислением, смесью или указателем. Хотя массивы и функции не могут быть переданы как параметры, но указатели на эти объекты могут быть переданы.

Выполнение вызова функции происходит следующим образом.
1. Вычисляются выражения в списке выражений и производятся обычные арифметические преобразования. Затем, если известен прототип функции, тип результирующего аргумента сравнивается с типом соответствующего формального параметра. Если они не совпадают, то либо производится преобразование типов, либо выдается диагностическое сообщение. Число выражений в списке выражений должно совпадать с числом формальных параметров, если только прототип функции или определение, не определяет явно переменное число аргументов. В этом случае компилятор проверяет столько аргументов, сколько имен типов имеется в списке формальных параметров и преобразует их, если необходимо, по описанным правилам. Если в прототипе функции, вместо списка формальных параметр задано ключевое слово void, это значит, что в функцию не передается никаких аргументов, и в определении функции не должно быть формальных параметров. Если это не так, выдаётся диагностическое сообщение.
2. Происходит замена формальных параметров на фактические. Первое выражение в списке всегда соответствует первому формальному параметру, второе - второму и т.д.
3. Управление передается на первый оператор функции.
4. Выполнение оператора return в теле функции возвращает управление и, возможно, значение в вызывающую функцию. Если оператор return не задан, то управление возвращается после выполнения последнего оператора тела функции. При этом возвращаемое значение не определено.

Выражения в списке аргументов могут вычисляться в любом порядке, поэтому аргументы, зависящие от побочных эффектов вычисления других аргументов, могут иметь в разных реализациях разные значения. Точка последовательности, устанавливаемая операцией вызова функции, гарантирует лишь, что все побочные эффекты будут вычислены перед передачей управления в вызванную функцию.

Как сказано выше, выражение перед скобками вычисляется как адрес функции. Это означает, что функцию можно вызвать через указатель на функцию. Например, в следующем объявлении идентификатор func_ptr объявлен как указатель на функцию с двумя аргументами типа int и возвращающую значение типа int:

int (* func_ptr) (int, int);


Вызов функции в этом случае может выглядеть так:

(*func_ptr)(i,j);


Как вы видите, здесь используется операция разадресации "*" (звёздочка), чтобы получить адрес функции, на которую ссылается указатель func_ptr. Адрес функции используется для её вызова. Круглые скобки, заключающие *func_ptr, обязательны, так как противном случае будет иметь место объявление функции func_ptr, возвращающей указатель на целое. Указатель на функцию может быть передан в качестве параметру функции.