Что из себя представляют функции
Функции представляют собой программные блоки, которые могут вызываться из разных частей программы. При вызове в них передаются некоторые переменные, константы, выражения, являющиеся аргументами, которые в самих процедурах и функциях воспринимаются как формальные параметры. При этом функции возвращают значение определенного типа, которое замещает в вызвавшем выражении имя вызванной функции. Например, оператор:
вызывает функцию F с аргументом X, умножает возвращенное ею значение на 5 и присваивает результат переменной I.
Допускается также вызов функции, не использующий возвращаемого ею значения. Например:
В этом случае возвращаемое функцией значение игнорируется. Функция описывается следующим образом:
тип_возвращаемого_значения имя_функции(список_параметров)
{
операторы тела функции
}
Первая строка этого описания, содержащая тип возвращаемого значения, имя функции и список параметров, называется заголовком функции. Тип возвращаемого значения может быть любым, кроме массива и функции. Могут быть также функции, не возвращающие никакого значения. В заголовке таких функций тип возвращаемого значения объявляется void.
Если тип возвращаемого значения не указан, он по умолчанию считается равным int.
Хотя тип возвращаемого значения int можно не указывать в заголовке функции, не следует использовать эту возможность. Всегда указывайте тип возвращаемого значения, кроме главной функции main. Указание типа делает программу более наглядной и предотвращает возможные ошибки, связанные с неправильным преобразованием типов.
Список параметров, заключаемый в скобки, в простейшем случае представляет собой разделяемый запятыми список вида:
тип_параметра идентификатор_параметра
Например, заголовок:
double FSum(double x1, double x2, int a);
объявляет функцию с именем FSum, с тремя параметрами X1, Х2 и А, из которых первые два имеют тип double, а последний — int. Тип возвращаемого результата — double. Имена параметров X1, Х2 и А — локальные, т.е. они имеют значение только внутри данной функции и никак не связаны с именами аргументов, переданных при вызове функции. Значения этих параметров в начале выполнения функции равны значениям аргументов на момент вызова функции.
Ниже приведен заголовок функции, не возвращающей никакого значения:
void SPrint(AnsiString S);
Она принимает один параметр типа строки и, например, отображает его в каком-нибудь окне приложения.
Если функция не принимает никаких параметров, то скобки или оставляются пустыми, или в них записывается ключевое слово void. Например:
void F1(void);
или
void F1();
Всегда указывайте void в списке параметров, если функция не получает никаких параметров. Эта делает программу более переносимой.
Роль пустого списка параметров функции в С++ существенно отличается от аналогичного списка в языке С. В С это означает, что все проверки аргументов отсутствуют (т.е. вызов функции может передать любой аргумент, который требуется). А в С++ пустой список означает отсутствие аргументов. Таким образом, программа на С, использующая эту особенность, может сообщить о синтаксической ошибке при компиляции в С++.
Как правило (хотя формально не обязательно), помимо описания функции в текст программы включается также прототип функции — ее предварительное объявление. Прототип представляет собой тот же заголовок функции, но с точкой с запятой в конце. Кроме того, в прототипе можно не указывать имена параметров. Если вы все-таки указываете имена, то их областью действия является только этот прототип функции. Вы можете использовать те же идентификаторы в любом месте программы в любом качестве. Таким образом, указание имен параметров в прототипе обычно преследует только одну цель — документирование программы, напоминание вам или сопровождающему программу человеку, какой параметр что именно обозначает.
Примеры прототипов приведенных выше заголовков функций:
double FSum(double x1, double x2, int a);
void SPrint(AnsiString S);
void F1(void);
или
double FSum(double, double, int);
void SPrint(AnsiString);
void F1();
Введение в программу прототипов функций преследует несколько целей. Во-первых, это позволяет использовать в данном модуле функцию, описанную в каком-нибудь другом модуле. Тогда из прототипа компилятор получает сведения, сколько параметров, какого типа и в какой последовательности получает данная функция. Во-вторых, если в начале модуля вы определили прототипы функций, то последовательность размещения в модуле описания функций безразлична. При отсутствии прототипов любая используемая функция должна быть описана до ее первого вызова в тексте. Это прибавляет вам хлопот, а иногда при взаимных вызовах функций друг из друга вообще невозможно. И, наконец, прототипы, размещенные в одном месте (обычно в начале модуля), делают программу более наглядной и самодокументированной. Особенно в случае, если вы снабжаете прототипы, хотя бы, краткими комментариями.
Если предполагается, что какие-то из описанных в модуле функций могут использоваться в других модулях, прототипы этих функций следует включать в заголовочный файл. Тогда в модулях, использующих данные функции, достаточно будет написать директиву #include, включающую данный заголовочный файл, и не надо будет повторять прототипы функций.
Включайте в модуль где-то в одном месте (обычно в начале) прототипы всех описанных в нем ваших функций с краткими комментариями. Это хорошо документирует программу, делает ее нагляднее, позволяет вам не заботиться о последовательности описаний функций. Если вы хотите, чтобы какие-то из описанных в модуле функций могли использовать другие модули, включайте прототипы этих функций в заголовочный файл.
Обычно функции принимают указанное в прототипе число параметров указанных типов. Однако могут быть функции, принимающие различное число параметров (например, библиотечная функция printf) или параметры неопределенных заранее типов. В этом случае в прототипе вместо неизвестного числа параметров или вместо параметров неизвестного типа ставится многоточие. Многоточие может помещаться только в конце списка параметров после известного числа параметров известного типа или полностью заменять список параметров. Например:
int prf(char *format, …);
Функция с подобным прототипом принимает один параметр format типа char *(например, строку форматирования) и произвольное число параметров произвольного типа. Функция с прототипом:
void Fp(...);
может принимать произвольное число параметров произвольного типа.
Если в прототипе встречается многоточие, то типы соответствующих параметров и их количество компилятором не проверяются.
Объявлению функции могут предшествовать спецификаторы класса памяти extern или static. Спецификатор extern предполагается по умолчанию, так что записывать его не имеет смысла. К функциям, объявленным как extern, можно полечить доступ из других модулей программы. Если же объявить функцию со спецификатором static, например:
static void F(void);
то доступ к ней из других модулей невозможен. Это надо использовать в крупных проектах во избежание недоразумений при случайных совпадениях имен функций в различных модулях.
Теперь рассмотрим описание тела функции. Тело функции пишется по тем же правилам, что и любой код программы, и может содержать объявления типов, констант, переменных и любые выполняемые операторы. Не допускается объявление и описание в теле других функций. Таким образом, функции не могут быть вложены друг в друга.
Надо иметь в виду, что все объявления в теле функции носят локальный характер. Объявленные переменные доступны только внутри данной функции. Если их идентификаторы совпадают с идентификаторами каких-то глобальных переменных модуля, то эти внешние переменные становятся невидимыми и недоступными. В этих случаях получить доступ к глобальной переменной можно, поставив перед ее именем два двоеточия "::", т.е. применив унарную операцию разрешения области действия.
Локальные переменные не просто видны только в теле функции, но по умолчанию они и существуют только внутри функции, создаваясь в момент вызова функции и уничтожаясь в момент выхода из функции. Если требуется этого избежать, соответствующие переменные должны объявляться со спецификацией static.
Выход из функции может осуществляться следующими способами. Если функция не должна возвращать никакого значения, то выход из нее происходит или по достижении закрывающей ее тело фигурной скобки, или при выполнении оператора return. Если же функция должна возвращать некоторое значение, то нормальный выход из нее осуществляется оператором return выражение, где выражение должно формировать возвращаемое значение и соответствовать типу, объявленному в заголовке функции. Например:
double FSum(double x1, double x2, int a)
{
return a *(x1 + x2);
}
Ниже приведен пример функции, не возвращающей никакого значения:
void SPrint(AnsiString S)
{
if (S != "")
ShowMessage (S);
}
Здесь возврат из функции происходит по достижении закрывающейся фигурной скобки тела функции. Приведем вариант той же функции, использующий оператор return:
void SPrint(AnsiString S)
{
if (S == "") return;
ShowMessage(S);
}
Прервать выполнение функции можно также генерацией какого-то исключения. Наиболее часто в этих целях используется процедура Abort, генерирующая «молчаливое» исключение EAbort, не связанное с каким-то сообщением об ошибке. Если в программе не предусмотрен перехват этого исключения, го применение функции Abort выводит управление сразу наверх из всех вложенных друг в друга вызовов функций.
Возвращаемое функцией значение может включать в себя вызов каких-то функций. В том числе функция может вызывать и саму себя, т.е. допускается рекурсия. В качестве примера приведем функцию, рекурсивно вычисляющую факториал. Как известно, значение факториала равно n! = n (n-1) (n-2) ... 1, причем считается, что 1! = 1 и 0! = 1. Факториал можно вычислить с помощью простого цикла for (и это, конечно, проще). Но можно факториал вычислять и с помощью рекуррентного соотношения n!=n(n—1)!. Для иллюстрации рекурсии воспользуемся именно этим соотношением. Тогда функция factorial вычисления факториала может быть описана следующим образом:
unsigned long factorial(unsigned long n)
{
if {n <= 1)
return 1;
else
return n*factorial(n - 1);
}
Если значение параметра n равно 0 или 1, то функция возвращает значение 1. В противном случае функция умножает текущее значение n на результат, возвращаемый вызовом той же функции factorial, но со значением параметра n, уменьшенным на единицу. Поскольку при каждом вызове значение параметра уменьшается, рано или поздно оно станет равно 1. После этого цепочка рекурсивных вызовов начнет свертываться и в конце концов вернет значение факториала.