Ссылки
Ссылка reference — механизм языка программирования (C++), позволяющий привязать имя к значению. В частности, ссылка позволяет дать дополнительное имя переменной и передавать в функции сами переменные, а не значения переменных.
Синтаксически ссылка оформляется добавлением знака & (амперсанд) после имени типа. Ссылка на ссылку невозможна.
Ссылка требует инициализации. В момент инициализации происходит привязка ссылки к тому, что указано справа от = . После инициализации ссылку нельзя “отвязать” или “перепривязать”.
Любые действия со ссылкой трактуются компилятором как действия, которые будут выполняться над объектом, к которому эта ссылка привязана. Следующий пример демонстрирует ссылку в качестве дополнительного имени переменной.
Казалось бы, зачем нам второе имя переменной? Ответа может быть, по крайней мере, два.
- Что-то имеет слишком длинное, неудобное название. Привязав к нему ссылку, мы получим более удобное, короткое локальное название. При этом мы можем не указывать тип этого “чего-то”, можно использовать вместо типа ключевое слово auto :
- Выбор объекта привязки ссылки может происходить во время исполнения программы и зависеть от некоего условия. Пример:
Впрочем, основным применением ссылок является передача параметров в функции “по ссылке” и возвращение функциями ссылок на некие внешние объекты.
Передача по ссылке by reference напоминает передачу “по имени”. Таким образом, можно сказать, что, используя ссылки, мы передаём не значения, а сами переменные, содержащие эти значения. В реальности “за ширмой” происходит передача адресов этих переменных. Передача ссылки на переменную, время жизни которой заканчивается, например, возврат из функции ссылки на локальную переменную, приводит к неопределённому поведению.
Ранний пример использования ссылок для возврата из функции более одного значения представлен в самостоятельной работе 3.
Приведём здесь ещё один пример: функцию, которая возвращает одну из двух переменных, содержащую максимальное значение. Для этого модифицируем предыдущий пример:
Так как при передаче ссылки реально копируется лишь адрес значения, а не само значение, то передав ссылку можно избежать копирования значения. Поэтому ссылки широко используются для передачи в функцию аргументов, которые или запрещено копировать или вычислительно дорого копировать. Типичный пример — объекты string. При копировании строки происходит выделение динамической памяти, копирование всех символов, затем — при удалении этой копии — освобождение памяти. Часто нет никакой необходимости в копировании. Например, следующей функции, считающей количество повторений заданного символа в строке нет нужды копировать строку — можно обойтись ссылкой:
Обратите внимание на ключевое слово const . Данное ключевое слово позволяет нам указать, что мы хотим ссылку на константу, т.е. функция char_freq использует s как константу и не пытается её изменять, а ссылка нужна для того, чтобы избежать копирования. Рекомендуется использовать const везде, где достаточно константы. Компилятор проверит, действительно ли мы соблюдаем константность.
Ставить слово const можно перед именем типа и после имени типа, это эквивалентные записи.
Указатели
Общие сведения
Что такое указатель pointer уже рассказывалось во введении.
В C и C++ указатель определяется с помощью символа * после типа данных, на которые этот указатель будет указывать.
Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.
Указатели — простые переменные. Указатели не “делают вид”, что они — те значения в памяти, к которым они привязаны. Чтобы получить указатель на переменную, нужно явно взять её адрес с помощью оператора & . Чтобы обратиться к переменной, на которую указывает указатель, требуется явно разыменовать его с помощью оператора * .
Так же, как и в случае ссылок, можно использовать ключевое слово const , чтобы создать указатель на константу.
Указатели можно сравнивать друг с другом. Указатели равны, если указывают на один и тот же объект, и не равны в противном случае.
Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:
Для обращения к полю структуры по указателю на объект структуры предусмотрен специальный оператор -> (“стрелка”).
В отличие от ссылок, указатели не обязательно инициализировать. Указатели можно инициализировать специальным значением нулевой указатель nullptr , которое сигнализирует об отсутствии привязки указателя к чему-либо. Присваивание указателю другого адреса меняет его привязку. Это позволяет использовать указатели там, где семантика ссылок слишком сильно ограничивает наши возможности.
Соответственно, ограничения, накладываемые на ссылки по сравнению с указателями, позволяют, с одной стороны, защитить программиста от ряда ошибок, и, с другой стороны, открывают ряд возможностей оптимизации кода для компилятора. Ссылки используются там, где нет нужды в “полноценных” указателях или есть желание не перегружать код взятиями адреса и разыменованиями.
Наличие нулевого указателя позволяет, например, возвращать указатель на искомый объект и в том случае, когда ничего не было найдено. Просто в этой ситуации возвращаем нулевой указатель, а принимающая сторона должна быть готова к такому развитию событий. Указатель автоматически преобразуется к булевскому значению: нулевой указатель даёт false , прочие указатели дают true , поэтому, если p — указатель, то
есть то же самое, что
есть то же самое, что
Например, поиск самого левого нуля в массиве чисел с плавающей точкой может быть записан так:
Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе массивы и ссылки.
Бестиповый указатель
Вместо типа данных при объявлении указателя можно поставить ключевое слово void . Данное ключевое слово означает, что мы описываем указатель “на что угодно”, т. е. просто адрес в памяти. Любой указатель автоматически приводится к типу void* — бестиповому указателю typeless pointer . Прочие указатели, соответственно, называются типизированными или типизованными typed . Приведение от void* к типизованному указателю возможно с помощью оператора явного приведения типа.
В C бестиповые указатели широко применяются для оперирования кусками памяти или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).
О цикле for (int byte: buffer) см. здесь.
Указатель на указатель
Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.
“Система ранжирования C-программистов.
Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.
Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.
Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент."
Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.
В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:
Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.
В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.
Неограниченный уровень косвенности
Несмотря на ограниченность применения уровня косвенности выше двух, довольно часто встречается то, что можно назвать неограниченным уровнем косвенности или рекурсивным типом данных. Типичный (и простейший) пример — структура данных, называемая “связанный список” linked list .
Следующий пример демонстрирует использование связанного списка для чтения последовательности строк и вывода этой последовательности в обратном порядке:
Упражнение. Попробуйте изменить этот пример так, чтобы введённые строки выводились в том же порядке, в котором были введены.
Указатели на функции
Язык C позволяет определять указатели на функции (в указателе хранится адрес точки входа в функцию) и вызывать функции по указателю. Таким образом, можно во время исполнения программы выбирать какая именно функция будет вызвана в конкретной точке, выбирая значение указателя. Язык C++ позволяет создавать также и ссылки на функции, но ввиду того, что ссылка после инициализации не может быть изменена, область применения ссылок на функции весьма узка.
Функцией высшего порядка higher order function называют функцию, принимающую в качестве параметров другие функции. Функции высшего порядка — одно из базовых понятий функционального программирования. Единственная форма функций высшего порядка в C — функции, принимающие указатели на функции. Язык C++ расширяет круг доступных форм функций высшего порядка, но в примерах ниже мы ограничимся возможностями C.
Простой пример использования указателя на функцию — функция, решающая уравнение вида f(x) = 0, где f(x) — произвольная функция. Конкретные функции f можно передавать по указателю. Приведение функций к указателю на функцию и наоборот производится неявно автоматически, поэтому при присваивании указателю адреса конкретной функции можно не использовать оператор взятия адреса & , а при вызове функции по указателю — не использовать оператор разыменования * (поведение, аналогичное поведению с массивами).
Довольно типичной областью применения указателей на функции является связывание источников (регистраторов) некоторых событий, обычно определяемых в составе некоторой библиотеки, и обработчиков событий, предоставляемых пользователем этой библиотеки. Обработчики событий (функции) вызываются автоматически по переданным указателям. Такие функции также называются функциями обратного вызова callback functions или колбеками callbacks . Например, при щелчке мышью по элементу графического интерфейса вызывается функция-обработчик этого события, “зарегистрированная”, путём передачи её адреса библиотеке графического интерфейса.
В качестве простого примера применения функции обратного вызова рассмотрим функцию, занимающуюся поиском набора корней уравнения f(x) = 0 на заданном отрезке. Сама функция будет работать по достаточно простому алгоритму (который, естественно, не гарантирует, что будут найдены все или даже какие-то из существующих на отрезке корней): предполагаем, что есть некая функция, способная найти один корень на отрезке, если он там есть (например, функция nsolve из примера выше). Теперь берём исходный отрезок поиска [a, b] и некоторое значение “шага” step и проходим по этому отрезку с этим шагом, проверяя участки [a + i step, min(b, a + (i + 1)step], i = 0, … пока не пересечём правую границу отрезка. На каждом участке проверяем, являются ли его границы корнями, и есть ли на нём корень (принимает ли функция f разнознаковые значения на границах). В последнем случае используем “решатель” вроде nsolve (переданный по указателю), чтобы найти корень. Каждый найденный корень — это событие, вызываем для него “обработчик” — функцию обратного вызова по указателю report.
Следующий пример демонстрирует “двухзвёздное программирование” и использование указателя на функцию для определения порядка сортировки массива строк с помощью стандартной функции qsort .
Функция qsort является частью Стандартной библиотеки C. Стандартная библиотека C++ предлагает более удобную и эффективную функцию sort (определённую в заголовочном файле <algorithm> ), однако её рассмотрение выходит за пределы темы данного раздела.
Следующий пример является развитием примера со списком из предыдущего подраздела и использует бестиповые указатели, указатели на указатели и указатели на функции для управления “обобщённым” связанным списком в стиле C. Звенья такого списка могут содержать произвольные данные. Основное требование к звеньям списка — наличие в начале звена указателя на следующее звено, фактически каждый предыдущий указатель указывает на следующий.
Теперь сама программа, выводящая строки в обратном порядке, упрощается:
Впрочем, необходимо отметить, что сочетая такие приёмы со средствами C++, выходящими за пределы “чистого” C, вы рискуете нарваться на неопределённое поведение. Низкоуровневые средства требуют особой внимательности, так как компилятор в таких случаях не страхует программиста. В частности, в общем случае нельзя интерпретировать произвольный указатель как void* и наоборот без выполнения приведения типа. А это может произойти неявно, например, в примере выше мы полагаем, что указатель prev, указывающий на объект структуры Line совпадает с указателем на поле prev этого объекта.
Синтаксическая справка
Правило чтения сложных описаний типов
Конструкции, определяющие переменные или вводящие новые типы в языках C и C++, могут порой иметь довольно запутанный вид. Ниже дано правило, помогающее разобраться в смысле сложных конструкций.
- Начиная с имени (в случае typedef , в случае using имя находится вне — см. ниже), читать вправо, пока это возможно (до закрывающей круглой скобки или точки с запятой).
- Пока невозможно читать вправо, читать влево (убирая скобки).
Некоторые примеры “расшифровки” типов переменных:
Разница между typedef и using
Директива typedef объявляет синоним типа. Используется синтаксис определения переменной, к которой добавили ключевое слово typedef , только вместо собственно переменной вводится синоним типа этой как-бы переменной с её именем.
В С++11 появилась возможность объявлять синонимы типов с помощью using-директивы в стиле инициализации переменных:
Объявление typedef можно превратить в using-директиву, заменив typedef на using , вставив после using имя типа и знак равно и убрав это имя типа из объявления справа.
Указатели, ссылки и массивы в C и C++: точки над i
В этом посте я постараюсь окончательно разобрать такие тонкие понятия в C и C++, как указатели, ссылки и массивы. В частности, я отвечу на вопрос, так являются массивы C указателями или нет.
Обозначения и предположения
- Я буду предполагать, что читатель понимает, что, например, в C++ есть ссылки, а в C — нет, поэтому я не буду постоянно напоминать, о каком именно языке (C/C++ или именно C++) я сейчас говорю, читатель поймёт это из контекста;
- Также, я предполагаю, что читатель уже знает C и C++ на базовом уровне и знает, к примеру, синтаксис объявления ссылки. В этом посте я буду заниматься именно дотошным разбором мелочей;
- Буду обозначать типы так, как выглядело бы объявление переменной TYPE соответствующего типа. Например, тип «массив длины 2 int’ов» я буду обозначать как int TYPE[2] ;
- Я буду предполагать, что мы в основном имеем дело с обычными типами данных, такими как int TYPE , int *TYPE и т. д., для которых операции =, &, * и другие не переопределены и обозначают обычные вещи;
- «Объект» всегда будет означать «всё, что не ссылка», а не «экземпляр класса»;
- Везде, за исключением специально оговоренных случаев, подразумеваются C89 и C++98.
Указатели и ссылки
Указатели. Что такое указатели, я рассказывать не буду. 🙂 Будем считать, что вы это знаете. Напомню лишь следующие вещи (все примеры кода предполагаются находящимися внутри какой-нибудь функции, например, main):
Также напомню следующее: char — это всегда ровно один байт и во всех стандартах C и C++ sizeof (char) == 1 (но при этом стандарты не гарантируют, что в байте содержится именно 8 бит :)). Далее, если прибавить к указателю на какой-нибудь тип T число, то реальное численное значение этого указателя увеличится на это число, умноженное на sizeof (T) . Т. е. если p имеет тип T *TYPE , то p + 3 эквивалентно (T *)((char *)p + 3 * sizeof (T)) . Аналогичные соображения относятся и к вычитанию.
Ссылки. Теперь по поводу ссылок. Ссылки — это то же самое, что и указатели, но с другим синтаксисом и некоторыми другими важными отличиями, о которых речь пойдёт дальше. Следующий код ничем не отличается от предыдущего, за исключением того, что в нём фигурируют ссылки вместо указателей:
Если слева от знака присваивания стоит ссылка, то нет никакого способа понять, хотим мы присвоить самой ссылке или объекту, на который она ссылается. Поэтому такое присваивание всегда присваивает объекту, а не ссылке. Но это не относится к инициализации ссылки: инициализируется, разумеется, сама ссылка. Поэтому после инициализации ссылки нет никакого способа изменить её саму, т. е. ссылка всегда постоянна (но не её объект).
Lvalue. Те выражения, которым можно присваивать, называются lvalue в C, C++ и многих других языках (это сокращение от «left value», т. е. слева от знака равенства). Остальные выражения называются rvalue. Имена переменных очевидным образом являются lvalue, но не только они. Выражения a[i + 2] , some_struct.some_field , *ptr , *(ptr + 3) — тоже lvalue.
Удивительный факт состоит в том, что ссылки и lvalue — это в каком-то смысле одно и то же. Давайте порассуждаем. Что такое lvalue? Это нечто, чему можно присвоить. Т. е. это некое фиксированное место в памяти, куда можно что-то положить. Т. е. адрес. Т. е. указатель или ссылка (как мы уже знаем, указатели и ссылки — это два синтаксически разных способа в C++ выразить понятие адреса). Причём скорее ссылка, чем указатель, т. к. ссылку можно поместить слева от знака равенства и это будет означать присваивание объекту, на который указывает ссылка. Значит, lvalue — это ссылка.
А что такое ссылка? Это один из синтаксисов для адреса, т. е., опять-таки, чего-то, куда можно класть. И ссылку можно ставить слева от знака равенства. Значит, ссылка — это lvalue.
Окей, но ведь (почти любая) переменная тоже может быть слева от знака равенства. Значит, (такая) переменная — ссылка? Почти. Выражение, представляющее собой переменную — ссылка.
Иными словами, допустим, мы объявили int x . Теперь x — это переменная типа int TYPE и никакого другого. Это int и всё тут. Но если я теперь пишу x + 2 или x = 3 , то в этих выражениях подвыражение x имеет тип int &TYPE . Потому что иначе этот x ничем не отличался бы от, скажем, 10, и ему (как и десятке) нельзя было бы ничего присвоить.
Этот принцип («выражение, являющееся переменной — ссылка») — моя выдумка. Т. е. ни в каком учебнике, стандарте и т. д. я этот принцип не видел. Тем не менее, он многое упрощает и его удобно считать верным. Если бы я реализовывал компилятор, я бы просто считал там переменные в выражениях ссылками, и, вполне возможно, именно так и предполагается в реальных компиляторах.
Более того, удобно считать, что особый тип данных для lvalue (т. е. ссылка) существует даже и в C. Именно так мы и будет дальше предполагать. Просто понятие ссылки нельзя выразить синтаксически в C, ссылку нельзя объявить.
Принцип «любое lvalue — ссылка» — тоже моя выдумка. А вот принцип «любая ссылка — lvalue» — вполне законный, общепризнанный принцип (разумеется, ссылка должна быть ссылкой на изменяемый объект, и этот объект должен допускать присваивание).
Теперь, с учётом наших соглашений, сформулируем строго правила работы со ссылками: если объявлено, скажем, int x , то теперь выражение x имеет тип int &TYPE . Если теперь это выражение (или любое другое выражение типа ссылка) стоит слева от знака равенства, то оно используется именно как ссылка, практически во всех остальных случаях (например, в ситуации x + 2 ) x автоматически конвертируется в тип int TYPE (ещё одной операцией, рядом с которой ссылка не конвертируется в свой объект, является &, как мы увидим далее). Слева от знака равенства может стоять только ссылка. Инициализировать (неконстантную) ссылку может только ссылка.
Операции * и &. Наши соглашения позволяют по-новому взглянуть на операции * и &. Теперь становится понятно следующее: операция * может применяться только к указателю (конкретно это было всегда известно) и она возвращает ссылку на тот же тип. & применяется всегда к ссылке и возвращает указатель того же типа. Таким образом, * и & превращают указатели и ссылки друг в друга. Т. е. по сути они вообще ничего не делают и лишь заменяют сущности одного синтаксиса на сущности другого! Таким образом, & вообще-то не совсем правильно называть операцией взятия адреса: она может быть применена лишь к уже существующему адресу, просто она меняет синтаксическое воплощение этого адреса.
Замечу, что указатели и ссылки объявляются как int *x и int &x . Таким образом, принцип «объявление подсказывает использование» лишний раз подтверждается: объявление указателя напоминает, как превратить его в ссылку, а объявление ссылки — наоборот.
Также замечу, что &*EXPR (здесь EXPR — это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR — указатель), а *&EXPR тоже эквивалентно EXPR всегда, когда имеет смысл (т. е. когда EXPR — ссылка).
Массивы
Итак, есть такой тип данных — массив. Определяются массивы, например, так:
Выражение в квадратных скобках должно быть непременно константой времени компиляции в C89 и C++98. При этом в квадратных скобках должно стоять число, пустые квадратные скобки не допускаются.
Подобно тому, как все локальные переменные (напомню, мы предполагаем, что все примеры кода находятся внутри функций) находятся на стеке, массивы тоже находятся на стеке. Т. е. приведённый код привёл к выделению прямо на стеке огромного блока памяти размером 5 * sizeof (int) , в котором целиком размещается наш массив. Не нужно думать, что этот код объявил некий указатель, который указывает на память, размещённую где-то там далеко, в куче. Нет, мы объявили массив, самый настоящий. Здесь, на стеке.
Чему будет равно sizeof (x) ? Разумеется, оно будет равно размеру нашего массива, т. е. 5 * sizeof (int) . Если мы пишем
то, опять-таки, место для массива будет целиком выделяться прямо внутри структуры, и sizeof от этой структуры будет это подтверждать.
От массива можно взять адрес ( &x ), и это будет самый настоящий указатель на то место, где этот массив расположен. Тип у выражения &x , как легко понять, будет int (*TYPE)[5] . В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &x и &(x[0]) численно равны (тут я лихо написал выражение &(x[0]) , на самом деле в нём не всё так просто, к этому мы ещё вернёмся). Но эти выражения имеют разный тип — int (*TYPE)[5] и int *TYPE , поэтому сравнить их при помощи == не получится. Но можно применить трюк с void * : следующее выражение будет истинным: (void *)&x == (void *)&(x[0]) .
Хорошо, будем считать, я вас убедил, что массив — это именно массив, а не что-нибудь ещё. Откуда тогда берётся вся эта путаница между указателями и массивами? Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент.
Итак, мы объявили int x[5] . Если мы теперь пишем x + 0 , то это преобразует наш x (который имел тип int TYPE[5] , или, более точно, int (&TYPE)[5] ) в &(x[0]) , т. е. в указатель на нулевой элемент массива x. Теперь наш x имеет тип int *TYPE .
Конвертирование имени массива в void * или применение к нему == тоже приводит к предварительному преобразованию этого имени в указатель на первый элемент, поэтому:
- x[2] эквивалентно *(x + 2)
- x + 2 относится к тем операциям, при которых имя массива преобразуется в указатель на его первый элемент, поэтому это происходит
- Далее, в соответствии с моими объяснениями выше, x + 2 эквивалентно (int *)((char *)x + 2 * sizeof (int)) , т. е. x + 2 означает «сдвинуть указатель x на два int’а»
- Наконец, от результата берётся операция разыменования и мы извлекаем тот объект, который размещён по этому сдвинутому указателю
Типы у участвовавших выражений следующие:
Также замечу, что слева от квадратных скобок необязательно должен стоять именно массив, там может быть любой указатель. Например, можно написать (x + 2)[3] , и это будет эквивалентно x[5] . Ещё замечу, что *a и a[0] всегда эквивалентны, как в случае, когда a — массив, так и когда a — указатель.
Теперь, как я и обещал, я возвращаюсь к &(x[0]) . Теперь ясно, что в этом выражении сперва x преобразуется в указатель, затем к этому указателю в соответствии с вышеприведённым алгоритмом применяется [0] и в результате получается значение типа int &TYPE , и наконец, при помощи & оно преобразуется к типу int *TYPE . Поэтому, объяснять при помощи этого сложного выражения (внутри которого уже выполняется преобразование массива к указателю) немного более простое понятие преобразования массива к указателю — это был немного мухлёж.
А теперь вопрос на засыпку: что такое &x + 1 ? Что ж, &x — это указатель на весь массив целиком, + 1 приводит к шагу на весь этот массив. Т. е. &x + 1 — это (int (*)[5])((char *)&x + sizeof (int [5])) , т. е. (int (*)[5])((char *)&x + 5 * sizeof (int)) (здесь int (*)[5] — это int (*TYPE)[5] ). Итак, &x + 1 численно равно x + 5 , а не x + 1 , как можно было бы подумать. Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива. Также, заметим, что выражение *(&x + 1) == x + 5 истинно. Ещё его можно записать вот так: (&x)[1] == x + 5 . Также будет истинным *((&x)[1]) == x[5] , или, что тоже самое, (&x)[1][0] == x[5] (если мы, конечно, не схватим segmentation fault за попытку обращения за пределы нашей памяти :)).
Массив нельзя передать как аргумент в функцию. Если вы напишите int x[2] или int x[] в заголовке функции, то это будет эквивалентно int *x и в функцию всегда будет передаваться указатель (sizeof от переданной переменной будет таким, как у указателя). При этом размер массива, указанный в заголовке будет игнорироваться. Вы запросто можете указать в заголовке int x[2] и передать туда массив длины 3.
Однако, в C++ существует способ передать в функцию ссылку на массив:
При такой передаче вы всё равно передаёте лишь ссылку, а не массив, т. е. массив не копируется. Но всё же вы получаете несколько отличий по сравнению с обычной передачей указателя. Передаётся ссылка на массив. Вместо неё нельзя передать указатель. Нужно передать именно массив указанного размера. Внутри функции ссылка на массив будет вести себя именно как ссылка на массив, например, у неё будет sizeof как у массива.
И что самое интересное, эту передачу можно использовать так:
Похожим образом реализована функция std::end в C++11 для массивов.
«Указатель на массив». Строго говоря, «указатель на массив» — это именно указатель на массив и ничто другое. Иными словами:
Однако, иногда под фразой «указатель на массив» неформально понимают указатель на область памяти, в которой размещён массив, даже если тип у этого указателя неподходящий. В соответствии с таким неформальным пониманием c и d (и b + 0 ) — это указатели на массивы.
Многомерные массивы. Если объявлено int x[5][7] , то x — это не массив длины 5 неких указателей, указывающих куда-то далеко. Нет, x теперь — это единый монолитный блок размером 5 x 7, размещённый на стеке. sizeof (x) равен 5 * 7 * sizeof (int) . Элементы располагаются в памяти так: x[0][0] , x[0][1] , x[0][2] , x[0][3] , x[0][4] , x[0][5] , x[0][6] , x[1][0] и так далее. Когда мы пишем x[0][0] , события развиваются так:
То же самое относится к **x . Замечу, что в выражениях, скажем, x[0][0] + 3 и **x + 3 в реальности извлечение из памяти происходит только один раз (несмотря на наличие двух звёздочек), в момент преобразования окончательной ссылки типа int &TYPE просто в int TYPE . Т. е. если бы мы взглянули на ассемблерный код, который генерируется из выражения **x + 3 , мы бы в нём увидели, что операция извлечения данных из памяти выполняется там только один раз. **x + 3 можно ещё по-другому записать как *(int *)x + 3 .
А теперь посмотрим на такую ситуацию:
Что теперь есть y? y — это указатель на массив (в неформальном смысле!) указателей на массивы (опять-таки, в неформальном смысле). Нигде здесь не появляется единый блок размера 5 x 7, есть 5 блоков размера 7 * sizeof (int) , которые могут находиться далеко друг от друга. Что есть y[0][0] ?
Теперь, когда мы пишем y[0][0] + 3 , извлечение из памяти происходит два раза: извлечение из массива y и последующее извлечение из массива y[0] , который может находиться далеко от массива y. Причина этого в том, что здесь не происходит преобразования имени массива в указатель на его первый элемент, в отличие от примера с многомерным массивом x. Поэтому **y + 3 здесь не эквивалентен *(int *)y + 3 .
Объясню ещё разок. x[2][3] эквивалентно *(*(x + 2) + 3) . И y[2][3] эквивалентно *(*(y + 2) + 3) . Но в первом случае наша задача найти «третий элемент во втором ряду» в едином блоке размера 5 x 7 (разумеется, элементы нумеруются с нуля, поэтому этот третий элемент будет в некотором смысле четвёртым :)). Компилятор вычисляет, что на самом деле нужный элемент находится на 2 * 7 + 3 -м месте в этом блоке и извлекает его. Т. е. x[2][3] здесь эквивалентно ((int *)x)[2 * 7 + 3] , или, что то же самое, *((int *)x + 2 * 7 + 3) . Во втором случае сперва извлекает 2-й элемент в массиве y, а затем 3-й элемент в полученном массиве.
В первом случае, когда мы делаем x + 2 , мы сдвигаемся сразу на 2 * sizeof (int [7]) , т. е. на 2 * 7 * sizeof (int) . Во втором случае, y + 2 — это сдвиг на 2 * sizeof (int *) .
В первом случае (void *)x и (void *)*x (и (void *)&x !) — это один и тот же указатель, во втором — это не так.
Урок №88. Ссылки
До этого момента мы успели рассмотреть 2 основных типа переменных:
обычные переменные, которые хранят значения напрямую;
указатели, которые хранят адрес другого значения (или null), для доступа к которым выполняется операция разыменования указателя.
Ссылки — это третий базовый тип переменных в языке C++.
Ссылки
Ссылка — это тип переменной в языке C++, который работает как псевдоним другого объекта или значения. Язык C++ поддерживает 3 типа ссылок:
Ссылки на неконстантные значения (обычно их называют просто «ссылки» или «неконстантные ссылки»), которые мы обсудим на этом уроке.
Ссылки на константные значения (обычно их называют «константные ссылки»), которые мы обсудим на следующем уроке.
В C++11 добавлены ссылки r-value, о которых мы поговорим чуть позже.
Ссылка (на неконстантное значение) объявляется с использованием амперсанда ( & ) между типом данных и именем ссылки:
В этом контексте амперсанд не означает «оператор адреса», он означает «ссылка на».
Ссылки в качестве псевдонимов
Ссылки обычно ведут себя идентично значениям, на которые они ссылаются. В этом смысле ссылка работает как псевдоним объекта, на который она ссылается, например:
Результат выполнения программы:
В примере, приведенном выше, объекты ref и value обрабатываются как одно целое. Использование оператора адреса с ссылкой приведет к возврату адреса значения, на которое ссылается ссылка:
Краткий обзор l-value и r-value
На уроке №10 мы уже рассматривали, что такое l-value и r-value. l-value — это объект, который имеет определенный адрес памяти (например, переменная x ) и сохраняется за пределами одного выражения. r-value — это временное значение без определенного адреса памяти и с областью видимости выражения (т.е. сохраняется в пределах одного выражения). В качестве r-values могут быть как результаты выражения (например, 2 + 3 ), так и литералы.
Инициализация ссылок
Ссылки должны быть инициализированы при создании:
В отличие от указателей, которые могут содержать нулевое значение, ссылки нулевыми быть не могут.
Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-values. Они не могут быть инициализированы константными l-values или r-values:
Обратите внимание, во втором случае вы не можете инициализировать неконстантную ссылку константным объектом. В противном случае, вы бы могли изменить значение константного объекта через ссылку, что уже является нарушением понятия «константа».
После инициализации изменить объект, на который указывает ссылка — нельзя. Рассмотрим следующий фрагмент кода:
Обратите внимание, во втором стейтменте ( ref = value2; ) выполняется не то, что вы могли бы ожидать! Вместо переприсваивания ref (ссылаться на переменную value2 ), значение из value2 присваивается переменной value1 (на которое и ссылается ref ).
Ссылки в качестве параметров в функциях
Ссылки чаще всего используются в качестве параметров в функциях. В этом контексте ссылка-параметр работает как псевдоним аргумента, а сам аргумент не копируется при передаче в параметр. Это в свою очередь улучшает производительность, если аргумент слишком большой или затратный для копирования.
На уроке №82 мы говорили о том, что передача аргумента-указателя в функцию позволяет функции при разыменовании этого указателя напрямую изменять значение аргумента.
Ссылки работают аналогично. Поскольку ссылка-параметр — это псевдоним аргумента, то функция, использующая ссылку-параметр, может изменять аргумент, переданный ей, также напрямую:
Результат выполнения программы:
Когда аргумент x передается в функцию, то параметр функции ref становится ссылкой на аргумент x . Это позволяет функции изменять значение x непосредственно через ref ! Обратите внимание, переменная x не обязательно должна быть ссылкой.
Совет: Передавайте аргументы в функцию через неконстантные ссылки-параметры, если они должны быть изменены функцией в дальнейшем.
Основным недостатком использования неконстантных ссылок в качестве параметров в функциях является то, что аргумент должен быть неконстантным l-value (т.е. константой или литералом он быть не может). Мы поговорим об этом подробнее (и о том, как это обойти) на следующем уроке.
Ссылки как более легкий способ доступа к данным
Второе (гораздо менее используемое) применение ссылок заключается в более легком способе доступа к вложенным данным. Рассмотрим следующую структуру:
Предположим, что нам нужно работать с полем value1 структуры Something переменной other структуры Other (звучит сложно, но такое также встречается на практике). Обычно, доступ к этому полю осуществлялся бы через other.something.value1 . А что, если нам нужно неоднократно получать доступ к этому члену? В этом случае код становится громоздким и беспорядочным. Ссылки же предоставляют более легкий способ доступа к данным:
Таким образом, следующие два стейтмента идентичны:
Ссылки позволяют сделать ваш код более чистым и понятным.
Ссылки vs. Указатели
Ссылка — это тот же указатель, который неявно разыменовывается при доступе к значению, на которое он указывает («под капотом» ссылки реализованы с помощью указателей). Таким образом, в следующем коде:
*ptr и ref обрабатываются одинаково. Т.е. это одно и то же:
Поскольку ссылки должны быть инициализированы корректными объектами (они не могут быть нулевыми) и не могут быть изменены позже, то они, как правило, безопаснее указателей (так как риск разыменования нулевого указателя отпадает). Однако они немного ограничены в функциональности по сравнению с указателями.
Если определенное задание может быть решено с помощью как ссылок, так и указателей, то лучше использовать ссылки. Указатели следует использовать только в тех ситуациях, когда ссылки являются недостаточно эффективными (например, при динамическом выделении памяти).
Заключение
Ссылки позволяют определять псевдонимы для других объектов или значений. Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-values. Они не могут быть переприсвоены после инициализации. Ссылки чаще всего используются в качестве параметров в функциях, когда мы хотим изменить значение аргумента или хотим избежать его затратного копирования.
Указатели и ссылки [дубликат]
Вот у Вас есть переменная int a . Это такая буковка, с помощью которой компилятор умеет определить, о чём Вы ему говорите. Он знает, что за буковкой a кроется значение в памяти, расположенное по какому-то адресу (адрес в памяти — это число). Ну, Вы же понимаете, что переменная — это такая штука, чтобы удобно по имени обращаться к данным, записанным в определённом участке памяти.
Так вот, указатель — это, тупо, то самое число, тот самый адрес. Т.е. &a равно именно в точности адресу в памяти, по которому хранится значение. Чтобы избежать невнятных проблем, тип «указатель на int (или на любой другой тип) — int*» — это такой совершенно отдельный тип.
Итак есть два волшебных оператора: * и & . Второй по данной переменной узнаёт её адрес в памяти. Первый по данному адресу (который, как мы помним, хранится в переменной типа int*) возвращает собственно данные, расположенные по этому адресу.
Про ссылки чуть попозже напишу. Или кто-нибудь другой напишет…
![]()
Ссылки, как и указатели в C++ представляют собой адреса объектов в памяти. Уметь ссылаться на конкретный объект настолько важно, что некоторые языки (Java, например, или семейство .NET) заявляют о том, что это практически единственный способ работать с объектами. На таком уровне абстракции между ссылками и указателями разницы нет. Разница появляется уровнем ниже: указатель — это значение, объект первого класса, грубо говоря, номер ячейки памяти. С ними можно производить некоторые арифметические операции, сравнивать как числа и т.д. Чтобы, имея указатель (т.е., адрес), получить доступ к объекту по этому адресу, применяется операция разыменования (dereference):
Ссылки же, в свою очередь, обладают не семантикой «адресов объектов», а семантикой самих объектов. Можно думать, что ссылка — это указатель, который сам, автоматически, применяет dereference. Из этого есть несколько следствий: например, неизменяемость ссылок. В C++ просто нет синтакисиса присвоения ссылки, любое такое присвоение будет присвоением объекту, на который она указывает.
Теперь о Ваших примерах: *p=&t; некорректно (попытка присвоить разыменованному указателю p указатель на число (конкретно, на число t), а не само число), а *p=t , как раз, абсолютно корректно (обращаемся по адресу, хранящемуся в указателе, и записываем туда число t).
Если говорить о глубоких мыслях, то, прежде всего, указатели пришли из C и остались тяжёлым наследием. Думаю, некорректная адресация к памяти — самая частая причина падения программ на C++. Ссылки заменить указатели не могут (они не объекты, в отличие от указателей), но здорово упрощают жизнь, когда вам надо передать некоторую структуру весом в пару килобайт параметром в функцию, и копирование замедлит выполнение программы, но Вы не хотите руками возится с указателями. Ссылки имеют такой же синтаксис использования, как и объекты, на которые они ссылаются, и поэтому более-менее взаимозаменяемы.
Чувствую, что написал-то я уже много, а ничего ясного сформулировать не смог 🙁
Надеюсь, хоть немного понятнее стало