Указатели
Указатели
Э то, пожалуй, самая сложная и самая важная тема во всём курсе. Без понимания указателей дальнейшее изучении си будет бессмысленным. Указатели – очень простая концепция, очень логичная, но требующая внимания к деталям.
Определение
У казатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип. Синтаксис объявления указателей
Например
float *a;
long long *b;
Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.
Рассмотрим код внимательно, ещё раз
Была объявлена переменная с именем A. Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.
Создали указатель типа int.
Теперь переменная p хранит адрес переменной A. Используя оператор * мы получаем доступ до содержимого переменной A.
Чтобы изменить содержимое, пишем
После этого значение A также изменено, так как она указывает на ту же область памяти. Ничего сложного.
Теперь другой важный пример
Будет выведено
4
4
8
4
Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?
Арифметика указателей
В о-первых, указателю нужен тип для того, чтобы корректно работала операция разыменования (получения содержимого по адресу). Если указатель хранит адрес переменной, необходимо знать, сколько байт нужно взять, начиная от этого адреса, чтобы получить всю переменную.
Во-вторых, указатели поддерживают арифметические операции. Для их выполнения необходимо знать размер.
операция + N сдвигает указатель вперёд на N*sizeof(тип) байт.
Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.
Заметьте, каким образом мы получили адрес первого элемента массива
Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому
Если же указатели равны, то они указывают на одну и ту же область памяти.
Указатель на указатель
У казатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как
Очевидно, ничто не мешает создать и указатель на указатель на указатель, и указатель на указатель на указатель на указатель и так далее. Это нам понадобится при работе с двумерными и многомерными массивами. А вот простой пример, как можно работать с указателем на указатель.
Указатели и приведение типов
Т ак как указатель хранит адрес, можно кастовать его до другого типа. Это может понадобиться, например, если мы хотим взять часть переменной, или если мы знаем, что переменная хранит нужный нам тип.
В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.
У казатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот «мусор» вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.
По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false. Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float).
Это значит, что в данном случае
вполне корректная операция, а в случае
поведение не определено. То есть указатель можно сравнивать с нулём, или с NULL, но нельзя NULL сравнивать с переменной целого типа или типа с плавающей точкой.
Примеры
Теперь несколько примеров работы с указателями
1. Пройдём по массиву и найдём все чётные элементы.
2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.
3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.
В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof), всё равно мы будет обменивать местами байты двух переменных.
4. Найдём длину строки, введённой пользователем, используя указатель
Кратко об указателях в Си: присваивание, разыменование и перемещение по массивам
Приветствую вас, дорогие читатели. В данной статье кратко описаны основные сведения об указателях в языке Си. Кроме основных операций с указателями (объявление, взятие адреса, разыменование) рассмотрены вопросы безопасности типов при работе с ними. К сожалению, в данной статье вы не найдёте информацию по операциям сравнений указателей. Однако, статья будет полезна новичкам, а также тем, кто работает с массивами. Все примеры в данной статье компилировались компилятором gcc (восьмой версии).
Введение
Количество звёздочек лишь указывает на длину цепочек хранимых адресов. Поскольку указатель также является переменной и имеет адрес, то его адрес также можно хранить в другом указателе. В выше приведённом примере адрес переменной a сохраняется в переменной-указателе ptr. Адрес же самой переменной ptr сохраняется в другом указателе pptr. Чтобы получить адрес переменной, перед её именем надо поставить знак амперсанда (&). Наконец, чтобы выполнить обратную операцию, т.е. получить значение (содержимое) по адресу, хранимому в указателе, имя указателя предваряется звёздочкой, почти как при объявлении. Почти, потому что одной звёздочки достаточно чтобы «распаковать» указатель. Поскольку pptr указывает по адресу на значение, хранимое в ptr, то необходимо два раза применить операцию разыменования.
Указатели в предыдущем примере хранят адрес переменной определённого типа. В случае, когда применяются указатели типа void (любого типа), то прежде чем распаковать значение по адресу, необходимо выполнить приведение к типизированному указателю. Следующий пример является версией предыдущего, но с использованием указателя любого типа.
Изменения значения переменной через указатель.
Так как указатель хранит адрес переменной, мы можем через адрес не только получить значение самой переменной, но также его изменить. Например:
Как было сказано выше, указатели хранят адреса. Естественно, что адреса могут указывать не только на ячейки данных переменных в вашей программе, но и на другие вещи: адрес стека процедур, адрес начала сегмента кода, адрес какой-то процедуры ядра ОС, адрес в куче и т. д. Логично, что не все адреса можно использовать напрямую в программе, поскольку некоторые из них указывают на те участки памяти, которые нельзя изменять (доступ для чтения), или которые нельзя затирать. В случае, при обращении к участку, доступному только для чтения, при попытке изменить значение получим ошибку Segmentation Fault (SF).
Кроме того, в языке Си определён макрос с именем NULL, для обозначения указателя с нулевым адресом. Данный адрес обычно используется операционной системой для сигнала об ошибке при работе с памятью. При попытке что либо читать по этому адресу, программа может получить неопределённое поведение. Поэтому ни в коем случае не пытайтесь извлечь значение по пустому указателю.
И ещё, указатели могут указывать на один и тот же объект. Например:
Этот простой пример показывает, что через адреса можно менять содержимое простых переменных, а также остальных указателей, ссылающихся на тоже самое. Таким образом, указатель p2 как бы является псевдонимом (alias) для p1.
Передача параметров через указатели.
Параметры функций могут быть указателями. В случае вызова таких функций, они копируют значения аргументов в свои параметры как обычно. Единственное отличие здесь в том, что они копируют адреса, содержащиеся в указателях параметрах. И с помощью полученных адресов, можно изменять объекты, на которые указывают параметры. Ниже приведена стандартная процедура обмена значений между двумя целочисленными переменными.
Здесь переменные а и b меняются своими значениями друг с другом (при условии, что параметры содержат не нулевой адрес). Отметим ещё раз, что мы можем изменить содержимое, указываемое по параметру-указателю методов. И, конечно, мы можем стереть данный адрес, присвоив параметру новое значение.
Проверка типов и массивы
Постоянные (const) и указатели.
Напомним, чтобы сделать переменную с постоянным, фиксированным значением, надо добавить ключевое слово const перед её именем (до имени типа или после). Например:
Для объявления указателя на постоянное значение, ключевое слово const должно быть ПЕРЕД звёздочкой.
В примере выше была создана переменная-указатель, ссылающееся на постоянное значение. Слово const перед звёздочкой указывает, что нельзя менять содержимое напрямую (путём разыменования, обращения к ячейке). Но сама переменная указатель постоянной не является. А значит, ей можно присвоить новый адрес. Например, адрес следующей ячейки в массиве.
Урок №80. Указатели
На уроке №10 мы узнали, что переменная — это название кусочка памяти, который содержит значение.
Оператор адреса &
При выполнении инициализации переменной, ей автоматически присваивается свободный адрес памяти, и, любое значение, которое мы присваиваем переменной, сохраняется по этому адресу в памяти. Например:
При выполнении этого стейтмента процессором, выделяется часть оперативной памяти. В качестве примера предположим, что переменной b присваивается ячейка памяти под номером 150. Всякий раз, когда программа встречает переменную b в выражении или в стейтменте, она понимает, что для того, чтобы получить значение — ей нужно заглянуть в ячейку памяти под номером 150.
Хорошая новость — нам не нужно беспокоиться о том, какие конкретно адреса памяти выделены для определенных переменных. Мы просто ссылаемся на переменную через присвоенный ей идентификатор, а компилятор конвертирует это имя в соответствующий адрес памяти. Однако этот подход имеет некоторые ограничения, которые мы обсудим на этом и следующих уроках.
Оператор адреса & позволяет узнать, какой адрес памяти присвоен определенной переменной. Всё довольно просто:
Результат на моем компьютере:
Примечание: Хотя оператор адреса выглядит так же, как оператор побитового И, отличить их можно по тому, что оператор адреса является унарным оператором, а оператор побитового И — бинарным оператором.
Оператор разыменования *
Оператор разыменования * позволяет получить значение по указанному адресу:
Результат на моем компьютере:
Примечание: Хотя оператор разыменования выглядит так же, как и оператор умножения, отличить их можно по тому, что оператор разыменования — унарный, а оператор умножения — бинарный.
Указатели
Теперь, когда мы уже знаем об операторах адреса и разыменования, мы можем поговорить об указателях.
Указатель — это переменная, значением которой является адрес ячейки памяти. Указатели объявляются точно так же, как и обычные переменные, только со звёздочкой между типом данных и идентификатором:
Синтаксически язык C++ принимает объявление указателя, когда звёздочка находится рядом с типом данных, с идентификатором или даже посередине. Обратите внимание, эта звёздочка не является оператором разыменования. Это всего лишь часть синтаксиса объявления указателя.
Однако, при объявлении нескольких указателей, звёздочка должна находиться возле каждого идентификатора. Это легко забыть, если вы привыкли указывать звёздочку возле типа данных, а не возле имени переменной. Например:
По этой причине, при объявлении указателя, рекомендуется указывать звёздочку возле имени переменной. Как и обычные переменные, указатели не инициализируются при объявлении. Содержимым неинициализированного указателя является обычный мусор.
Присваивание значений указателю
Поскольку указатели содержат только адреса, то при присваивании указателю значения — это значение должно быть адресом. Для получения адреса переменной используется оператор адреса:
Приведенное выше можно проиллюстрировать следующим образом:
Еще очень часто можно увидеть следующее:
Результат на моем компьютере:
Тип указателя должен соответствовать типу переменной, на которую он указывает:
Следующее не является допустимым:
Это связано с тем, что указатели могут содержать только адреса, а целочисленный литерал 7 не имеет адреса памяти. Если вы все же сделаете это, то компилятор сообщит вам, что он не может преобразовать целочисленное значение в целочисленный указатель.
Язык C++ также не позволит вам напрямую присваивать адреса памяти указателю:
Оператор адреса возвращает указатель
Стоит отметить, что оператор адреса & не возвращает адрес своего операнда в качестве литерала. Вместо этого он возвращает указатель, содержащий адрес операнда, тип которого получен из аргумента (например, адрес переменной типа int передается как адрес указателя на значение типа int):
Результат выполнения программы:
Разыменование указателей
Как только у нас есть указатель, указывающий на что-либо, мы можем его разыменовать, чтобы получить значение, на которое он указывает. Разыменованный указатель — это содержимое ячейки памяти, на которую он указывает:
0034FD90
5
0034FD90
5
Вот почему указатели должны иметь тип данных. Без типа указатель не знал бы, как интерпретировать содержимое, на которое он указывает (при разыменовании). Также, поэтому и должны совпадать тип указателя с типом переменной. Если они не совпадают, то указатель при разыменовании может неправильно интерпретировать биты (например, вместо типа double использовать тип int).
Одному указателю можно присваивать разные значения:
Когда адрес значения переменной присвоен указателю, то выполняется следующее:
ptr — это то же самое, что и &value ;
Разыменование некорректных указателей
Указатели в языке C++ по своей природе являются небезопасными, а их неправильное использование — один из лучших способов получить сбой программы.
При разыменовании указателя, программа пытается перейти в ячейку памяти, которая хранится в указателе и извлечь содержимое этой ячейки. По соображениям безопасности современные операционные системы (ОС) запускают программы в песочнице для предотвращения их неправильного взаимодействия с другими программами и для защиты стабильности самой операционной системы. Если программа попытается получить доступ к ячейке памяти, не выделенной для нее операционной системой, то ОС сразу завершит выполнение этой программы.
Следующая программа хорошо иллюстрирует вышесказанное. При запуске вы получите сбой (попробуйте, ничего страшного с вашим компьютером не произойдет):
Размер указателей
Размер указателя зависит от архитектуры, на которой скомпилирован исполняемый файл: 32-битный исполняемый файл использует 32-битные адреса памяти. Следовательно, указатель на 32-битном устройстве занимает 32 бита (4 байта). С 64-битным исполняемым файлом указатель будет занимать 64 бита (8 байт). И это вне зависимости от того, на что указывает указатель:
Как вы можете видеть, размер указателя всегда один и тот же. Это связано с тем, что указатель — это всего лишь адрес памяти, а количество бит, необходимое для доступа к адресу памяти на определенном устройстве, — всегда постоянное.
В чём польза указателей?
Сейчас вы можете подумать, что указатели являются непрактичными и вообще ненужными. Зачем использовать указатель, если мы можем использовать исходную переменную?
Однако, оказывается, указатели полезны в следующих случаях:
Случай №1: Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву.
Случай №2: Они являются единственным способом динамического выделения памяти в C++. Это, безусловно, самый распространенный вариант использования указателей.
Случай №3: Они могут использоваться для передачи большого количества данных в функцию без копирования этих данных.
Случай №4: Они могут использоваться для передачи одной функции в качестве параметра другой функции.
Случай №5: Они используются для достижения полиморфизма при работе с наследованием.
Случай №6: Они могут использоваться для представления одной структуры/класса в другой структуре/классе, формируя, таким образом, целые цепочки.
Указатели применяются во многих случаях. Не волнуйтесь, если вы многого не понимаете из вышесказанного. Теперь, когда мы разобрались с указателями на базовом уровне, мы можем начать углубляться в отдельные случаи, в которых они полезны, что мы и сделаем на последующих уроках.
Заключение
Указатели — это переменные, которые содержат адреса памяти. Их можно разыменовать с помощью оператора разыменования * для извлечения значений, хранимых по адресу памяти. Разыменование указателя, значением которого является мусор, приведет к сбою в вашей программе.
Совет: При объявлении указателя указывайте звёздочку возле имени переменной.
Задание №1
Какие значения мы получим в результате выполнения следующей программы (предположим, что это 32-битное устройство, и тип short занимает 2 байта):
Указатели в C абстрактнее, чем может показаться
Указатель ссылается на ячейку памяти, а разыменовать указатель — значит считать значение указываемой ячейки. Значением самого указателя является адрес ячейки памяти. Стандарт языка C не оговаривает форму представления адресов памяти. Это очень важное замечание, поскольку разные архитектуры могут использовать разные модели адресации. Большинство современных архитектур использует линейное адресное пространство или аналогичное ему. Однако даже этот вопрос не оговаривается строго, поскольку адреса могут быть физическими или виртуальными. В некоторых архитектурах используется и вовсе нечисловое представление. Так, Symbolics Lisp Machine оперирует кортежами вида (object, offset) в качестве адресов.
| Через некоторое время, уже после публикации перевода на Хабре автор внёс большие модификации в текст статьи. Обновлять перевод на Хабре не очень хорошая идея, так как некоторые комментарии потеряют смысл или будут смотреться неуместно. Публиковать текст, как новую статью, тоже не хочется. Поэтому мы просто обновили перевод статьи на сайте viva64.com, а здесь оставили всё как есть. Если Вы новый читатель, то предлагаю читать более свежий перевод на нашем сайте, перейдя по приведённой выше ссылке. |
Стандарт не оговаривает форму представления указателей, но оговаривает — в большей или меньшей степени — операции с ними. Ниже мы рассмотрим эти операции и особенности их определения в стандарте. Начнём со следующего примера:
Если мы скомпилируем этот код GCC с уровнем оптимизации 1 и запустим программу под Linux x86-64, она напечатает следующее:
Обратите внимание, что указатели p и q ссылаются на один и тот же адрес. Однако результат выражения p == q есть false, и это на первый взгляд кажется странным. Разве два указателя на один и тот же адрес не должны быть равны?
Вот как стандарт C определяет результат проверки двух указателей на равенство:
| C11 § 6.5.9 пункт 6 Два указателя равны тогда и только тогда, когда оба являются нулевыми, либо указывают на один и тот же объект (в том числе указатель на объект и первый подобъект в составе объекта) или функцию, либо указывают на позицию за последним элементом массива, либо один указатель ссылается на позицию за последним элементом массива, а другой — на начало другого массива, следующего сразу за первым в том же адресном пространстве. |
Прежде всего возникает вопрос: что такое «объект»? Поскольку речь идёт о языке C, то очевидно, что здесь объекты не имеют ничего общего с объектами в языках ООП вроде C++. В стандарте C это понятие определяется не вполне строго:
| C11 § 3.15 Объект — это область хранения данных в среде выполнения, содержимое которой может использоваться для представления значений ПРИМЕЧАНИЕ При упоминании объект может рассматриваться как имеющий конкретный тип; см. 6.3.2.1. |
Давайте разбираться. 16-битная целочисленная переменная — это набор данных в памяти, которые могут представлять 16-битные целочисленные значения. Следовательно, такая переменная является объектом. Будут ли два указателя равны, если один из них ссылается на первый байт данного целого числа, а второй — на второй байт этого же числа? Комитет по стандартизации языка, разумеется, имел в виду совсем не это. Но тут надо заметить, что на этот счёт у него нет чётких разъяснений, и мы вынуждены гадать, что же имелось в виду на самом деле.
Когда на пути встаёт компилятор
Вернёмся к нашему первому примеру. Указатель p получен из объекта a, а указатель q — из объекта b. Во втором случае применяется адресная арифметика, которая для операторов «плюс» и «минус» определена следующим образом:
| C11 § 6.5.6 пункт 7 При использовании с этими операторами указатель на объект, не являющийся элементом массива, ведёт себя, как указатель на начало массива длиной в один элемент, тип которого соответствует типу исходного объекта. |
Поскольку любой указатель на объект, не являющийся массивом, фактически становится указателем на массив длиной в один элемент, стандарт определяет адресную арифметику только для указателей на массивы — это уже пункт 8. Нас интересует следующая его часть:
| C11 § 6.5.6 пункт 8 Если целочисленное выражение прибавляется к указателю или вычитается из него, результирующий указатель имеет тот же тип, что и исходный указатель. Если исходный указатель ссылается на элемент массива и массив имеет достаточную длину, то исходный и результирующий элементы отстоят друг от друга так, что разность между их индексами равна значению целочисленного выражения. Другими словами, если выражение P указывает на i-й элемент массива, выражения (P)+N (или равносильное ему N+(P)) и (P)-N (где N имеет значение n) указывают соответственно на (i+n)-й и (i−n)-й элементы массива, при условии что они существуют. Более того, если выражение P указывает на последний элемент массива, то выражение (P)+1 указывает на позицию за последним элементом массива, а если выражение Q указывает на позицию за последним элементом массива, то выражение (Q)-1 указывает на последний элемент массива. Если и исходный, и результирующий указатели ссылаются на элементы одного и того же массива либо на позицию за последним элементом массива, то переполнение исключено; в противном случае поведение не определено. Если результирующий указатель ссылается на позицию за последним элементом массива, к нему не может применяться унарный оператор *. |
Из этого следует, что результатом выражения &b + 1 совершенно точно должен быть адрес, и, значит, p и q — это валидные указатели. Напомню, как определено равенство двух указателей в стандарте: «Два указателя равны тогда и только тогда, когда [. ] один указатель ссылается на позицию за последним элементом массива, а другой — на начало другого массива, следующего сразу за первым в том же адресном пространстве» (C11 § 6.5.9 пункт 6). Именно это мы и наблюдаем в нашем примере. Указатель q ссылается на позицию за объектом b, за которым сразу же следует объект a, на который ссылается указатель p. Получается, в GCC баг? Это противоречие было описано в 2014 году как ошибка #61502, но разработчики GCC не считают его багом и поэтому исправлять его не собираются.
С похожей проблемой в 2016 году столкнулись программисты под Linux. Рассмотрим следующий код:
Символами _start и _end задают границы области памяти. Поскольку они вынесены во внешний файл, компилятор не знает, как на самом деле массивы расположены в памяти. По этой причине он должен здесь проявить осторожность и исходить из предположения, что они следуют в адресном пространстве друг за другом. Однако GCC компилирует условие цикла так, что оно всегда верно, из-за чего цикл становится бесконечным. Эта проблема описана вот в этом посте на LKML — там используется похожий фрагмент кода. Кажется, в данном случае авторы GCC все-таки учли замечания и изменили поведение компилятора. По крайней мере я не смог воспроизвести эту ошибку в версии GCC 7.3.1 под Linux x86_64.
Разгадка — в отчёте об ошибке #260?
Наш случай может прояснить отчёт об ошибке #260. Он больше касается неопределённых значений, однако в нём можно найти любопытный комментарий от комитета:
Реализации компиляторов [. ] могут также различать указатели, полученные из разных объектов, даже если эти указатели имеют одинаковый набор битов.
Если понимать этот комментарий буквально, то тогда логично, что результат выражения p == q есть «ложь», так как p и q получены из разных объектов, никак не связанных между собой. Похоже, мы всё ближе подбираемся к истине — или нет? До сих пор мы имели дело с операторами равенства, а как насчёт операторов отношения?
Окончательная разгадка — в операторах отношения?
Определение операторов отношения и >= в контексте сравнения указателей содержит одну любопытную мысль:
| C11 § 6.5.8 пункт 5 Результат сравнения двух указателей зависит от взаимного расположения указываемых объектов в адресном пространстве. Если два указателя на объектные типы ссылаются на один и тот же объект, либо оба ссылаются на позицию за последним элементом одного и того же массива, то такие указатели равны. Если указываемые объекты являются членами одного и того же составного объекта, то указатели на члены структуры, объявленные позже, больше указателей на члены, объявленные раньше, а указатели на элементы массива с большими индексами больше указателей на элементы того же массива с меньшими индексами. Все указатели на члены одного и того же объединения равны. Если выражение P указывает на элемент массива, а выражение Q — на последний элемент того же массива, то значение указателя-выражения Q+1 больше, чем значение выражения P. Во всех остальных случаях поведение не определено. |
Согласно этому определению, результат сравнения указателей определён только в том случае, если указатели получены из одного и того же объекта. Покажем это на двух примерах.
Здесь указатели p и q ссылаются на два разных объекта, которые не связаны между собой. Поэтому результат их сравнения не определён. А вот в следующем примере:
указатели p и q ссылаются на один и тот же объект и, следовательно, связаны между собой. Значит, их можно сравнить — если только malloc не вернёт нулевое значение.
Резюме
Стандарт C11 недостаточно строго описывает сравнение указателей. Наиболее проблемным моментом, с которым мы столкнулись, стал пункт 6 § 6.5.9, где явно разрешено сравнивать два указателя, ссылающиеся на два разных массива. Это противоречит комментарию из отчёта об ошибке #260. Однако там речь идёт о неопределённых значениях, и я не хотел бы строить свои рассуждения на основании одного лишь этого комментария и толковать его в другом контексте. При сравнении указателей операторы отношения определяются несколько иначе, чем операторы равенства — а именно, операторы отношения определены, только если оба указателя получены из одного и того же объекта.
Если отвлечься от текста стандарта и задаться вопросом, можно ли сравнивать два указателя, полученных из двух различных объектов, то в любом случае ответ, скорее всего, будет «нет». Пример в начале статьи демонстрирует скорее теоретическую проблему. Поскольку переменные a и b имеют автоматическую продолжительность хранения, наши предположения об их размещении в памяти будут ненадёжными. В отдельных случаях мы можем угадать, но совершенно очевидно, что такой код не получится безопасно портировать, и узнать смысл программы можно, только скомпилировав и запустив или деассемблировав код, а это противоречит любой серьёзной парадигме программирования.
Однако в целом я не удовлетворён формулировками в стандарте C11, и так как уже несколько человек столкнулось с этой проблемой, актуальным остаётся вопрос: почему бы не сформулировать правила яснее?
Дополнение
Указатели на позицию за последним элементом массива
Что касается правила о сравнении и адресной арифметике указателей на позицию за последним элементом массива, сплошь и рядом можно найти исключения из него. Предположим, что стандарт не разрешал бы сравнивать два указателя, полученные из одного и того же массива, при том что хотя бы один из них ссылается на позицию за концом массива. Тогда следующий код не работал бы:
Можно ли изменить наш пример так, чтобы на позицию за последним элементом массива x не ссылался бы ни один указатель? Можно, но это будет сложнее. Придётся изменить условие цикла и запретить инкремент переменной i на последней итерации.
В этом коде полно технических тонкостей, возня с которыми отвлекает от главной задачи. Кроме того, в теле цикла появилась дополнительная ветка. Так что я считаю разумным, что стандарт разрешает исключения при сравнении указателей на позицию за последним элементом массива.
Примечание команды PVS-Studio
При разработке анализатора кода PVS-Studio нам приходится иногда разбираться с тонкими моментами, чтобы сделать диагностики более точными или чтобы давать подробные консультации нашим клиентам. Эта статья показалась нам интересной, так как затрагивает вопросы, в которых мы сами до конца не чувствуем себя уверенными. Поэтому мы попросили у автора выложить её перевод. Надеемся, так с ней познакомится больше C и C++ программистов и поймут, что не всё так просто и что, когда вдруг анализатор выдаёт странное сообщение, не стоит сразу спешить считать его ложным срабатыванием :).




