Оператор return. Вызов функций в аргументах

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

Смотреть материал на YouTube | RuTube

На этом занятии подробнее ознакомимся с работой оператора return.

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

#include <stdio.h>
#include <math.h>
 
double sq4_x(double x)
{
         double res = (x < 0) ? NAN : pow(x, 0.25);
         return res;
}
 
int main(void) 
{
         printf("%f\n", sq4_x(16));
         return 0;
}

Функция sq4_x() имеет возвращаемый тип double и один параметр этого же типа. Затем, во временную переменную res заносится значение nan (сокращение от Not a Number – не число) если переменная x меньше нуля, а иначе корень четвертой степени. Далее, записан оператор return с указанием возвращаемого значения – переменной res. Ниже, в функции main(), вызывается функция sq4_x() с аргументом 16 и значение выводится на экран. После запуска программы увидим результат:

2.000000

Очевидно, это число 2.0 было возвращено функцией sq4_x() благодаря оператору return. Если этот оператор убрать, то функция будет возвращать неопределенные значения. Однако в современных стандартах языка Си/С++ оператор return строго обязателен, если тип функции отличен от void. Поэтому следует придерживаться правила: если функция возвращает какой-либо определенный тип данных (не void), то в ней следует прописывать оператор return.

В действительности, этот оператор делает две вещи: собственно, возвращает указанные значения; и завершает выполнение функции. Вот этот второй момент не следует упускать из виду. Например, если после оператора return прописать еще какой-либо оператор, например:

double sq4_x(double x)
{
         double res = (x < 0) ? NAN : pow(x, 0.25);
         return res;
         puts("sq4_x");
}

то он выполнен не будет, так как выполнение функции завершится на операторе return.

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

double sq4_x(double x)
{
         if(x < 0)
                   return NAN;
 
         return pow(x, 0.25);
}

Вначале проверяем, если x меньше нуля, то сразу завершаем выполнение функции с возвращением специального значения nan. Иначе, при ложности условия, выполнение тела функции дойдет до второго оператора return и будет вычислен корень четвертой степени.

Оператор return в функции типа void

Если при объявлении функции возвращаемый тип указан как void, то оператор return в ней прописывать не обязательно. Например, функция, которая выводит на экран значение переданной переменной, если она четная:

void print_even_x(int x)
{
         if(x % 2 == 0)
                   printf("x = %d\n", x);
}

Здесь нет никакого оператора return. Функция просто выводит значение для четных чисел. Ниже в функции main() мы можем ее вызвать следующим образом:

int main(void) 
{
         printf("%f\n", sq4_x(-16));
         print_even_x(4);
 
         return 0;
}

Программа скомпилируется и успешно запустится. Однако эту же функцию print_even_x() можно записать и с оператором return, например, так:

void print_even_x(int x)
{
         if(x % 2 != 0)
                   return;
 
         printf("x = %d\n", x);
}

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

Функции, как отдельные, независимые модули программы

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

  • функции следует выполнять только свою узко поставленную задачу;
  • для решения поставленной задачи, функции следует использовать только переданные ей параметры;
  • если тело функции становится слишком большим – это повод задуматься над правильностью организации логики программы; возможно, подзадачу следует раздробить на более мелкие подзадачи;
  • в 99% случаях функции ничего не считывают с клавиатуры (из потока stdin) и ничего не выводят на экран (в поток stdout); если ваша функция делает это, убедитесь, что это действительно необходимо.

Вызов функций в аргументах

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

double min_2(double a, double b)
{
         return (a < b) ? a : b;
}

А, затем, на основе этой функции, определим вторую – для нахождения минимального среди трех чисел:

double min_3(double a, double b, double c)
{
         double min_ab = min_2(a, b);
         return (min_ab < c) ? min_ab : c;
}

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

Давайте теперь посмотрим, как можно использовать эти функции:

int main(void) 
{
         int x = 1, y = -2, z = 10;
 
         double res_1 = min_2(x, y);
         double res_2 = min_3(x, y, z);
         double res_3 = min_2(min_2(x, y), z);
         double res_4 = min_2(x, min_2(y, z));
 
         printf("res_1 = %.2f, res_2 = %.2f, res_3 = %.2f, res_4 = %.2f\n", 
                      res_1, res_2, res_3, res_4);
 
         return 0;
}

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

Как вы понимаете, мы можем пойти еще дальше и использовать функции min_2() и min_3() следующим образом:

double r4 = min_2(min_2(-2, 3), min_2(x, y));
double r5 = min_2(min_2(-2, 3), min_3(x, y, z));

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

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

double res_1 = min_2(++x, x);
printf("res_1 = %.2f\n", res_1);

И думаем, ага, вначале отработает первый аргумент и переменная x увеличится на 1, а затем, второй. В результате, минимальное будет равно 2. Но после запуска программы видим другое значение 1. А вот если в моем случае записать инкремент у второго аргумента:

double res_1 = min_2(x, ++x);

то получаем значение 2. То есть, порядок иногда имеет значение, но, так как мы его не знаем, то полагаться на подобные конструкции не стоит. Можно использовать инкремент или декремент, только если такая же переменная не стоит в других аргументах, например, так:

double res_1 = min_2(y, ++x);

В этом случае порядок для нас не важен. И, обратите внимание, операция инкремента у переменной x записана в префиксной форме. Почему именно так? Очевидно, в этом случае значение второго параметра b функции min_2() будет принимать значение:

b = ++x;

То есть, значение x будет увеличено на 1 и только после этого присвоено переменной b. Если же при вызове функции min_2() использовать постфиксную форму записи:

double res_1 = min_2(y, x++);

то это будет эквивалентно присваиванию:

b = x++;

Соответственно, значение параметра b будет равно прежнему значению x без увеличения на единицу. Вот этот момент нужно хорошо себе представлять.

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

Видео по теме