Для чего необходим указатель на массив
Кратко об указателях в Си: присваивание, разыменование и перемещение по массивам
Приветствую вас, дорогие читатели. В данной статье кратко описаны основные сведения об указателях в языке Си. Кроме основных операций с указателями (объявление, взятие адреса, разыменование) рассмотрены вопросы безопасности типов при работе с ними. К сожалению, в данной статье вы не найдёте информацию по операциям сравнений указателей. Однако, статья будет полезна новичкам, а также тем, кто работает с массивами. Все примеры в данной статье компилировались компилятором gcc (восьмой версии).
Введение
Количество звёздочек лишь указывает на длину цепочек хранимых адресов. Поскольку указатель также является переменной и имеет адрес, то его адрес также можно хранить в другом указателе. В выше приведённом примере адрес переменной a сохраняется в переменной-указателе ptr. Адрес же самой переменной ptr сохраняется в другом указателе pptr. Чтобы получить адрес переменной, перед её именем надо поставить знак амперсанда (&). Наконец, чтобы выполнить обратную операцию, т.е. получить значение (содержимое) по адресу, хранимому в указателе, имя указателя предваряется звёздочкой, почти как при объявлении. Почти, потому что одной звёздочки достаточно чтобы «распаковать» указатель. Поскольку pptr указывает по адресу на значение, хранимое в ptr, то необходимо два раза применить операцию разыменования.
Указатели в предыдущем примере хранят адрес переменной определённого типа. В случае, когда применяются указатели типа void (любого типа), то прежде чем распаковать значение по адресу, необходимо выполнить приведение к типизированному указателю. Следующий пример является версией предыдущего, но с использованием указателя любого типа.
Изменения значения переменной через указатель.
Так как указатель хранит адрес переменной, мы можем через адрес не только получить значение самой переменной, но также его изменить. Например:
Как было сказано выше, указатели хранят адреса. Естественно, что адреса могут указывать не только на ячейки данных переменных в вашей программе, но и на другие вещи: адрес стека процедур, адрес начала сегмента кода, адрес какой-то процедуры ядра ОС, адрес в куче и т. д. Логично, что не все адреса можно использовать напрямую в программе, поскольку некоторые из них указывают на те участки памяти, которые нельзя изменять (доступ для чтения), или которые нельзя затирать. В случае, при обращении к участку, доступному только для чтения, при попытке изменить значение получим ошибку Segmentation Fault (SF).
Кроме того, в языке Си определён макрос с именем NULL, для обозначения указателя с нулевым адресом. Данный адрес обычно используется операционной системой для сигнала об ошибке при работе с памятью. При попытке что либо читать по этому адресу, программа может получить неопределённое поведение. Поэтому ни в коем случае не пытайтесь извлечь значение по пустому указателю.
И ещё, указатели могут указывать на один и тот же объект. Например:
Этот простой пример показывает, что через адреса можно менять содержимое простых переменных, а также остальных указателей, ссылающихся на тоже самое. Таким образом, указатель p2 как бы является псевдонимом (alias) для p1.
Передача параметров через указатели.
Параметры функций могут быть указателями. В случае вызова таких функций, они копируют значения аргументов в свои параметры как обычно. Единственное отличие здесь в том, что они копируют адреса, содержащиеся в указателях параметрах. И с помощью полученных адресов, можно изменять объекты, на которые указывают параметры. Ниже приведена стандартная процедура обмена значений между двумя целочисленными переменными.
Здесь переменные а и b меняются своими значениями друг с другом (при условии, что параметры содержат не нулевой адрес). Отметим ещё раз, что мы можем изменить содержимое, указываемое по параметру-указателю методов. И, конечно, мы можем стереть данный адрес, присвоив параметру новое значение.
Проверка типов и массивы
Постоянные (const) и указатели.
Напомним, чтобы сделать переменную с постоянным, фиксированным значением, надо добавить ключевое слово const перед её именем (до имени типа или после). Например:
Для объявления указателя на постоянное значение, ключевое слово const должно быть ПЕРЕД звёздочкой.
В примере выше была создана переменная-указатель, ссылающееся на постоянное значение. Слово 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 ) — это указатели на массивы.
А теперь посмотрим на такую ситуацию:
Массивы и указатели
Указатели и массивы
П усть есть массив
int A[5] = <1, 2, 3, 4, 5>;
Мы уже показали, что указатели очень похожи на массивы. В частности, массив хранит адрес, откуда начинаются его элементы. Используя указатель можно также получить доступ до элементов массива
int *p = A;
тогда вызов A[3] эквивалентен вызову *(p + 3)
На самом деле оператор [ ] является синтаксическим сахаром – он выполняет точно такую же работу. То есть вызов A[3] также эквивалентен вызову *(A + 3)
Это правильный код, который будет работать. Дело в том, что компилятор подменяет массив на указатель. Данный пример работает, потому что мы действительно работаем с указателем (хотя помним, что массив отличается от указателя). То же самое происходит и при вызове функции. Если функция требует указатель, то можно передавать в качестве аргумента массив, так как он будет подменён указателем.
Многомерные массивы и указатели на многомерные массивы.
Т еперь рассмотрим такой пример
Этот код не скомпилируется. Дело в том, что правило подмены массива на указатель на рекурсивное. Поэтому при определении многомерного массива нужно указывать размер явно, а пустыми оставлять можно только первые скобки. Этот пример можно переписать так
Только здесь уже p будет именем массива, каждый элемент которого является указателем. И точно так же, как мы обращались к элементам массива через массив указателей *p[3], через имя массива можно обратиться к элементу массива
Тоже самое правило действует и при вызове функций. Если функция требует указателя на указатель, то нельзя просто передать двумерный массив, потому что он не будет подменён указателем на указатель, а будет заменён массивом указателей.
Чтобы динамически созданный двумерный массив имел структуру статического двумерного массива, необходимо, чтобы он знал «число столбцов» двумерного массива, то есть длину одной строки. Для этого можно воспользоваться указателем на одномерный массив. Неудобство такого подхода в том, что необходимо заранее знать число элементов каждого подмассива. Однако, многомерный массив всегда можно создать из одномерного, тогда вообще никаких проблем не обнаружится.
10.10 – Указатели и массивы
Указатели и массивы неразрывно связаны в C++.
Разложение массива
На предыдущем уроке вы узнали, как определить фиксированный массив:
Во всех случаях, кроме двух (которые мы рассмотрим ниже), когда в выражении используется фиксированный массив, фиксированный массив будет раскладываться (неявно преобразовываться) в указатель, указывающий на первый элемент массива. Вы можете увидеть это в следующей программе:
На машине автора эта программа напечатала:
Мы скоро увидим, в чем разница.
Все элементы массива по-прежнему доступны через указатель (мы увидим, как это работает в следующем уроке), но информация, полученная на основе типа массива (например, длина массива), не может быть доступна из указателя.
Однако это также эффективно позволяет нам в большинстве случаев обрабатывать фиксированные массивы и указатели одинаково.
Например, мы можем использовать косвенное обращение к массиву, чтобы получить значение первого элемента:
Обратите внимание, что на самом деле мы не выполняем косвенное обращение через сам массив. Массив (типа int[5] ) неявно преобразуется в указатель (типа int* ), и мы используем косвенное обращение через этот указатель, чтобы получить значение по адресу памяти, который хранит указатель (значение первого элемента массива).
Мы также можем присвоить указателю значение, чтобы тот указывал на массив:
Различия между указателями и фиксированными массивами
Есть несколько случаев, когда разница при наборе кода между фиксированными массивами и указателями имеет значение. Это помогает проиллюстрировать, что фиксированный массив и указатель – это не одно и то же.
Эта программа печатает:
Фиксированный массив знает, какой длины массив, на который он указывает. Указатель на массив – нет.
Второе отличие возникает при использовании оператора адреса ( & ). Взятие адреса указателя дает адрес памяти переменной указателя. Взятие адреса массива возвращает указатель на весь массив. Этот указатель также указывает на первый элемент массива, но информация о типе отличается (в приведенном выше примере тип &array – это int(*)[5] ). Вряд ли вам когда-нибудь это понадобится.
Возвращаясь к передаче фиксированных массивов функциям
Еще в уроке «10.2– Массивы (часть 2)» мы упоминали, что, поскольку копирование больших массивов может быть очень дорогостоящим, C++ не копирует массив, когда тот передается в функцию. При передаче массива в качестве аргумента функции фиксированный массив раскладывается на указатель, и функции передается этот указатель:
Обратите внимание, что это происходит, даже если параметр объявлен как фиксированный массив:
В приведенном выше примере C++ неявно преобразует параметр, использующий синтаксис массива ( [] ), в синтаксис указателя ( * ). Это означает, что следующие два объявления функций идентичны:
Лучшая практика
Для параметра функции, принимающего массив, используйте синтаксис указателя ( * ) вместо синтаксиса массива ( [] ).
Знакомство с передачей по адресу
Тот факт, что массивы раскладываются на указатели при передаче в функцию, объясняет основную причину, по которой изменение массива в функции изменяет настоящий переданный массив-аргумент. Рассмотрим следующий пример:
Проницательные читатели заметят, что это явление работает и с указателями на значения, не являющиеся массивами. Мы рассмотрим эту тему (называемую передачей по адресу) более подробно в следующей главе.
Массивы в структурах и классах не раскладываются
Наконец, стоит отметить, что массивы, которые являются частью структур или классов, не раскладываются, когда вся структура или класс передается функции. Это дает полезный способ предотвратить разложение, если это необходимо, и будет полезно позже, когда мы будем писать классы, использующие массивы.
В следующем уроке мы рассмотрим арифметику указателей и поговорим о том, как на самом деле работает индексирование массивов.
Указатель на массив в C++
В этом уроке мы узнаем о связи между массивами и указателями с помощью примеров. В C++ указатели – это переменные, которые содержат адреса других переменных. В указателе может храниться не только адрес отдельной переменной, но и адрес ячеек массива.
Рассмотрим этот пример:
Обратите внимание, что мы использовали arr вместо arr [0], поскольку они оба они одинаковы. Итак, код ниже такой же, как и код выше.
Адреса остальных элементов массива задаются, как arr [1], arr [2], arr [3] и arr [4].
Если нужно указать на все элементы массива
Если ptr указывает на первый элемент в приведенном выше примере, тогда ptr + 3 будет указывать на четвертый элемент. Например:
Точно так же мы можем получить доступ к элементам, используя единственный указатель. Например:
Предположим, если мы инициализировали ptr = arr [2], тогда:
Примечание. Адрес между ptr и ptr + 1 отличается на 4 байта. Это потому, что ptr является указателем на данные типа int и его размер составляет 4 байта в 64-битной операционной системе.
Точно так же, если указатель ptr указывает на данные типа char. Тогда адрес между ptr и ptr + 1 составляет 1 байт, поскольку размер символа составляет 1 байт.
Пример 1
В большинстве случаев имена массивов превращаются в указатели. Проще говоря, имена массивов преобразуются в указатели. Вот почему мы можем использовать указатели для доступа к элементам массивов в С++.
Однако следует помнить, что указатели и массивы – это не одно и то же. Есть несколько случаев, когда имена массивов не превращаются в указатели.
Пример 2: имя массива, используемое в качестве указателя
Этот код эквивалентен приведенному ниже коду: