Является ли инкремент нулевого указателя корректным?
Есть много примеров undefined/неуказанного поведения при выполнении арифметики указателя - указатели должны указывать внутри одного и того же массива (или один за концом) или внутри одного и того же объекта, ограничения на то, когда вы можете делать сравнения/операции на основе вышеизложенного и т.д.
Является ли следующая операция корректной?
int* p = 0;
p++;
Ответы
Ответ 1
§5.2.6/1:
Значение объекта операнда модифицируется добавлением к нему 1
, если только объект имеет тип bool
[..]
И аддитивные выражения, содержащие указатели, определены в §5.7/5:
Если оба операнда указателя и результат указывают на элементы тот же объект массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае, поведение undefined.
Ответ 2
Похоже, что довольно слабое понимание того, что означает "undefined поведение".
В C, С++ и родственных языках, таких как Objective-C, существует четыре типа поведения: существует поведение, определенное стандартом языка. Существует определенное поведение реализации, что означает, что в стандарте языка явно указано, что реализация должна определять поведение. Существует неопределенное поведение, когда в языковом стандарте указано, что возможны несколько вариантов поведения. И существует поведение undefined, , где языковой стандарт ничего не говорит о результате. Поскольку языковой стандарт ничего не говорит о результате, все может произойти с поведением undefined.
Некоторые люди предполагают, что "поведение undefined" означает "что-то плохое". Это неправильно. Это означает, что "все может случиться", и это включает в себя "что-то плохое может случиться", а не "что-то плохое должно произойти". На практике это означает: "ничего плохого не происходит, когда вы проверяете свою программу, но как только она отправляется клиенту, все ад разрывается". Поскольку все может случиться, компилятор может фактически предположить, что в вашем коде нет поведения undefined, потому что либо оно истинно, либо оно ложно, и в этом случае что-либо может произойти, что означает, что все происходит из-за неправильного предположения компилятора все еще верна.
Кто-то утверждал, что когда p указывает на массив из трех элементов и вычисляется p + 4, ничего плохого не произойдет. Неправильно. Вот ваш оптимизационный компилятор. Скажите, что это ваш код:
int f (int x)
{
int a [3], b [4];
int* p = (x == 0 ? &a [0] : &b [0]);
p + 4;
return x == 0 ? 0 : 1000000 / x;
}
Оценка p + 4 - это поведение undefined, если p указывает на [0], но не указывает на b [0]. Поэтому компилятору разрешено считать, что p указывает на b [0]. Поэтому компилятору разрешено считать, что x!= 0, потому что x == 0 приводит к поведению undefined. Поэтому компилятору разрешено удалить проверку x == 0 в операторе return и просто вернуть 1000000/x. Это означает, что ваша программа вылетает, когда вы вызываете f (0) вместо возврата 0.
Другое предположение состояло в том, что если вы увеличиваете нулевой указатель и затем уменьшаете его снова, результат снова является нулевым указателем. Неправильно. Помимо возможности того, что приращение нулевого указателя может просто сработать на каком-то оборудовании, что об этом: так как приращение нулевого указателя undefined behavour, компилятор проверяет, является ли указатель нулевым и только увеличивает указатель, если он не является нулевой указатель, поэтому p + 1 снова является нулевым указателем. И, как правило, он будет делать то же самое для декрементирования, но, будучи умным компилятором, замечает, что p + 1 всегда undefined, если результат был нулевым указателем, поэтому можно предположить, что p + 1 не является нулевым указатель, поэтому проверка нулевого указателя может быть опущена. Это означает, что (p + 1) - 1 не является нулевым указателем, если p был нулевым указателем.
Ответ 3
Операции над указателем (например, incrementing, add, etc) обычно действительны только в том случае, если начальное значение указателя и результат указывают на элементы одного и того же массива (или на один последний элемент). В противном случае результат будет undefined. В стандарте существуют различные условия для различных операторов, говорящих об этом, в том числе для приращения и добавления.
(Существует несколько исключений, таких как добавление нуля в NULL или вычитание нулевого значения из NULL, но это не применимо здесь).
Указатель NULL не указывает ни на что, поэтому приращение его дает поведение undefined (применяется предложение "иначе" ).
Ответ 4
Оказывается, это действительно undefined. Существуют системы, для которых это верно
int *p = NULL;
if (*(int *)&p == 0xFFFF)
Следовательно, ++ p будет отключать правило переполнения undefined (получается, что sizeof (int *) == 2)). Указатели не гарантируют отсутствие целых чисел без знака, поэтому правило обнуления без знака не применяется.
Ответ 5
Как сказал Коломбо, это UB. И с точки зрения адвоката языка это окончательный ответ.
Однако все реализации компилятора С++, которые я знаю, дадут тот же результат:
int *p = 0;
intptr_t ip = (intptr_t) p + 1;
cout << ip - sizeof(int) << endl;
дает 0
, что означает, что p
имеет значение 4 в 32-битной реализации и 8 на 64 битах
Говорят иначе:
int *p = 0;
intptr_t ip = (intptr_t) p; // well defined behaviour
ip += sizeof(int); // integer addition : well defined behaviour
int *p2 = (int *) ip; // formally UB
p++; // formally UB
assert ( p2 == p) ; // works on all major implementation
Ответ 6
Из ИСО МЭК 14882-2011 §5.2.6:
Значение выражения postfix ++ является значением его операнда. [Примечание: полученное значение является копией оригинальное значение -end note] Операнд должен быть модифицируемым значением lvalue. Тип операнда должен быть арифметический тип или указатель на полный тип объекта.
Так как nullptr является указателем на полный тип объекта. Поэтому я не понимаю, почему это будет undefined.
Как уже было сказано выше, в том же документе также говорится в п. 5.2.6/1:
Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или одно прошлое последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined.
Это выражение кажется немного неоднозначным. В моей интерпретации часть undefined вполне может быть оценкой объекта. И я думаю, что никто не согласился бы с этим. Однако для арифметики указателя требуется только полный объект.
Конечно, операторы postfix [] и вычитания или умножения на указатель на объекты массива только четко определены, если они фактически указывают на один и тот же массив. В основном важно, потому что у вас может возникнуть соблазн подумать, что 2 массива, определенные последовательно в 1 объекте, можно повторить, как если бы они были единым массивом.
Итак, мой вывод состоял бы в том, что операция хорошо определена, но оценки не будет.
Ответ 7
Вернувшись в забавные C-дни, если p был указателем на что-то, p ++ эффективно добавлял размер p к значению указателя, чтобы сделать p точкой следующего. Если вы установите указатель p на 0, то разумно, что p ++ все равно укажет его на следующее, добавив к нему размер p.
Что еще, вы могли бы делать что-то вроде добавления или вычитания чисел из p, чтобы переместить его по памяти (p + 4 будет указывать на четвертое что-то мимо p.) Это были хорошие времена, которые имели смысл. В зависимости от компилятора вы можете пойти в любом месте в своем пространстве памяти. Программы выполнялись быстро, даже на медленном оборудовании, потому что C просто сделал то, что вы ему сказали, и потерпел крах, если вы слишком сумасшедшие/неряшливые.
Таким образом, реальный ответ заключается в том, что установка указателя на 0 корректно определена и приращение указателя хорошо определено. Любые другие ограничения накладываются на вас разработчиками компилятора, разработчиками и разработчиками оборудования.
Ответ 8
Учитывая, что вы можете увеличивать любой указатель на четко определенный размер (так что все, что не является указателем на void), а значение любого указателя - это просто адрес (там нет специальной обработки указателей NULL, если они существуют), Я полагаю, нет причин, по которым инкрементный нулевой указатель не будет (бесполезно) указывать на пункт "один после элемента NULL".
Рассмотрим это:
// These functions are horrible, but they do return the 'next'
// and 'prev' items of an int array if you pass in a pointer to a cell.
int *get_next(int *p) { return p+1; }
int *get_prev(int *p) { return p-1; }
int *j = 0;
int *also_j = get_prev(get_next(j));
and_j выполнил математику, но он равен j, так что это нулевой указатель.
Поэтому я бы предположил, что он четко определен, просто бесполезен.
(И нулевой указатель, имеющий значение 0, когда printfed не имеет значения. Значение нулевого указателя зависит от платформы. Использование нуля в языке для инициализации переменных указателя - это определение языка.)