Урок #9 — Указатели и ссылки
Каждая переменная или объект хранит данные в определенной ячейке в памяти компьютера. Каждый раз, создавая новую переменную, мы создаем новую ячейку в памяти, с новым значением для неё. Чем больше ячеек, тем больше компьютерной памяти будет занято.
Адрес в памяти компьютера это число, к которому мы можем получить доступ. Указатель — это тот же адрес в памяти, по которому мы получаем переменную и по итогу её значение.
Чтобы работать с указателями необходимо воспользоваться двумя специальными символами: & и * . Пример использования:
Переменные t и p будут равны числу 237, так как оба ссылаются на одну ячейку. Сам же компьютер на вычислении обеих переменных потратит меньше усилий, ведь обе переменные ссылаются на одно и то же.
Ссылки в C++
Ссылки и указатели схожи между собой, так как оба в качестве значения имеют лишь адрес некого объекта.
Указатель хранит адрес ячейки и если мы захотим изменить значение этой ячейки, то нам придется выполнить операцию «разыменования»:
В ссылках такого понятия нет, так как меняя ссылку вы автоматически меняете и переменную. Ссылки напрямую ссылаются к переменной, поэтому их синтаксис проще:
Использование ссылок и указателей оправдано в случае передачи данных в функции или же в объекты. Также данные технологии отлично подойдут для передачи большого объема данных в ходе программы.
Ссылки // FAQ C++
Здесь i и j – псевдонимы соответственно для x и y из main . Другими словами, i – это x , не указатель на x и не копия x , а сам x . Всё, что вы делаете с i , происходит с x , и наоборот. Это включает в себя взятие адреса. Значения &i и &x идентичны.
Вот как программист должен думать о ссылках. Рискую сбить вас с толку, представив вам другую точку зрения, как реализуются ссылки. В конечном итоге ссылка i на объект x обычно является машинным адресом объекта x . Но когда программист говорит i++ , компилятор генерирует код, увеличивающий x . То есть биты адреса, которые компилятор использует для нахождения x , не изменяются. Программист на C будет думать об этом так, как если бы вы использовали указатель передачи в стиле C, с синтаксическим вариантом (1) перемещения & от вызывающего объекта к вызываемому и (2) удаления *s . Другими словами, программист на C будет думать о i как о макросе для (*p) , где p – указатель на x (например, компилятор автоматически разыменовывает базовый указатель; i++ меняется на (*p)++ ; i = 7 автоматически заменяется на (*p) = 7 .
Важное примечание: Несмотря на то, что в используемом ассемблере ссылка часто реализуется с использованием адреса, не думайте о ссылке как о забавно выглядящем указателе на объект. Ссылка – это объект, только с другим именем. Это не указатель на объект и не его копия. Это объект. В C++ нет синтаксиса, позволяющего работать с самой ссылкой отдельно от объекта, на который она ссылается.
Что произойдет, если вы выполните присваивание ссылке?
Вы измените состояние референта (референт — это объект, на который ссылается ссылка).
Помните: ссылка – это референт, поэтому изменение ссылки изменяет состояние референта. На жаргоне разработчика компилятора ссылка – это "lvalue" (то, что может появиться слева от оператора присваивания).
Что будет, если вы вернете ссылку?
Вызов функции может появляться слева от оператора присваивания.
Поначалу эта способность может показаться странной. Например, никто не считает, что выражение f() = 7 имеет смысл. Тем не менее, если a является объектом класса Array , большинство людей думает, что a[i] = 7 имеет смысл, даже если a[i] – это на самом деле просто замаскированный вызов функции (он вызывает метод Array::operator[](int) , который является оператором индекса для класса Array ).
Что означает object.method1().method2() ?
Данная строка связывает вызовы этих методов, поэтому это называется цепочкой вызовов методов.
Первое, что выполняется, – это object.method1() . Он возвращает некий объект, который может быть ссылкой на объект (т.е. method1() может заканчиваться с return *this; ), или это может быть какой-то другой объект. Назовем возвращенный объект objectB . Затем objectB становится объектом this для method2() .
Чаще всего цепочки методов используются в библиотеке iostream. Например, cout << x << y работает, потому что cout << x – это функция, возвращающая cout .
Менее распространенное, но всё же довольно изящное использование цепочки методов — это идиома именованного параметра.
Как можно изменить ссылку так, чтобы она ссылалась на другой объект?
Ссылку нельзя отделить от референта.
В отличие от указателя, ссылка, привязанная к объекту, не может быть «переустановлена» на другой объект. Ссылка не является отдельным объектом. У нее нет идентичности. Взяв адрес ссылки, вы получите адрес референта. Помните: ссылка – это ее референт.
В этом смысле ссылка похожа на константный указатель, такой как int* const p (в отличие от указателя на константу, такого как const int* p ). Но, пожалуйста, не путайте ссылки с указателями; они очень разные с точки зрения программиста.
Почему в C++ есть и указатели, и ссылки?
C++ унаследовал указатели от C, поэтому их нельзя было удалить без серьезных проблем с совместимостью. Ссылки полезны для нескольких вещей, но непосредственной причиной их появления в C++ была поддержка перегрузки операторов. Например:
В более общем плане, если вы хотите иметь как функциональность указателей, так и функциональность ссылок, вам нужны либо два разных типа (как в C++), либо два разных набора операций с одним типом. Например, с одним типом вам нужны как операция для присваивания объекту, на который ссылаетесь, так и операция для присваивания ссылке/указателю. Это можно сделать с помощью отдельных операторов (как в Simula). Например:
В качестве альтернативы вы можете полагаться на проверку (перегрузку) типа. Например:
Когда мне следует использовать ссылки, а когда – указатели?
Используйте ссылки, когда можете, и указатели, когда вам нужно.
Ссылки обычно предпочтительнее указателей, если вам не нужна «переустановка». Обычно это означает, что ссылки наиболее полезны в общедоступном интерфейсе класса. Ссылки обычно появляются на поверхности объекта, а указатели – внутри.
Исключением из вышеизложенного является ситуация, когда параметру функции или возвращаемому значению требуется «контрольная» ссылка – ссылка, которая не ссылается на объект. Обычно это лучше всего сделать, возвращая/принимая указатель и придавая этот особый смысл значению nullptr (ссылки всегда должны быть псевдонимами объектов, а не разыменованного нулевого указателя).
Примечание. Старомодные C-программисты иногда не любят ссылки, поскольку они предоставляют семантику ссылок, которая не является явной в коде вызывающего. Однако после некоторого опыта работы с C++ можно быстро понять, что ссылки – это форма сокрытия информации, которая скорее предоставляет удобства, чем является помехой. То есть программисты должны писать код на языке задачи, а не на языке машины.
Что означает, что ссылка должна ссылаться на объект, а не на разыменованный нулевой указатель?
Это значит, что следующий код нелегален:
ПРИМЕЧАНИЕ: Пожалуйста, не пишите нам, говоря, что приведенный код работает с вашей конкретной версией вашего конкретного компилятора. Это всё еще нелегально. Язык C++, как он определен стандартом C++, говорит, что это незаконно. Стандарт C++ не требует диагностики для этой конкретной ошибки, что означает, что ваш конкретный компилятор не обязан замечать, что p имеет значение nullptr , и выдавать сообщение об ошибке, но это всё равно незаконно. Язык C++ также не требует, чтобы компилятор генерировал код, который взорвался бы во время выполнения. Фактически, ваша конкретная версия вашего конкретного компилятора может генерировать, а может и не генерировать код, который приведен выше, и который, по вашему мнению, имеет смысл. Но суть в следующем: поскольку от компилятора не требуется генерирование разумного кода, вы не знаете, что компилятор сделает. Поэтому, пожалуйста, не пишите, что ваш конкретный компилятор генерирует хороший код; нам все равно. Это всё еще незаконно. Дополнительные сведения смотрите в стандарте C++, например, C++ 2014 в разделе 8.3.2 [dcl.ref] p5.
В качестве примера, а не в качестве ограничения, некоторые компиляторы оптимизируют тесты nullptr , поскольку они «знают», что все ссылки относятся к реальным объектам, и что ссылки никогда (легально) не являются разыменованными nullptr . Это может заставить компилятор оптимизировать следующий тест:
Как указано выше, это всего лишь пример того, что может делать ваш компилятор на основе языкового правила, согласно которому ссылка должна ссылаться на действительный объект. Не ограничивайте свое мышление приведенным выше примером; основная идея ответа на этот вопрос заключается в том, что компилятор не обязан делать что-то разумное, если вы нарушаете правила. Поэтому не нарушайте правила.
Пациент: «Доктор, доктор, у меня болит глаз, когда тыкаю его ложкой».
Врач: "Тогда не тыкайте".
Что такое дескриптор (handle) объекта? Это указатель? Это ссылка? Это указатель на указатель? Что это?
Термин дескриптор (handle) используется для обозначения любого метода, который позволяет вам перейти к другому объекту, — обобщенного псевдоуказателя. Термин (намеренно) двусмысленный и расплывчатый.
В некоторых случаях двусмысленность на самом деле является преимуществом. Например, на ранней стадии проектирования вы можете быть не готовы к определенному представлению дескрипторов. Возможно, вы не уверены, нужны ли вам простые указатели, ссылки или указатели на указатели, или ссылки на указатели, или целочисленные индексы в массиве, или строки (или другой ключ), которые можно найти в хеш-таблице (или другой структуре данных), или ключи базы данных, или какая-либо другая технология. Если вы просто знаете, что вам понадобится какая-то штука, которая будет однозначно идентифицировать объект и обращаться к нему, вы называете эту штуку Handle (дескриптор).
Итак, если ваша конечная цель – позволить куску кода однозначно идентифицировать/найти конкретный объект некоего класса Fred , вам необходимо в этот кусок кода передать дескриптор Fred . Дескриптор может быть строкой, которая может использоваться в качестве ключа в какой-нибудь известной таблице поиска (например, ключ в std::map<std::string,Fred> или std::map<std::string,Fred*> ), или это может быть целое число, которое будет индексом в некотором известном массиве (например, Fred* array = new Fred[maxNumFreds] ), или это может быть просто Fred* , или это может быть что-то другое.
Новички часто вспоминают об указателях, но на самом деле использование сырых указателей чревато рисками. Например, что, если объект Fred нужно переместить? Как узнать, безопасно ли удалять объекты Fred ? Что, если объект Fred необходимо (временно) сериализовать на диске? И т.д. и т.д. В большинстве случаев для управления подобными ситуациями мы добавляем дополнительные уровни косвенного обращения. Например, дескрипторы могут быть Fred** , где указывающие на Fred* указатели гарантированно никогда не перемещаются; но когда объекты Fred необходимо переместить, вы просто обновляете указывающие на Fred* указатели. Или вы делаете дескриптор целым числом, а затем ищете объекты Fred (или указатели на объекты Fred ) в таблице / массиве / где угодно.
Дело в том, что мы используем слово «дескриптор» (Handle), когда еще не знаем деталей того, что мы собираемся делать.
В другой ситуации мы используем слово «дескриптор», когда хотим говорить размыто о том, что уже сделали (иногда для этого также используется термин «волшебное печенье» (magic cookie), например, «Программа передает волшебное печенье (куки), которое используется для уникального определения и поиска соответствующего объекат Fred »). Причина, по которой мы (иногда) хотим говорить размыто о том, что уже сделали, состоит в том, чтобы минимизировать волновой эффект, если/когда конкретные детали/представление дескриптора изменятся. Например, если/когда кто-то изменит дескриптор со строки, которая используется в таблице поиска, на целое число, которое ищется в массиве, мы не хотим идти и обновлять миллион строк кода.
Чтобы еще больше упростить поддержку, если/когда детали/представление дескриптора изменятся (или в целом для облегчения чтения/записи кода), мы часто инкапсулируем дескриптор в класс. Этот класс часто перегружает операторы operator-> и operator* (поскольку дескриптор действует как указатель, то он может и выглядеть как указатель).
Что следует использовать, вызов по значению или вызов по ссылке?
(Примечание: этот ответ необходимо обновить для C++ 11.)
Это зависит от того, чего вы пытаетесь достичь:
- если вы хотите изменить переданный объект, вызовите его по ссылке или используйте указатель; например, void f(X&); или void f(X*);
- если вы не хотите изменять переданный объект, и он большой, вызовите по константной ссылке; например, void f(const X&);
- в противном случае вызовите по значению; например void f(X);
Что значит «большой»? Всё, что больше пары слов.
Зачем вам изменять аргумент? Что ж, нам часто приходится делать подобное, но часто у нас есть и альтернатива: создать новое значение. Рассмотрим:
Читающему, вероятно, легче понять incr2() . То есть incr1() с большей вероятностью приведет к ошибкам. Поэтому, если создание и копирование нового значения не являются дорогостоящими, мы должны предпочесть стиль, который возвращает новое значение, а не стиль, который изменяет существующее значение.
Если вы действительно хотите изменить аргумент, что в этом случае следует использовать, указатель или ссылку? Если возможна передача «не объекта» (например, нулевого указателя), имеет смысл использование указателя. Предпочтительный стиль, когда вы хотите изменить объект, – использовать указатель потому, что в некоторых контекстах это позволяет проще дать знать, что модификация возможна.
Также обратите внимание, что вызов функции-члена по сути является вызовом по ссылке на объект, поэтому мы часто используем функции-члены, когда хотим изменить значение/состояние объекта.
Почему this – это не ссылка?
Потому что this был введен в C++ (на самом деле в C с классами) до добавления ссылок. Кроме того, Страуструп выбрал this , чтобы следовать примеру Simula, а не (позднее) использованию self в Smalltalk.
Указатели и ссылки в языке C++
Указатели представляют собой объекты, значением которых служат адреса других объектов:
- переменных
- констант
- функций
- других указателей
Объявление указателей
Синтаксис объявления указателей аналогичен объявлению переменных, за исключением того, что между типом данных и именем переменной должен быть указан символ "*" ("звездочка").
Инициализация указателей
Указателю можно присвоить адрес объекта, полученный с помощью оператора взятия адреса &. Стоит отметить, что оператор & не возвращает напрямую адрес своего операнда. Вместо этого он возвращает указатель, содержащий адрес.
Указателю нельзя присвоить адрес переменной другого типа. То есть нельзя указателю типа int* присвоить адрес переменной типа double.
Также указателю можно присвоить значение другого указателя.
Указатель также может быть проинициализирован пустым значением. Это можно сделать несколькими способами:
- использовать значение 0 или макроопределение NULL
- использовать значение nullptr
- использовать значение std::nullptr_t (C++ 11)
В некоторых случаях, использование значения 0 в качестве аргумента функции может привести к проблемам, так как компилятор не сможет определить, используется ли нулевой указатель или целое число. Поэтому использование значения nullptr является предпочтительным способом присвоить указателю пустое значение.
Тип std::nullptr_t может иметь только одно значение — nullptr. Использование этого типа поможет в тех редких случаях, когда существуют перегруженные функции и требуется передать нулевой указатель. В этом случае непонятно какую именно функцию нужно будет вызвать. Поэтому в таком случае в функции можно задать аргумент с типом std::nullptr_t.
Напрямую записать адрес в указатель можно только с помощью операций преобразования типов, либо операции reinterpret_cast.
Разыменование указателей
Для получения значения переменной, на которую ссылается указатель, используется операция разыменования указателя. Эта операция записывается как символ * (звездочка), написанный перед указателем.
Арифметические действия с указателями
С указателем можно производить следующие арифметические действия:
- сложение и вычитание с целым числом
- операции инкремента/декремента
При использовании арифметических операций, указатель изменяется на величину кратную размеру типа указателя. Например, если указатель имеет тип 32-разрядного int, то увеличение указателя на 1 приведет к увеличению значения адреса в указателе на 4.
Указатель на указатель
В языке C++ можно объявить указатель, который будет указывать на другой указатель.
Синтаксис объявления такой же, как и у объявления указателя, за исключением того, что ставится два символа * (звездочка).
Указатель на указатель работает подобно обычному указателю: его можно разыменовать для получения значения, на которое он указывает. И, поскольку этим значением является другой указатель, для получения исходного значения потребуется выполнить разыменование еще раз. Разыменования можно выполнять последовательно:
Использовать указатели на указатели может потребоваться, например для создания массива из массивов, и в частности массива из строк.
Язык C++ также позволяет работать с указателями на указатели на указатели, или сделать еще большую вложенность. Их можно объявлять просто увеличивая количество символов * (звездочек). Однако на практике такие указатели используются крайне редко.
Ссылки
Ссылка — это тип переменной в языке C++, который работает как псевдоним другого объекта или значения. При объявлении ссылки перед её именем ставится символ амперсанда &. Сама же ссылка не может быть пустой, и должна быть обязательно проинициализирована именем переменной, на которую она ссылается. Изменить значение ссылки после инициализации невозможно.
При создании ссылки на константную переменную, ссылка тоже должна быть создана как константная. Можно также создать константную ссылку на обычную переменную: в этом случае изменить переменную через ссылку не получится.
Любые действия со ссылкой трактуются компилятором как действия, которые будут выполняться над объектом, на который она ссылается.
Ссылки чаще всего используются в качестве параметров в функциях. В этом контексте ссылка-параметр работает как псевдоним аргумента, а сам аргумент не копируется при передаче в параметр. Это в свою очередь улучшает производительность, если аргумент слишком большой или затратный для копирования.
Также ссылка может использоваться и при возврате значений из функции, однако тут следует быть осторожным, так как, если переменная, на которую ссылается ссылка, выйдет из области видимости (например локальная переменная в функции), то это приведет к неопределенному поведению программы.
Ссылки r-value
В стандарте C++11 ввели новый тип ссылок — ссылки r-value. Ссылки r-value — это ссылки, которые инициализируются только значениями r-values. Объявляются такие ссылки, в отличие от обычных, с помощью двух символов амперсанда &&.
Ссылки r-value, в отличие от обычных ссылок, ссылаются не на постоянный, а на временный объект, созданный при инициализации ссылки r-value.
Такие ссылки обладают двумя важными свойствами:
- продолжительность жизни объекта, на который ссылается ссылка увеличивается до продолжительности жизни самой ссылки
- неконстантные ссылки r-value позволяют менять значение r-values, на который они ссылаются
Ссылки r-value — позволяют избегать логически ненужного копирования и обеспечивать возможность идеальной передачи (perfect forwarding). Прежде всего они предназначены для использования в высокопроизводительных проектах и библиотеках.