Является ли инкремент нулевого указателя корректным? - программирование
Подтвердить что ты не робот

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

Есть много примеров undefined/неуказанного поведения при выполнении арифметики указателя - указатели должны указывать внутри одного и того же массива (или один за концом) или внутри одного и того же объекта, ограничения на то, когда вы можете делать сравнения/операции на основе вышеизложенного и т.д.

Является ли следующая операция корректной?

int* p = 0;
p++;
4b9b3361

Ответ 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 не имеет значения. Значение нулевого указателя зависит от платформы. Использование нуля в языке для инициализации переменных указателя - это определение языка.)