6.3.2. Функции, возвращающие значение
Вторая форма оператора return предназначена для возвращения результата из функции. Каждый случай возвращения значения типа, отличного от void, должен возвратить значение. Возвращаемое значение должно иметь тип, либо совпадающий, либо допускающий неявное преобразование (см. раздел 4.11) в тип, указанный для возвращаемого значения функции при определении.
Хотя язык С++ не может гарантировать правильность результата, он способен гарантировать, что каждое возвращаемое функцией значение будет соответствовать объявленному типу. Это может получиться не во всех случаях, компилятор попытается обеспечить возвращение значения и выход только через допустимый оператор return. Например:
// некорректное возвращение значения, этот код не будет откомпилирован
bool str_subrange(const string &str1, const string &str2) {
// размеры одинаковы: возвратить обычный результат сравнения
if (str1.size() == str2.size())
return str1 == str2; // ok: == возвращает bool
// найти размер меньшей строки; условный оператор см. раздел 4.7
auto size = (str1.size() < str2.size())
? str1.size() : str2.size();
// просмотреть все элементы до размера меньшей строки
for (decltype(size) i = 0; i != size; ++i) {
if (str1[i] != str2[i])
return; // ошибка #1: нет возвращаемого значения; компилятор
// должен обнаружить эту ошибку
}
// ошибка #2: выполнение может дойти до конца функции, так и
// не встретив оператор return
// компилятор может и не обнаружить эту ошибку
}
Оператор return в цикле for является ошибочным потому, что он не в состоянии вернуть значение. Эту ошибку компилятор должен обнаружить.
Вторая ошибка заключается в том, что функция не имеет оператора return после цикла. Если произойдет вызов этой функции со строкой, являющейся подмножеством другой, процесс выполнения минует цикл for. Однако оператор return для этого случая не предусмотрен. Эту ошибку компилятор может и не обнаружить. В этом случае поведение программы во время выполнения будет непредсказуемо.
Отсутствие оператора return после цикла, который этот оператор содержит, является особенно коварной ошибкой. Однако большинство компиляторов ее не обнаружит.
Как возвращаются значения
Значения функций возвращаются тем же способом, каким инициализируются переменные и параметры: возвращаемое значение используется для инициализации временного объекта в точке вызова, и этот временный объект является результатом вызова функции.
В функциях, возвращающих локальные переменные, важно иметь в виду правила инициализации. Например, можно написать функцию, которой передают счетчик, слово и окончание. Функция возвращает множественную форму слова, если счетчик больше 1:
// возвратить множественную форму слова, если ctr больше 1
string make_plural(size_t ctr, const string &word,
const string &ending) {
return (ctr > 1) ? word + ending : word;
}
Тип возвращаемого значения этой функции — string, это значит, что возвращаемое значение копируется в точке вызова. Функция возвращает копию значения word или безымянную временную строку, полученную конкатенацией word и ending.
Когда функция возвращает ссылку, она, подобно любой другой ссылке, является только другим именем для объекта, на который ссылается. Рассмотрим, например, функцию, возвращающую ссылку на более короткую из двух переданный ей строк:
// возвратить ссылку на строку, которая короче
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
Параметры и возвращаемое значение имеют тип ссылки на const string. Строки не копируются ни при вызове функции, ни при возвращении результата.
Никогда не возвращайте ссылку на локальный объект
По завершении работы функции все хранилища ее локальных объектов освобождаются (см. раздел 6.1.1). Поэтому после завершения работы функции ссылки на ее локальные объекты ссылаются на несуществующие объекты.
// катастрофа: функция возвращает ссылку на локальный объект
const string &manip() {
string ret;
// обработать ret некоторым образом
if (!ret.empty())
return ret; // ошибка: возвращение ссылки на локальный объект!
else
return "Empty"; // ошибка: "Empty" - локальная временная строка
}
Эта функция приведет к отказу во время выполнения, поскольку она возвращает ссылку на локальный объект. Когда функция завершит работу, область памяти, которую занимала переменная ret, будет освобождена. Возвращаемое значение будет ссылаться на ту область памяти, которая уже недоступна.
Оба оператора return возвращают здесь неопределенное значение — неизвестно, что будет, попробуй мы использовать значение, возвращенное функцией manip(). В первом операторе return очевидно, что функция пытается вернуть ссылку на локальный объект. Во втором случае строковый литерал преобразуется в локальный временный объект класса string. Этот объект, как и строка s, является локальным объектом функции manip(). Область памяти, в которой располагается временный объект, освобождается по завершении функции. Оба оператора return возвращают ссылки на области памяти, которые больше недоступны.
Чтобы удостовериться в безопасности возвращения значения, следует задаться вопросом: к какому существовавшему ранее объекту относится ссылка?
Возвращать указатель на локальный объект нельзя по тем же причинам. По завершении функции локальные объекты освобождаются, и указатель указывает на несуществующий объект.
Функции, возвращающие типы класса и оператор вызова
Подобно любому оператору, оператор вызова обладает порядком (ассоциативностью) и приоритетом (см. раздел 4.1.2). У оператора вызова тот же приоритет, что и у операторов "точка" и "стрелка" (см. раздел 4.6). Как и эти операторы, оператор вызова имеет левосторонний порядок. В результате, если функция возвращает указатель, ссылку или объект типа класса, результат вызова можно использовать для обращения к члену полученного объекта.
Например, размер более короткой строки можно определить следующим образом:
// обращение к методу size объекта строки, возвращенной shorterString
auto sz = shorterString(s1, s2).size();
Поскольку эти операторы имеют левосторонний порядок, результат вызова функции shorterString() будет левым операндом точечного оператора. Этот оператор выбирает метод size() объекта строки. Этот метод становится левым операндом второго оператора вызова.
Возвращаемая ссылка является l-значением
Является ли вызов функции l-значением (см. раздел 4.1.1), зависит от типа возвращаемого значения функции. Вызовы функции, возвращающей ссылку, являются l-значением; другие типы возвращаемого значения являются r-значениями. Вызов функции, возвращающей ссылку, применяется таким же способом, как и любое другое l-значение. В частности, можно осуществлять присвоение результату вызова функции, возвращающей ссылку на неконстанту:
char &get_val(string &str, string::size_type ix) {
return str[ix]; // get_val подразумевает, что данный индекс допустим
}
int main() {
string s("a value");
cout << s << endl; // отображает значение
get_val(s, 0) = 'A'; // изменяет s[0] на A
cout << s << endl; // отображает значение A
return 0;
}
Может быть несколько странно видеть вызов функции слева от оператора присвоения. Однако в этом нет ничего необычного. Возвращаемое значение — ссылка, поэтому вызов — это l-значение, а любое l-значение может быть левым операндом оператора присвоения.
Если тип возвращаемого значения является ссылкой на константу, то (как обычно) присвоение результату вызова невозможно:
shorterString("hi", "bye") = "X"; // ошибка: возвращаемое значение
// является константой
Списочная инициализация возвращаемого значения
По новому стандарту функции могут возвращать окруженный скобками список значений. Подобно любому другому случаю возвращения значения, список используется для инициализации временного объекта, представляющего возвращение функцией значения. Если список пуст, временный объект инициализируется значением по умолчанию (см. раздел 3.3.1). В противном случае возвращаемое значение зависит от типа возвращаемого значения функции.
Для примера вернемся к функции error_msg из раздел 6.2.6. Эта функция получала переменное количество строковых аргументов и выводило сообщение об ошибке, составленное из переданных строк. Теперь вместо вызова функции error_msg() мы возвратим вектор, содержащий строки сообщений об ошибке:
vector<string> process() {
// ...
// expected и actual - строки
if (expected.empty())
return {}; // возвратить пустой вектор
else if (expected == actual)
return {"functionX", "okay"}; // возвратить вектор
// инициализированный списком
else
return {"functionX", expected, actual};
}
В первом операторе return возвращается пустой список. В данном случае возвращенный обработанный вектор будет пуст. В противном случае возвращается вектор, инициализированный двумя или тремя элементами, в зависимости от того, равны ли expected и actual.
У функции, возвращающей встроенный тип, заключенный в скобки список может содержать хотя бы одно значение, и это значение не должно требовать сужающего преобразования (см. раздел 2.2.1). Если функция возвращает тип класса, то используемые инициализаторы определяет сам класс (см. раздел 3.3.1).
Возвращение значения из функции main()
Есть одно исключение из правила, согласно которому функция с типом возвращаемого значения, отличного от void, обязана возвратить значение: функция main() может завершить работу без возвращения значения. Если процесс выполнения достигает конца функции main() и нет никакого значения для возвращения, компилятор неявно добавляет возвращение значения 0.
Как упоминалось в разделе 1.1, значение, возвращаемое из функции main(), рассматривается как индикатор состояния. Возвращение нулевого значения означает успех; большинство других значений — неудачу. У значения, отличного от нуля, есть машинно-зависимое значение. Чтобы сделать его независимым от машины, заголовок cstdlib определяет две переменные препроцессора (см. раздел 2.3.2), которые можно использовать для индикации успеха или отказа:
int main() {
if (some failure)
return EXIT_FAILURE; // определено в cstdlib
else
return EXIT_SUCCESS; // определено в cstdlib
}
Поскольку это переменные препроцессора, им не должна предшествовать часть std:: и их нельзя использовать в объявлениях using.
Рекурсия
Функция, которая вызывает себя прямо или косвенно, является рекурсивной функцией (recursive function). В качестве примера можно переписать функцию вычисления факториала так, чтобы использовать рекурсию:
// вычислить val!, т.е. 1 * 2 * 3 ... * val
int factorial(int val) {
if (val > 1)
return factorial(val-1) * val;
return 1;
}
В этой реализации осуществляется рекурсивный вызов функции factorial(), чтобы вычислить факториал числа, начиная со значения, первоначально переданного val, и далее в обратном порядке. Когда значение val достигнет 1, рекурсия останавливается и возвращается значение 1.
В рекурсивной функции всегда должно быть определено условие выхода или останова (stopping condition); в противном случае рекурсия станет бесконечной, т.е. функция продолжит вызывать себя до тех пор, пока стек программы не будет исчерпан. Иногда эта ошибка называется бесконечной рекурсией (infinite recursion). В случае функции factorial() условием выхода является равенство значения параметра val единице.
Ниже приведена трассировка выполнения функции factorial() при передаче ей значения 5.
Трассировка вызова функции factorial(5)
Вызов Возвращает Значение factorial(5) factorial(4) * 5 120 factorial(4) factorial(3) * 4 24 factorial(3) factorial(2) * 3 6 factorial(2) factorial(1) * 2 2 factorial(1) 1 1Функция main() не может вызывать сама себя.
Упражнения раздела 6.3.2
Упражнение 6.30. Откомпилируйте версию функции str_subrange(), представленной в начале раздела, и посмотрите, что ваш компилятор делает с указанными сообщениями об ошибках.
Упражнение 6.31. Когда допустимо возвращение ссылки? Когда ссылки на константу?
Упражнение 6.32. Укажите, корректна ли следующая функция. Если да, то объясните, что она делает; в противном случае исправьте ошибки, а затем объясните все.
int &get(int *arry, int index) { return arry[index]; }
int main() {
int ia[10];
for (int i = 0; i != 10; ++i)
get(ia, i) = i;
}
Упражнение 6.33. Напишите рекурсивную функцию, выводящую содержимое вектора.
Упражнение 6.34. Что случится, если условие остановки функции factorial() будет таким:
if (val != 0)
Упражнение 6.35. Почему в вызове функции factorial() мы передали val-1, а не val--?
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК