Указатель и ссылка в чем разница

Чем отличаются ссылки от указателей в С++

В чем принципиальное отличие ссылки от указателя в С++? Когда лучше использовать ссылку, а когда указатель? Какие ограничения есть у первых, а какие у вторых?

Указатель и ссылка в чем разница

Указатель и ссылка в чем разница

2 ответа 2

Еще отличия:

Указатель может иметь «невалидное» значение с которым его можно сравнить перед использованием.

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

(Standart) A null pointer constant is an integer literal (2.13.2) with value zero or a prvalue of type std::nullptr_t. A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of object pointer or function pointer type.

Ссылка не обладает квалификатором const

О весёлом

Некоторые ссылаются на отрывок с интервью с Страуструпом:

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

Другие задают в ответ лишь в один вопрос:

Чем является реультат разыменовывания указателя?

На тему, нужно ли знать отличия указателя от ссылки, писал Джоэл Спольски в своей статье «Закон Дырявых Абстракций».

Источник

Указатели, ссылки и массивы в C и C++: точки над i

В этом посте я постараюсь окончательно разобрать такие тонкие понятия в C и C++, как указатели, ссылки и массивы. В частности, я отвечу на вопрос, так являются массивы C указателями или нет.

Обозначения и предположения

Указатели и ссылки

Указатели. Что такое указатели, я рассказывать не буду. 🙂 Будем считать, что вы это знаете. Напомню лишь следующие вещи (все примеры кода предполагаются находящимися внутри какой-нибудь функции, например, main):

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

Если слева от знака присваивания стоит ссылка, то нет никакого способа понять, хотим мы присвоить самой ссылке или объекту, на который она ссылается. Поэтому такое присваивание всегда присваивает объекту, а не ссылке. Но это не относится к инициализации ссылки: инициализируется, разумеется, сама ссылка. Поэтому после инициализации ссылки нет никакого способа изменить её саму, т. е. ссылка всегда постоянна (но не её объект).

Удивительный факт состоит в том, что ссылки и lvalue — это в каком-то смысле одно и то же. Давайте порассуждаем. Что такое lvalue? Это нечто, чему можно присвоить. Т. е. это некое фиксированное место в памяти, куда можно что-то положить. Т. е. адрес. Т. е. указатель или ссылка (как мы уже знаем, указатели и ссылки — это два синтаксически разных способа в C++ выразить понятие адреса). Причём скорее ссылка, чем указатель, т. к. ссылку можно поместить слева от знака равенства и это будет означать присваивание объекту, на который указывает ссылка. Значит, lvalue — это ссылка.

А что такое ссылка? Это один из синтаксисов для адреса, т. е., опять-таки, чего-то, куда можно класть. И ссылку можно ставить слева от знака равенства. Значит, ссылка — это lvalue.

Окей, но ведь (почти любая) переменная тоже может быть слева от знака равенства. Значит, (такая) переменная — ссылка? Почти. Выражение, представляющее собой переменную — ссылка.

Этот принцип («выражение, являющееся переменной — ссылка») — моя выдумка. Т. е. ни в каком учебнике, стандарте и т. д. я этот принцип не видел. Тем не менее, он многое упрощает и его удобно считать верным. Если бы я реализовывал компилятор, я бы просто считал там переменные в выражениях ссылками, и, вполне возможно, именно так и предполагается в реальных компиляторах.

Более того, удобно считать, что особый тип данных для lvalue (т. е. ссылка) существует даже и в C. Именно так мы и будет дальше предполагать. Просто понятие ссылки нельзя выразить синтаксически в C, ссылку нельзя объявить.

Принцип «любое lvalue — ссылка» — тоже моя выдумка. А вот принцип «любая ссылка — lvalue» — вполне законный, общепризнанный принцип (разумеется, ссылка должна быть ссылкой на изменяемый объект, и этот объект должен допускать присваивание).

Операции * и &. Наши соглашения позволяют по-новому взглянуть на операции * и &. Теперь становится понятно следующее: операция * может применяться только к указателю (конкретно это было всегда известно) и она возвращает ссылку на тот же тип. & применяется всегда к ссылке и возвращает указатель того же типа. Таким образом, * и & превращают указатели и ссылки друг в друга. Т. е. по сути они вообще ничего не делают и лишь заменяют сущности одного синтаксиса на сущности другого! Таким образом, & вообще-то не совсем правильно называть операцией взятия адреса: она может быть применена лишь к уже существующему адресу, просто она меняет синтаксическое воплощение этого адреса.

Также замечу, что &*EXPR (здесь EXPR — это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR — указатель), а *&EXPR тоже эквивалентно EXPR всегда, когда имеет смысл (т. е. когда EXPR — ссылка).

Массивы

Итак, есть такой тип данных — массив. Определяются массивы, например, так:

Выражение в квадратных скобках должно быть непременно константой времени компиляции в C89 и C++98. При этом в квадратных скобках должно стоять число, пустые квадратные скобки не допускаются.

то, опять-таки, место для массива будет целиком выделяться прямо внутри структуры, и sizeof от этой структуры будет это подтверждать.

Хорошо, будем считать, я вас убедил, что массив — это именно массив, а не что-нибудь ещё. Откуда тогда берётся вся эта путаница между указателями и массивами? Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент.

Конвертирование имени массива в void * или применение к нему == тоже приводит к предварительному преобразованию этого имени в указатель на первый элемент, поэтому:

Типы у участвовавших выражений следующие:

Массив нельзя передать как аргумент в функцию. Если вы напишите int x[2] или int x[] в заголовке функции, то это будет эквивалентно int *x и в функцию всегда будет передаваться указатель (sizeof от переданной переменной будет таким, как у указателя). При этом размер массива, указанный в заголовке будет игнорироваться. Вы запросто можете указать в заголовке int x[2] и передать туда массив длины 3.

Однако, в C++ существует способ передать в функцию ссылку на массив:

При такой передаче вы всё равно передаёте лишь ссылку, а не массив, т. е. массив не копируется. Но всё же вы получаете несколько отличий по сравнению с обычной передачей указателя. Передаётся ссылка на массив. Вместо неё нельзя передать указатель. Нужно передать именно массив указанного размера. Внутри функции ссылка на массив будет вести себя именно как ссылка на массив, например, у неё будет sizeof как у массива.

И что самое интересное, эту передачу можно использовать так:

Похожим образом реализована функция std::end в C++11 для массивов.

«Указатель на массив». Строго говоря, «указатель на массив» — это именно указатель на массив и ничто другое. Иными словами:

Однако, иногда под фразой «указатель на массив» неформально понимают указатель на область памяти, в которой размещён массив, даже если тип у этого указателя неподходящий. В соответствии с таким неформальным пониманием c и d (и b + 0 ) — это указатели на массивы.

А теперь посмотрим на такую ситуацию:

Источник

Указатель и ссылка в чем разница

На языке C++ есть ссылки (reference), и есть указатели (pointer). В сущности ссылки являются синтаксическим «бантиком» над указателями, упрощающим чтение и написание кода. Однако чем реально различаются ссылки и указатели?

Если кратко, то вот отличия ссылок от указателей:

1. Указатель может быть переназначен любое количество раз, в то время как ссылка после привязки не может быть перемещена на другую ячейку памяти.

3. Вы не можете получить адрес ссылки, как можете это делать с указателями.

4. Не существует арифметики ссылок, в то время как существует арифметика указателей. Однако есть возможность получить адрес объекта, указанного по ссылке, и применить к этому адресу арифметику указателей (например &obj + 5 ).

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

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

Основные правила использования ссылок и указателей:

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

На языке C++ ссылки и указатели имеют перекрывающий друг друга функционал. Здесь приведена информация, которая поможет Вам принять решение, что лучше использовать для определенной задачи.

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

[Основы]

Декларация ссылки почти идентична декларации указателя, отличие только в том, что декларация ссылки использует оператор & вместо оператора *. Например, если:

декларирует pi как объект типа «указатель на int», у которого начальное значение будет адресом объекта i. В то время как:

декларирует ri как объект типа «ссылка на int», который ссылается на i. Инициализация ссылки для обращения к объекту часто описывают как «привязку ссылки к объекту».

Ключевое отличие между указателями и ссылками состоит в том, что нужно явно использовать оператор * для разыменования указателя (т. е. чтобы обратиться к объекту, на который он указывает), однако для такого же разыменования ссылки не нужно применять специальный оператор. Как только предыдущие определения были выполнены, выражение косвенной адресации *pi разыменовывает указатель pi, чтобы обратиться к переменной i. В отличие от этого выражение ri без каких-либо операторов сразу делает разыменование ссылки ri для обращения к переменной i. Таким образом, присвоение с указателем:

поменяет значение i на 4, и то же самое сделает присвоение с помощью ссылки:

Стандарт C++ не заморачивается с требованиями к компиляторам по поводу того, как генерировать код под обработку ссылок, так что все компиляторы работают со ссылками так же, как и с указателями. Таким образом, выделение памяти под хранение указателя будет таким же, как и для хранения ссылки. Также присвоение

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

Ни одно из этих вариантов присвоений не работают лучше другого, поведение одинаковое.

[Ссылки как параметры]

На языке C++ Вы можете декларировать параметры функции, у которых будет тип ссылок. Рассмотрим реализацию функции перестановки значений переменных (с именем swap), которая принимает два аргумента int и меняет значение своего первого аргумента на значение во втором аргументе. Например:

оставит значение, которое было в i, в переменной j, и значение, которое было в переменной j, оставит в переменной i.

Вот одна из возможных реализаций для этой функции:

Эта реализация проста и код понятен, но он работать не будет. Проблема языка C++, как и языка C, что он передает аргументы функции как значения. Таким образом, вызов:

сделает копию аргумента i в параметр v1, и копию аргумента j в параметр v2. Тело функции поменяет значение v1 на значение в переменной v2, но при возврате v1 и v2 будут уничтожены (обычно параметры функции передаются в стеке). Оригинальные значения переменных i и j останутся неизменными после вызова функции.

Чтобы перестановка работала, на языке C вы обязаны реализовать функцию с использованием в параметрах указателей, вот так:

Тогда она будет вызываться следующим образом:

Этот вызов будет передавать адрес переменной i вместо её копии. То же самое и для j. В коде тела функции *v1 обращается к i, и *v2 обращается к j, так что вызов сделает реальную перестановку значений переменных i и j.

На языке C++ Вы также можете использовать ссылки вместо указателей в параметрах функции, вот так:

В этом случае вызов будет выглядеть так:

В момент вызова параметр ссылки v1 будет привязан к аргументу i, и параметр ссылки v2 будет привязан к j. В теле функции swap, v1 обращается к i и v2 обращается к j, так что этот вызов также правильно сделает изменение значений i и j.

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

однозначно говорит нам о том, что в функцию передаются адреса переменных.

Хотя это утверждение для данного случая выглядит справедливым, C++ позволяет Вам написать функции, для которых Вы не захотите видеть & в вызовах. Это чаще случается, когда происходит работа с перегруженными операторами (overloaded operator), как показано в следующем примере, где вовлечены типы перечисления.

[Оператор перезагрузки и перечисления]

На языке C++, как и на языке C, типы перечислений (enum) предоставляют простой механизм для определения новых скалярных типов. К примеру предположим, что у Вас есть приложение, которое работает с днями недели и месяцами года. Вы можете определить тип day, представляющий дни недели, следующим образом:

После этого определения константа Sunday получит значение 0, Monday значение 1, и так далее. Позже в программе Вы можете написать код с циклом наподобие такого:

Этот код нормально скомпилируется в языке C, но не в C++. Компиляторы C++ пожалуются на выражение ++d в последнем операторе тела цикла.

На языке C каждый тип перечисления это просто целочисленный тип (int). Вы можете применять ++ или любой другой арифметический оператор, так что day это все равно что любое целое число. Но язык C++ рассматривает каждое перечисление как новый тип, отличающийся от целых чисел. Встроенные арифметические операторы C++ не применяются к перечислениям. Чтобы сохранить некоторую обратную совместимость с C, значения перечислений в C++ неявно преобразуются в целочисленные значения. Таким образом, на языке C++ Вы можете получить цикл, как в предыдущем примере путем изменения типа day объекта d на int:

Теперь присвоение d = Sunday конвертирует Sunday в 0, и присваивает его переменной d. Неравенство Saturday > d эффективно сравнивает d с 6.

Использование объектов int вместо объектов перечисления ослабляет возможности компилятора для обнаружения случайных (ошибочных) преобразований между разными типами перечисления. C++ предоставляет подход лучше этого. Вы можете сделать перезагрузку оператора ++ для типа day. Для такого решения Вы определяете функцию с именем operator++, которая принимает аргумент типа day. После этого, когда компилятор видит выражение ++d, он транслирует это выражение в вызов функции operator++(d).

Вот первая попытка определить такую функцию:

Для любого x арифметического типа или типа указателя, справедливо, что:

Для day d, выражение d + 1 преобразует d в int перед прибавлением 1 (которая тоже типа int). В результате получится int. Хотя C++ преобразует day в int, он не может преобразовать int в day без явного приведения типа (cast). Поэтому присвоение:

использует явное приведение типа, чтобы преобразовать результат сложения из int обратно в day перед тем, как присвоить его переменной d.

Как и в первой версии swap, эта первая версия operator++ не выполнит свою работу. Вызов operator++(d) передаст d по значению, поэтому в теле оператора будет модифицирована копия переменной d, а не сама переменная d.

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

Но с таким определением оператора нужно использовать выражения наподобие ++&d, что не выглядит по-настоящему правильным. Весь смысл перезагрузки оператора в том, чтобы код для обработки пользовательских типов выглядел точно так же, как и для встроенных. Но выражение ++&d выглядит несколько иначе, как если бы оператор ++ применялся для встроенного типа. В этом случае & в вызове оператора снижает ясность кода.

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

При использовании этой функции все выражения наподобие ++d будут не только выглядеть ожидаемо, но и будут правильно работать.

[Что внутри?]

Скорее всего, причина появления ссылок в C++ в том, чтобы позволить перезагрузку операторов для пользовательских типов. Чтобы перезагрузка выглядела и работала так же, как и операторы.

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

[Давайте копнем глубже]

Передача ссылки не просто лучший путь для написания operator++, это единственный путь. C++ реально не дает Вам другого выбора. Декларация наподобие:

не будет скомпилирована. Каждая перегруженная функция оператора должна быть либо членом класса, либо иметь параметр типа T, T & или T const &, где T это класс или перечисляемый тип. Другими словами, каждый перезагруженный оператор должен принимать в аргументе тип класса или перечисляемый тип. Указатель, даже если он указывает на объект класса или перечисляемого типа, не в счет. C++ не позволит Вам перегрузить операторы, которые меняют смысл операторов для встроенных типов, включая типы указателя. Таким образом, Вы не можете декларировать:

что делает попытку изменить смысл ++ для int, и также не получится декларировать:

что делает попытку переопределить ++ для int *.

[Отличие ссылок от указателей const]

В [5] объясняется, что C++ не позволяет декларировать «const reference», потому что ссылка по своей сути константа. Другими словами, как только Вы привязали ссылку к объекту, то больше не сможете перепривязать её к другому объекту. Нет синтаксиса изменения привязки, после того как Вы декларировали ссылку. Пример:

привяжет ri к переменной i. Тогда присвоение наподобие следующего:

не привяжет ri к j. Вместо этого значение в j попадет в объект, на который ссылается ri, т. е. в переменную i.

Короче говоря, тогда как указатель может указывать на разные объекты в течение своей жизни, ссылка может обращаться только к одному объекту в течение своей жизни. Некоторые утверждают, что это значимое различие между ссылками и указателями. Автор не разделяет эту идею. Может быть, что это различие между ссылками и указателями, но это не различие между ссылками и постоянными указателями. И снова, как только Вы сделали привязку ссылки к объекту, то уже не сможете поменять это, чтобы ссылаться на что-то другое. Поскольку Вы не можете поменять ссылку после её привязки, то должны выполнить эту привязку в начале жизни этой ссылки. Иначе ссылка никогда не будет привязана к чему-либо и будет бесполезной, если не реально опасной.

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

Пропуск инициализатора приведет ошибке компиляции:

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

Пропуск такого инициализатора также приведет к ошибке:

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

[NULL-ссылки]

Несмотря на все сказанное, постоянные указатели отличаются от ссылок одним тонким, но значительным моментом. Допустимая ссылка должна указывать на объект; указатель этого делать не обязан. Указатель, даже если он постоянный, может иметь нулевое значение (null). Просто нулевой указатель ни на что не указывает.

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

оставит в переменной j значение, которое было в i, и оставит в переменной i значение, которое было в j. Вы могли бы написать эту функцию так:

так что вызов этой функции будет выглядеть следующим образом:

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

Определение функции с параметрами в виде ссылок, не указателей:

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

[Больше безопасности?]

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

Если вдруг получится так, что указатель равен null в момент определения ссылки, то эта ссылка получится нулевой. Технически в привязке такой ссылки нет ошибки, но ошибка появится при разыменовании указателя null. Разыменование указателя (или ссылки), который равен null приведет к непредсказуемому поведению. Это означает что множество вещей может произойти, но большинство из этого не будет хорошим (спасутся не все). Вероятно, что когда программа привязывает ссылку r к *p (к объекту, на который указывает p), то она не может реально сделать разыменование p, чтобы понять, что тут дело нечисто. Вместо этого программа просто выполнит копию значения p в указатель, который реализует r. Программа продолжит работать до тех пор, пока ошибка не вылезет где-то совершенно неожиданным образом. И найти такую ошибку бывает очень непросто.

Следующая функция показывает еще один способ сделать недопустимую ссылку:

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

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

Как уже упоминалось в предыдущих врезках, ссылка это объект, который косвенно обращается к другому объекту. Ссылки предоставляют многие те же самые возможности, которые предоставляют указатели. Ключевое отличие между ссылками и указателями в том, как они появляются в коде, когда Вы их используете. В то время как Вы должны обязательно использовать специальный оператор, такой как * или [], чтобы разыменовать указатель, ничего подобного не нужно для разыменования ссылки. Ссылка разыменовывает сама себя, когда Вы её используете.

[Вернемся снова к деклараторам]

Каждая декларация языка C и C++ содержит две принципиальные части: последовательность из нулевого или большего количества спецификаторов декларации, и последовательности их одной или большего количества деклараторов, отделяемых друг от друга запятыми. Например:

Указатель и ссылка в чем разница

Операторы в группе декларатора обрабатываются с таким же приоритетом, с каким обрабатываются в выражении. Например, у оператора [] выше приоритет, чем у *. Таким образом, декларатор *x[N] означает, что x это «массив из N элементов, каждый из которых имеет тип указатель», а не «указатель на массив из N элементов».

Круглые скобки выполняют 2 роли в деклараторах: как оператор вызова функции и как группирующий элемент. Как оператор вызова функции, оператор () имеет тот же самый приоритет, что и оператор []. Как группирующий элемент, () превосходит все другие операторы. Например, в выражении:

круглые скобки вокруг списка параметров имеют более высокий приоритет, чем оператор *. Таким образом, здесь f декларируется просто как «функция, возвращающая указатель на char» вместо «указатель на функцию, возвращающую char». Если последнее то, что Вам нужно, то нужно написать так:

Оператор & имеет тот же приоритет, что и *. Таким образом:

декларирует g как «функция, возвращающая ссылку на char» вместо «ссылка на функцию, которая возвращает char». Если последнее именно то, что нужно, то следует переписать эту декларацию так:

[Немного о стиле написания кода]

Вероятно, что большинство программистов C++ пишут декларации ссылок таким образом, что оператор & прилегает к последнему спецификатору декларации, вместо того чтобы сделать оператор & частью декларатора. Например, они пишут декларации так:

Автор предпочитает приклеивать & к декларатору, вот так:

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

[Ссылка на const]

Спецификаторы декларации которые появляются перед декларатором, могут указывать тип, как например int, unsigned, или здесь может быть указан идентификатор имени типа. Они могут быть со спецификаторами класса памяти (storage class specifiers), такими как extern или static. Они также могут быть спецификаторами функции, такими как inline или virtual.

Когда ключевое слово const появляется как спецификатор декларации, оно является спецификатором типа. Например, const в декларации:

модифицирует int, тип объекта, на который ссылается ri. Здесь декларируется, что ri является «ссылкой на константу int», и ri ссылается на n.

Когда Вы используете ссылку ri в выражении, она ведет себя как объект типа «const int». Это означает, что Вы можете использовать ri для чтения, но не для модификации числа типа int, на которое ri ссылается. Например, следующие выражения приведут к ошибке компиляции:

Они не скомпилируются, потому что сделана попытка модифицировать объект, на который ссылается ri.

В этом частном примере ri ссылается на n. Хотя Вы не можете использовать ri для модификации n, но все еще можно модифицировать n в каком-нибудь другом выражении. Все зависит от того, как Вы декларируете n. Если n декларирована так:

то n конечно не модифицируемый объект, и Вы не можете изменить n каким-либо образом (кроме как путем использования выражения приведения типа, cast expression). С другой стороны, если переменная n декларирована так:

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

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

В общем, для любого типа T объект типа «ссылка на const T» может обращаться к объекту, который либо просто обычный объект типа T, либо объект типа «const T». В обоих случаях компилятор обрабатывает ссылку так, как если бы она обращалась к const-объекту. C++ обрабатывает «указатель на const T» точно таким же способом. Объект типа «указатель на const T» может указывать на объект, который как обычный объект типа T, так и как объект типа «const T». В любом случае, компилятор обрабатывает этот указатель, как если бы он указывал на const-объект [7].

[Снова поговорим о стиле]

Порядок, в котором спецификаторы декларации появляются в декларации, не имеет никакого значения для компилятора. Это еще одна вещь, которая запутывает синтаксис декларации C/C++ [8]. Поэтому, к примеру:

Многие программисты C++ предпочитают писать const в левой части, перед другими спецификаторами типа, как в (1). В статье [6] объясняется, почему автор считает, что правильнее будет писать const справа, как в (2). Второй способ предпочтительнее, потому что это помогает лучше понимать эффект от применения квалификатора const. Автор пишет декларации ссылки в том же стиле, что и декларации указателя, чтобы поддержать целостность стиля.

[Постоянные ссылки]

Как упоминалось выше, декларации указателя позволяют декларировать его либо как «указатель на const», либо как «const-указатель». Например:

декларирует p как объект типа «указатель на const int», в то время как:

декларирует q как объект типа «const-указатель на int». В последней декларации ключевое слово const появляется в деклараторе. В частности это часть модуля синтаксиса, который называется оператор указателя (ptr-operator). Этот ptr-operator может быть либо просто *, либо *, за которым сразу идет ключевое слово const.

Конечно, ptr-operator может быть также и оператором &, как в декларации:

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

Если коротко, когда декларируете ссылку, то она может быть «ссылкой на const», но Вы не можете декларировать её как «const-ссылку». Грамматика языка C++ просто не позволяет этого. Причина этого в том, что ссылка и так уже сама по себе константа. Как только Вы сделали привязку ссылки, чтобы она ссылалась на объект, то Вы уже не можете привязать её к другому объекту. Нет никакой нотации для перепривязки ссылки, после того, как она была декларирована. Например:

делает привязку ri, чтобы она ссылалась на i. Тогда присваивание, такое как:

не делает привязку ri к j. Это присваивает значение в j объекту, на который ссылается ri, т. е. значение будет присвоено переменной i.

Как только Вы определили ссылку, то больше не можете поменять это, чтобы обращаться к какому-то другому объекту. Из-за того, что Вы не можете поменять ссылку после того, как определили её, то Вы обязаны сделать привязку ссылки к объекту в момент начала жизни ссылки в коде. Например, декларация ссылки в блоке кода обязательно должна иметь свой инициализатор:

Пропуск инициализатора приведет к ошибке:

Хотя Вы не можете определить напрямую «const-ссылку», Вы можете сделать это косвенно через typedef. Например,

определяет r как «const int_ref». Поскольку int_ref это просто алиас (псевдоним) для «ссылки на int», тип r появляется как «const-ссылка на int». Но ссылка сама по себе уже изначально константа, так что ключевое слово const здесь избыточно, и не дает эффекта. Компилятор C++ просто игнорирует const в этой декларации, так что r получит тип «ссылка на int».

[Немного о терминологии]

В то время как автор пишет декларации так:

многие программисты C++ написали бы так:

Автор не может примириться с этим. Еще больше беспокоит то, что многие программисты также назвали бы ri «const-ссылкой». Хотя на самом деле это «ссылка на const». Важно то, что в то время как нет никакой причины разделять «ссылку на const» и «const-ссылку» (поскольку последнего не существует в природе), все еще важно понимать разницу «указателя на const» и «const-указателя». «Указатель на const» совсем не то же самое, что «const-указатель», разница существенная.

Проблема в том, что многие программисты, которые говорят «const-ссылка», не имеют в виду ничего плохого и подразумевают просто «ссылку на const», но они допускают при этом неаккуратность. Скорее всего они сделают ошибку и скажут подобным образом «const-указатель», имея в виду «указатель на const». На самом деле, многие используют термин «const-указатель» для обозначения либо «const-указателя», либо «указателя на const». Поди разберись.

Так что лучше избегать термина «const-ссылка», когда имеете в виду «ссылка на const».

Источник

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

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