Выбрать главу

По умолчанию возвращаемое значение передается по значению, т.е. вызывающая функция получает копию результата вычисления выражения, указанного в инструкции 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].)