Объявление и вызов функций

Практический курс по C/C++: https://stepik.org/course/193691

Мы начинаем большую и ключевую тему языка Си, посвященную функциям. До сих пор у нас в программах объявлялась только одна функция с именем main, из которой вызывались другие стандартные функции:

#include <stdio.h>
 
int main(void) 
{
         int x;
         if(scanf("%d", &x) == 1)
                   puts("Correct input!");
         else
                   puts("Incorrect input!");
 
         return 0;
}

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

DRY – Don’t Repeat Yourself

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

Объявление и вызов функций

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

<тип данных> <имя функции>([набор параметров])
{
        оператор_1;
        ...
        оператор_N;
}

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

int, char, double, short, ...

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

void

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

get_coord, show_x, is_digit, и т.п.

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

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

Давайте в качестве примера объявим функцию, которая выводит приветственное сообщение:

#include <stdio.h>
 
void print_hi(void)
{
         puts("Hello! I'm Sergey Balakirev!");
}
 
int main(void) 
{
         return 0;
}

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

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

Итак, если сейчас запустить программу, то никакого вывода на экран не увидим. Почему? Потому что функция всего лишь объявлена, но нигде не была вызвана (не запущена). Давайте сделаем вызов нашей функции в main() следующим образом:

int main(void) 
{
         print_hi();
         return 0;
}

Обратите внимание на круглые скобки после имени функции print_hi. Это операция вызова функции. Если их убрать, то программа выполнится, но функция print_hi() запущена не будет. Круглые скобки строго обязательны для запуска функции.

Если прописать еще два вызова этой же функции:

int main(void) 
{
         print_hi();
         print_hi();
         print_hi();
         return 0;
}

то при выполнении программы увидим три строчки приветствия:

Hello! I'm Sergey Balakirev!
Hello! I'm Sergey Balakirev!
Hello! I'm Sergey Balakirev!

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

Объявление и вызов функций с параметрами

Приведенная функция print_hi(), по большому счету, бесполезна. Она не дает нам ничего существенно нового. И это не удивительно, так как она ничего не принимает и ничего не возвращает. Давайте усложним пример и объявим функцию для вычисления периметра прямоугольника. Что нам для этого нужно? Очевидно, функция должна иметь информацию о длине и ширине прямоугольника, иначе периметр не вычислить. И, кроме того, должна вернуть вычисленное значение, чтобы мы его могли использовать дальше по программе.

Пусть функция называется get_per, а ширина и длина прямоугольника определяются целочисленными переменными:

int width, int height

Тогда объявить эту функцию можно следующим образом:

int get_per(int width, int height)
{
         int p = 2 * (width + height);
         return p;
}

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

int get_per(int width, height)       // ошибка

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

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

Кстати, тело функции в данном случае можно было бы записать в одну строчку, не создавая промежуточную переменную p:

int get_per(int width, int height)
{
         return 2 * (width + height);
}

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

Забегая вперед отмечу, что все обычные параметры и переменные, объявленные внутри функции, создаются в момент вызова этой функции и исчезают при ее завершении. То есть, параметры width, height и переменная p доступны только внутри функции get_per() и не существуют за ее пределами. Более подробно мы с вами об этом еще будем говорить.

Давайте теперь посмотрим, на способы вызова функции get_per():

int main(void) 
{
         int w = 2, h = 5;
         
         get_per(w, h);
         int per_1 = get_per(w+1, h-4);
         int per_2 = get_per(w, 3);
         int per_3 = get_per(1, 3);
 
         return 0;
}

Смотрите, в первом случае мы вызвали get_per(w, h) и не сохраняли возвращаемое ею значение. Так вполне можно делать, если не важно, что именно при вызове вернет функция. В этом случае говорят, что функция была вызвана ради побочного эффекта. То есть, не ради того, чтобы получить возвращаемое значение, а только ради выполнения операторов тела функции. Например, так часто поступают при вызове функции printf():

printf("get_per\n");

Она возвращает число байт, помещенных в выходной поток stdout, но нас это значение не интересует, так как обычно с выводом данных проблем не возникает.

Если же возвращаемое значение важно, то его можно сохранить с помощью операции присваивания в переменной соответствующего типа.

Далее, при каждом вызове функции get_per() в круглых скобках указываются значения, которые ей передаются. Эти значения называются аргументами и их должно быть ровно столько, сколько прописано параметров при объявлении функции. Каждое переданное значение копируется в соответствующий параметр: первый аргумент копируется в параметр width, а второй – в параметр height. Обратите внимание, данные именно копируются, то есть, создается их копия в переменных функции width и height. Соответственно, при первом вызове get_per() в параметры width и height записываются значения 2 и 5; при втором – значения 2+1 = 3 и 5-4 = 1; при третьем – 2 и 3; при четвертом – 1 и 3.

Если в момент вызова функции get_per() указать неверное число аргументов, например:

int per_3 = get_per(1);

то при компиляции возникнет ошибка. Число аргументов в нашем примере должно совпадать с числом параметров функции.

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме