Как вернуть ссылку на объект c
Перейти к содержимому

Как вернуть ссылку на объект c

  • автор:

Возвращаемые значения функции ссылочного типа

Функции можно объявить, чтобы они возвращали ссылочный тип. Существует две причины создания такого объявления.

  • Возвращаемая информация представляет собой настолько крупный объект, что возврат ссылки является более эффективным, чем возврат копии.
  • Тип функции должен представлять собой l-значение.
  • Объект, на который указывает ссылка, не выйдет из области видимости при возврате управления функцией.

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

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

Пример

Рассмотрим пример Point .

// refType_function_returns.cpp // compile with: /EHsc #include using namespace std; class Point < public: // Define "accessor" functions as // reference types. unsigned& x(); unsigned& y(); private: // Note that these are declared at class scope: unsigned obj_x; unsigned obj_y; >; unsigned& Point :: x() < return obj_x; >unsigned& Point :: y() < return obj_y; >int main() < Point ThePoint; // Use x() and y() as l-values. ThePoint.x() = 7; ThePoint.y() = 9; // Use x() and y() as r-values. cout

Выходные данные

x = 7 y = 9 

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

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

Объявления ссылочных типов должны содержать инициализаторы. Исключение составляют следующие случаи.

  • Явное extern объявление
  • Объявление члена класса
  • Объявление в классе
  • Объявление аргумента в адрес функции или типа возвращаемого значения для функции

Предупреждение при возвращении адреса локальной переменной

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

// C4172 means Don't do this. Foo& GetFoo() < Foo f; . return f; >// f is destroyed here 

Компилятор выдает предупреждение в этом случае: warning C4172: returning address of local variable or temporary В простых программах доступ может случайно сохраниться, если ссылка будет использована вызывающим объектом до перезаписи соответствующей области памяти. Однако это чистая случайность. Обратите внимание на предупреждение.

Возврат ссылок

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

#include
char &replace(int i) ; // возврат ссылки
char s [80] = "Hello There";
int main()
replace(5) = 'X'; // присвоение X пробелу после Hello
cout return 0;
>
char &replace(int i)
return s [ i ];
>

Эта программа заменяет пробел между словами «Hello» и «There» символом «X». В результате программа выводит на экран «HelloXThere».

Функция replace() в соответствии со своим объявлением возвращает ссылку на символьный мас­сив. В соответствии со своей реализацией функция replace() возвращает ссылку на элемент масси­ва s, определяющийся аргументом i. Далее возвращаемая функцией replace() ссылка используется функцией main() для присвоения элементу буквы «X».

Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект

Как только программисты осознают проблемы эффективности, связанные с передачей объектов по значению (см. правило 20), они, подобно крестоносцам, преисполняются решимости искоренить зло – передачу по значению – везде, где бы оно ни пряталось. Непреклонные в своем «святом» порыве, они с неизбежностью допускают фатальную ошибку: начинают передавать по ссылке значения несуществующих объектов. А это неправильно.

Рассмотрим класс для представления рациональных чисел, включающий в себя дружественную функцию для перемножения двух таких чисел:

Rational(int numerator = 0, // см. в правиле 24 – почему этот

int denominator = 1); // конструктор не explicit

const Rational // см. в правиле 3 -

operator*(const Rational& lhs, // почему возвращаемый тип const

Ясно, что эта версия operator* возвращает результирующий объект по значению, и вы обнаружили бы непрофессиональный подход, если бы не уделили внимания вопросу о затратах на создание и удаление объекта. Вы не хотите платить за то, за что платить не должны. Отсюда вопрос: должны ли вы платить?

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

Очевидно, нет никаких оснований полагать, что такой объект существует до вызова operator*. Например, если у вас есть

Rational a(1, 2); // a = 1/2

Rational a(3, 5); // b = 3/5

Rational c = a*b; // c должно равняться 3/10

то неразумно ожидать, что уже существует то рациональное число со значением три десятых. Если operator* будет возвращать такое число, то он должен создать его самостоятельно.

Функция может создать новый объект только двумя способами: в стеке или в куче. Создание в стеке осуществляется посредством определения локальной переменной. Используя эту стратегию, вы можете попытаться написать operator* так:

const Rational& operator*(const Rational& lhs, // предупреждение!

const Rational& rhs) // плохой код!

Rational result(lhs.n * rhs.h, lhs.d * rhs.d);

Этот подход можно отвергнуть сразу, потому что вашей целью было избежать вызова конструктора, а result должен быть создан, подобно любому другому объекту. Кроме того, эта функция порождает и более серьезную проблему, поскольку возвращает ссылку на result, но result – это локальный объект, а локальные объекты разрушаются при завершении функции, в которой они объявлены. Таким образом, эта версия operator* возвращает ссылку не на Rational, а на бывший Rational – пустой, отвратительный, гнилой скелет того, что когда-то было объектом Rational, но уже не является таковым, потому что он уничтожен. Стоит вызвать эту функцию – вы попадете в область неопределенного поведения. Запомним: любая функция, которая возвращает ссылку на локальный объект, некорректна (то же касается и функций, возвращающих указатель на локальный объект).

А теперь давайте рассмотрим возможность конструирования объекта в «куче» с возвратом ссылки на него. Объекты в «куче» создаются посредством new. Вот как мог бы выглядеть operator* в этом случае:

const Rational& operator*(const Rational& lhs, // предупреждение!

const Rational& rhs) // Опять плохой код!

Rational *result = new Rational(lhs.n * rhs.h, lhs.d * rhs.d);

Да, вам все же придется расплачиваться за вызов конструктора, поскольку память, выделяемая new, инициализируется вызовом соответствующего конструктора, но теперь возникает новая проблема: кто выполнит delete для объекта, созданного вами с использованием new?

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

Rational w, x, y, z;

w = x * y * z; // то же, что operator*(operator*(x, y), z)

Здесь выполняется два вызова operator* в одном предложении, поэтому получаются два вызова new, которым должны соответствовать два delete. Но у пользователя operator* нет возможности это сделать, так как он не может получить указатели, скрытые за ссылками, которые возвращает функция operator*. Это гарантированная утечка ресурсов.

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

const Rational& operator*(const Rational& lhs, // предупреждение!

const Rational& rhs) // Код еще хуже!

static Rational result; // статический объект,

// на который возвращается ссылка

result = . ; // умножить lhs на rhs и поместить

// произведение в result

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

bool operator==(const Rational& lhs, // оператор == для Rational

Rational a, b, c, d;

действия, необходимые в случае, если два произведения равны;

действия, необходимые в противном случае;

Догадываетесь, что не так? Выражение ((a*b) == (c*d)) будет всегда равно true независимо от значений a, b, c и d!

Легче всего найти объяснение такому неприятному поведению, переписать проверку на равенство в эквивалентной функциональной форме:

if(operator==(operator*(a, b), operator*(c, d)))

Заметьте, что когда вызывается operator==, уже присутствуют два активных вызова operator*, каждый из которых будет возвращать ссылку на статический объект Rational внутри operator*. Таким образом, operator== будет сравнивать статический объект Rational, определенный в функции operator*, со значением статического объект Rational внутри той же функции. Было бы удивительно, если бы они не оказались равны всегда.

Этого должно быть достаточно, чтобы убедить вас, что возвращение ссылки из функции, подобной operator*, – пустая трата времени, но я не настолько наивен, чтобы полагаться на везение. Кое-кто в настоящий момент думает: «Хорошо, если недостаточно одного статического объекта, то, может быть, для этого подойдет статический массив…»

Я не снизойду до того, чтобы посвятить такой программе отдельный пример, но вкратце могу пояснить, почему даже возникновение такой идеи должно повергать вас в стыд. Во-первых, вы должны выбрать n – размер массива. Если n слишком мало, у вас может закончиться место для хранения, и вы ничего не выиграете по сравнению с вышеописанной программой. Если же n чересчур велико, вы уменьшаете производительность вашей программы, поскольку каждый объект в массиве конструируется при первом вызове функции. Это будет стоить вам n вызовов конструкторов и n вызовов деструкторов, даже если данная функция вызывается всего один раз. Если процесс повышения производительности программного обеспечения называется оптимизацией, тогда самое верное название происходящему – «пессимизация». И наконец, подумайте о том, как заносить необходимые вам значения в массив объектов и во что это обойдется. Наиболее прямой способ передачи объектов – операция присваивания, но с чем она связана? В общем случае это вызов деструктора (для уничтожения старого значения) плюс вызов конструктора (для копирования нового значения). А ваша цель – избежать вызовов конструктора и деструктора! Так что затея весьма неудачна (нет-нет: применение векторов вместо массивов не улучшит ситуацию).

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

inline const Rational operator*(const Rational& lhs, Rational& rhs)

return Rational(lhs.n*rhs.h, lhs.d*rhs.d);

Конечно, в этом случае вам придется смириться с издержками на вызов конструктора и деструктора для объектов, возвращаемых operator*, но в глобальном масштабе это небольшая цена за корректное поведение. Притом, вероятно, все не так уж страшно. Подобно всем языкам программирования, C++ позволяет разработчикам компиляторов применить оптимизацию для повышения производительности генерируемого кода, и, как оказывается, в некоторых случаях вызовы конструктора и деструктора возвращаемого operator* значения можно безопасно устранить. Когда компилятор пользуется этой возможностью (а часто он так и поступает), ваша программа продолжает делать то, чего вы от нее хотите, и даже быстрее, чем ожидалось.

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

Что следует помнить

• Никогда не возвращайте указатель или ссылку на локальный объект, ссылку на объект, распределенный в «куче», либо указатель или ссылку на локальный статический объект, если есть шанс, что понадобится более, чем один экземпляр такого объекта. В правиле 4 приведен пример ситуации, когда возврат ссылки на локальный статический объект имеет смысл, по крайней мере, в однопоточных средах.

Данный текст является ознакомительным фрагментом.

Продолжение на ЛитРес

Читайте также

Не пытайтесь

Не пытайтесь Худшее, что может сделать Пола в ответ на манипуляции Майка, – сказать: «Хорошо, я попытаюсь». Не хочу приплетать сюда Йоду, но в данном случае он прав. Не надо пытаться.Вам не нравится эта мысль? Может, вы думаете, что пытаться что-то сделать полезно? Ведь

(3.35) После изменения прав доступа к файлам (security permissions) на NTFS хочется вернуть все к тому виду, как было после установки. Возможно ли это?

(3.35) После изменения прав доступа к файлам (security permissions) на NTFS хочется вернуть все к тому виду, как было после установки. Возможно ли это? Да, это возможно. Более того, это просто необходимо, если вы поставили W2k на FAT или FAT32, а затем отконвертировали файловую систему в NTFS (см.

Как создать magnet-ссылку

Как создать magnet-ссылку Magnet-ссылка создается очень просто. Здесь могут быть два варианта: вы создаете ссылку на раздаваемые вами файлы или вы создаете ссылку на файлы, которые желаете закачать к себе на компьютер. Технология создания ссылки одна. Вам следует открыть список

2.17.После изменения прав доступа к файлам (security permissions) на NTFS хочется вернуть все к тому виду, как было после установки. Возможно ли это?

2.17.После изменения прав доступа к файлам (security permissions) на NTFS хочется вернуть все к тому виду, как было после установки. Возможно ли это? Да, это возможно. Более того, это просто необходимо, если вы поставили XP на FAT или FAT32, а затем cконвертировали файловую систему в NTFS. Для

Не пытайтесь заработать на чужой рекламе

Не пытайтесь заработать на чужой рекламе Показывая на своем сайте рекламу AdSense или «Яндекс. Директ», вы теряете много профессиональных покупателей в Интернете, которые часто и охотно покупают продукцию в интернет-магазинах. Это сбивает их с толку.Если вы действительно

Как, разместив одну ссылку на сайт, получить еще сто

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

1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро

1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро Программное обеспечение должно быть столь же прозрачным при выходе из строя, как и при нормальной работе. Лучше всего, если программа способна справиться

Правило 10: Операторы присваивания должны возвращать ссылку на *this

Правило 10: Операторы присваивания должны возвращать ссылку на *this Одно из интересных свойств присваивания состоит в том, что такие операции можно выполнять последовательно:int x,y,z;x = y = z = 15; // цепочка присваиванийТакже интересно, что оператор присваивания

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

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

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

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

Правило 50: Когда имеет смысл заменять new и delete

Правило 50: Когда имеет смысл заменять new и delete Вернемся к основам. Прежде всего зачем кому-то может понадобиться подменять предлагаемые компилятором версии operator new и operator delete? Существуют, по крайней мере, три распространенные причины.• Чтобы обнаруживать ошибки

1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро

1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро Программное обеспечение должно быть столь же прозрачным при выходе из строя, как и при нормальной работе. Лучше всего, если программа способна справиться

OIT и OAT должны постоянно изменяться

OIT и OAT должны постоянно изменяться Совет "OIT и OAT должны постоянно изменяться" является ключевой фразой для решения всех проблем, связанных с производительностью базы данных. Время, потраченное на изучение цикла жизни транзакций в многоверсионной архитектуре Firebird, будет

Что вы должны знать

Что вы должны знать Данная книга представляет собой не учебник по Flash, а практическое руководство по изучению Flash 8 ActionScript. Подразумевается, что вы уже немного знакомы с рабочей средой Flash и имеете какой-то опыт работы с программой.При этом вы, также как и я, не обязаны быть

10. Когда открыть, а когда закрыть

10. Когда открыть, а когда закрыть Рассмотрев деловые модели, которые поддерживают разработку программного обеспечения с открытыми текстами, мы можем теперь приблизиться к общему вопросу о том, когда исходному коду, с точки зрения экономики, имеет смысл быть «открытым», а

Будущее вернуть Сергей Тихомиров

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

Как вернуть ссылку на объект c

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

// пример некорректного возвращения значения int* max(int a, int b) < if (a >b) return &a; else return &b; >

Параметры функции аналогичны переменным - при вызове функции в стеке для них выделяется память. И вданном случае возвращается адрес участка памяти соответствующего параметра ( return &a или return &b ). Но после возвращения адреса функция завершает свою работу, соответствующие участки памяти очищаются, параметры удаляются, поэтому возвращенный адрес будет недействительным. И хотя компилятор даже может скомпилировать данную функцию, ограничившись предупреждениями, но такая функция не будет работать корректно.

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

#include int* max(int*, int*); int main() < int n; int m; int* ptr = max(&n, &m); std::cout // пример корректного возвращения значения int* max(int *a, int *b) < if (*a >*b) // разыменовываем указатели return a; else return b; >

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

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

int main() < int n; int m; std::cout 

Возвращение ссылки

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

// пример некорректного возвращения ссылки int& max(int a, int b) < if (a >b) return a; else return b; >

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

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

#include int& max(int&, int&); int main() < int n; int m; int result = max(n, m); std::cout // пример корректного возвращения ссылки int& max(int& a, int& b) < if (a >b) return a; else return b; >

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

#include const int& max(const int&, const int&); int main() < int n; int m; int result = max(n, m); std::cout // пример корректного возвращения ссылки const int& max(const int& a, const int& b) < if (a >b) return a; else return b; >

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *