В современных, правильно написанных программах на языке С каждую функцию перед использованием необходимо объявлять. Обычно это делается с помощью прототипа функции. В первоначальном варианте языка С прототипов не было; но они были введены уже в Стандарт С89. Хотя прототипы формально не требуются, но их использование очень желательно. (Впрочем, в C++ прототипы обязательны!) Во всех примерах этой книги имеются полные прототипы функций. Прототипы дают компилятору возможность тщательнее выполнять проверку типов, подобно тому, как это делается в таких языках как Pascal. Если используются прототипы, то компилятор может обнаружить любые сомнительные преобразования типов аргументов, необходимые при вызове функции, если тип ее параметров отличается от типов аргументов. При этом будут выданы предупреждения обо всех таких сомнительных преобразованиях. Компилятор также обнаружит различия в количестве аргументов, использованных при вызове функции, и в количестве параметров функции.
В общем виде прототип функции должен выглядеть таким образом:
тип имя_функции(тип имя_парам1, тип имя_парам2, ..., имя_парамN);
Использование имен параметров не обязательно. Однако они дают возможность компилятору при наличии ошибки указать имена, для которых обнаружено несоответствие типов, так что не поленитесь указать этих имен — это позволит сэкономить время впоследствии.
Следующая программа показывает, насколько ценными являются прототипы функций. В ней выводится сообщение об ошибке, происходящей из-за того, что программа содержит попытку вызова sqr_it() с целым аргументом, в то время как требуется указатель на целое.
/* В этой программе используется прототип функции
чтобы обеспечить тщательную проверку типов. */
void sqr_it(int *i); /* прототип */
int main(void)
{
int x;
x = 10;
sqr_it(x); /* несоответствие типов */
return 0;
}
void sqr_it(int *i)
{
*i = *i * *i;
}
В качестве прототипа функции может также служить ее определение, если оно находится в программе до первого вызова этой функции. Вот, например, правильная программа:
#include <stdio.h>
/* Это определение будет также служить и
прототипом внутри этой программы. */
void f(int a, int b)
{
printf("%d ", a % b);
}
int main(void)
{
f(10,3);
return 0;
}
В этом примере специальный прототип не требуется; так как функция f() определена еще до того, как она начинает использоваться в main(). Хотя определение функции и может служить ее прототипом в малых программах, но в больших такое встречается редко — особенно, когда используется несколько файлов. В программах, приведенных в качестве примеров в этой книге, для каждой функции автор старался приводить отдельный прототип потому, что именно так обычно и пишется код на языке С.
Единственная функция, для которой не требуется прототип — это main(), так как это первая функция, вызываемая в начале работы программы.
Имеется небольшая, но важная разница в том, как именно в С и C++ обрабатывается прототип функции, не имеющей параметров. В C++ пустой список параметров указывается полным отсутствием в прототипе любых параметров. Например,
int f(); /* Прототип C++ для функции, не имеющей параметров */
Однако в С это выражение означает нечто другое. Из-за необходимости придерживаться совместимости с первоначальной версией С пустой список параметров сообщает, что просто о параметрах не предоставлено никакой информации. Что касается компилятора, то для него эта функция может иметь несколько параметров, а может не иметь ни одного. (Такой оператор называется старомодным объявлением функции, он описан в следующем разделе.)
Если функция в языке С не имеет параметров, то в ее прототипе внутри списка параметров стоит только ключевое слово void. Вот, например, прототип функции f() в том виде, в каком он должен быть в программе на языке С:
float f(void);
Таким образом компилятор узнает, что у функции нет параметров, и любое обращение к ней, в котором имеются аргументы, будет считаться ошибкой. В C++ использование ключевого слова void внутри пустого списка параметров также разрешено, но считается излишним.
Прототипы функций позволяют «отлавливать» ошибки еще до запуска программы. Кроме того, они запрещают вызов функций при несовпадении типов (т.е. с неподходящими аргументами) и тем самым помогают проверять правильность программы.
И напоследок хотелось бы сказать следующее: так как в ранних версиях С синтаксис прототипов в полном объеме не поддерживался, то в С прототипы формально не обязательны. Такой подход необходим для совместимости с С-кодом, созданным еще до появления прототипов. Но если старый С-код переносится в C++, то перед компиляцией этого кода в него необходимо добавить полные прототипы функций. Помните, что хотя прототипы в С не обязательны, но они обязательны в C++. Это значит, что каждая функция в программе на языке C++ должна иметь полный прототип. Поэтому при написании программ на С в них указываются полные прототипы функций — именно так поступает большинство программистов, работающих на этом языке.
Старомодные объявления функций
В «ранней молодости» языка С, еще до создания прототипов функций, все-таки была необходимость сообщить компилятору о типе результата функции, чтобы при вызове функции был создан правильный код. (Так как размеры разных типов данных разные, то размер типа результата надо было знать еще до вызова функции.) Это выполнялось с помощью объявления функции, не содержащего никакой информации о параметрах. С точки зрения теперешних стандартов этот старомодный подход является архаичным. Однако его до сих пор можно найти в старых кодах. По этой причине важно понимать, как он работает.
Согласно старомодному подходу, тип результата и имя функции, как показано ниже, объявляются почти что в начале программы:
#include <stdio.h>
double div(); /* старомодное объявление функции */
int main(void)
{
printf("%f", div(10.2, 20.0));
return 0;
}
double div(double num, double denom)
{
return num / denom;
}
Старомодное объявление типа функции сообщает компилятору, что функция div() возвращает результат типа double. Это объявление позволяет компилятору правильно генерировать код для вызовов этой функции. Однако оно ничего не говорит о параметрах div().
Общий вид старомодного оператора объявления функции такой:
спецификатор_типа имя_функции();
Обратите внимание, что список параметров пустой. Даже если функция принимает аргументы, то ни один из них не перечисляется в объявлении типа.
Как уже говорилось, старомодное объявление функции устарело и не должно использоваться в новом коде. Кроме того, оно несовместимо с C++.
Прототипы старомодных библиотечных функций
Любая стандартная библиотечная функция в программе должна иметь прототип. Поэтому для каждой такой функции необходимо ввести соответствующий заголовок. Все необходимые заголовки предоставляются компилятором С. В системе программирования на языке С библиотечными заголовками (обычно) являются файлы, в именах которых используется расширение .h. В заголовке имеется два основных элемента: любые определения, используемые библиотечными функциями, и прототипы библиотечных функций. Например, почти во все программы из этой книги включается файл <stdio.h>, потому что в этом файле находится прототип для printf(). Заголовки для стандартных функций описаны в части II.