7.4. Возврат значения

7.4. Возврат значения

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

return;

return expression;

Первая форма используется в функциях, для которых типом возвращаемого значения является void. Использовать return в таких случаях обязательно, если нужно принудительно завершить работу. (Такое применение return напоминает инструкцию break, представленную в разделе 5.8.) После конечной инструкции функции подразумевается наличие return. Например:

void d_copy( double "src, double *dst, int sz )

{

/* копируем массив "src" в "dst"

* для простоты предполагаем, что они одного размера

*/

// завершение, если хотя бы один из указателей равен 0

if ( !src || !dst )

return;

// завершение,

// если указатели адресуют один и тот же массив

if ( src == dst )

return;

// копировать нечего

if ( sz == 0 )

return;

// все еще не закончили?

// тогда самое время что-то сделать

for ( int ix = 0; ix sz; ++ix )

dst[ix] = src[ix];

// явного завершения не требуется

}

Во второй форме инструкции return указывается то значение, которое функция должна вернуть. Это значение может быть сколь угодно сложным выражением, даже содержать вызов функции. В реализации функции factorial(), которую мы рассмотрим в следующем разделе, используется return следующего вида:

return val * factorial(val-1);

В функции, не объявленная с void в качестве типа возвращаемого значения, обязательно использовать вторую форму return, иначе произойдет ошибка компиляции. Хотя компилятор не отвечает за правильность результата, он сможет гарантировать его наличие. Следующая программа не компилируется из-за двух мест, где программа завершается без возврата значения:

// определение интерфейса класса Matrix

#include "Matrix.h"

bool is_equa1( const Matrix ml, const Matrix m2 )

{

/* Если содержимое двух объектов Matrix одинаково,

* возвращаем true;

* в противном случае - false

*/

// сравним количество столбцов

if ( ml.colSize() != m2.co1Size() )

// ошибка: нет возвращаемого значения

return;

// сравним количество строк

if ( ml.rowSize() != m2.rowSize() )

// ошибка: нет возвращаемого значения

return;

// пробежимся по обеим матрицам, пока

// не найдем неравные элементы

for ( int row = 0; row ml.rowSize(); ++row )

for ( int col = 0; co1 ml.colSize(); ++co1 )

if ( ml[row][col] != m2[row][col] )

return false;

// ошибка: нет возвращаемого значения

// для случая равенства

}

Если тип возвращаемого значения не точно соответствует указанному в объявлении функции, то применяется неявное преобразование типов. Если же стандартное приведение невозможно, происходит ошибка компиляции. (Преобразования типов рассматривались в разделе 4.1.4.)

По умолчанию возвращаемое значение передается по значению, т.е. вызывающая функция получает копию результата вычисления выражения, указанного в инструкции return. Например:

Matrix grow( Matrix* p ) {

Matrix val;

// ...

return val;

}

grow() возвращает вызывающей функции копию значения, хранящегося в переменной val.

Такое поведение можно изменить, если объявить, что возвращается указатель или ссылка. При возврате ссылки вызывающая функция получает l-значение для val и потому может модифицировать val или взять ее адрес. Вот как можно объявить, что grow() возвращает ссылку:

Matrix grow( Matrix* p ) {

Matrix *res;

// выделим память для объекта Matrix

// большого размера

// res адресует этот новый объект

// скопируем содержимое *p в *res

return *res;

}

Если возвращается большой объект, то гораздо эффективнее перейти от возврата по значению к использованию ссылки или указателя. В некоторых случаях компилятор может сделать это автоматически. Такая оптимизация получила название именованное возвращаемое значение. (Она описывается в разделе 14.8.)

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

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

// ошибка: возврат ссылки на локальный объект

Matrix add( Matrix m1, Matrix m2 )

{

Matrix result:

if ( m1.isZero() )

return m2;

if ( m2.isZero() )

return m1;

// сложим содержимое двух матриц

// ошибка: ссылка на сомнительную область памяти

// после возврата

return result;

}

В таком случае тип возврата не должен быть ссылкой. Тогда локальная переменная может быть скопирована до окончания времени своей жизни:

Matrix add( ... )

• функция возвращает l-значение. Любая его модификация затрагивает сам объект. Например:

#include vector

int get_val( vectorint vi, int ix ) {

return vi [ix];

}

int ai[4] = { 0, 1, 2, 3 };

vectorint vec( ai, ai+4 ); // копируем 4 элемента ai в vec

int main() {

// увеличивает vec[0] на 1

get_val( vec.0 )++;

// ...

}

Для предотвращения нечаянной модификации возвращенного объекта нужно объявить тип возврата как const:

const int get_val( ... )

Примером ситуации, когда l-значение возвращается намеренно, чтобы позволить модифицировать реальный объект, может служить перегруженный оператор взятия индекса для класса IntArray из раздела 2.3.

7.4.1. Передача данных через параметры и через глобальные объекты

Различные функции программы могут общаться между собой с помощью двух механизмов. (Под словом “общаться” мы подразумеваем обмен данными.) В одном случае используются глобальные объекты, в другом – передача параметров и возврат значений.

Глобальный объект определен вне функции. Например:

int glob;

int main() {

// что угодно

}

Объект glob является глобальным. (В главе 8 рассмотрение глобальных объектов и глобальной области видимости будет продолжено.) Главное достоинство и одновременно один из наиболее заметных недостатков такого объекта – доступность из любого места программы, поэтому его обычно используют для общения между разными модулями. Обратная сторона медали такова:

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

* при модификации такой программы повышается вероятность ошибок. Даже для внесения локальных изменений необходимо понимание всей программы в целом;

* если глобальный объект получает неверное значение, ошибку нужно искать по всей программе. Отсутствует локализация;

* используя глобальные объекты, труднее писать рекурсивные функции (Рекурсия возникает тогда, когда функция вызывает сама себя. Мы рассмотрим это в разделе 7.5.);

* если используются потоки (threads), то для синхронизации доступа к глобальным объектам требуется писать дополнительный код. Отсутствие синхронизации – одна из распространенных ошибок при использовании потоков. (Пример использования потоков при программировании на С++ см. в статье “Distributing Object Computing in C++” (Steve Vinoski and Doug Schmidt) в [LIPPMAN96b].)

Можно сделать вывод, что для передачи информации между функциями предпочтительнее пользоваться параметрами и возвращаемыми значениями.

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

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

Упражнение 7.9

Каковы две формы инструкции return? Объясните, в каких случаях следует использовать первую, а в каких вторую форму.

Упражнение 7.10

Найдите в данной функции потенциальную ошибку времени выполнения:

vectorstring readText( ) {

vectorstring text;

string word;

while ( cin word ) {

text.push_back( word );

// ...

}

// ....

return text;

}

Упражнение 7.11

Каким способом вы вернули бы из функции несколько значений? Опишите достоинства и недостатки вашего подхода