2015-04-23 3 views
48

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

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

int* p = 0; 
p++; 
+7

Мне любопытно, почему вы думаете, что это не будет ... – Borgleader

+0

@ Борлингер из-за всех ограничений, о которых я говорил. Если бы это указывало на одно значение, оно было бы определено, но (только?), Потому что оно будет рассматриваться как указатель на массив размера 1. Что в этом случае? –

+1

В конце концов, это всего лишь арифметика (указатель). Арифметика всегда четко определена. – Matt

ответ

37

§5.2.6/1:

Значение объекта операнда модифицируется путем добавления к нему 1, если объект имеет тип bool [..]

И добавочные выражения, содержащие указатели, определены в п. 5.7/5:

Если оба указателя r, а результат указывает на элементы одного объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение не определено.

+6

Мне интересно, как «объект массива» определен в стандарте. Возвращаемое значение 'malloc' считается объектом массива? – user3528438

+0

Как вы гарантируете, что '1' имеет соответствующее представление указателя для ** сопоставлений между указателями и целыми числами, в противном случае определяется реализацией **? – Lingxi

+1

@Lingxi В первой части этой цитаты говорится, что вы можете ее преобразовать. Результирующее значение определяется импиментацией, но законность операций гарантирована, не так ли? (На самом деле, это должно было быть sizeof (int), забыл приспособить это - все равно преобразование с 1 на него должно быть действительно.) – Columbo

13

Операции над указателем (как приращением, добавление и т.д.), как правило, действует только если обе начальное значение указателя и точка результат к элементам одного и того же массива (или к одному мимо последнего элемента) , В противном случае результат не определен. В стандарте существуют различные условия для различных операторов, говорящих об этом, в том числе для приращения и добавления.

(Существует несколько исключений, таких как добавление нуля в NULL или вычитание нулевого значения из NULL, но это не применимо здесь).

Указатель NULL не указывает ни на что, поэтому приращение его дает неопределенное поведение (применяется предложение «иначе»).

+2

Они могут указывать на объект и на один байт за объектом. – Columbo

+1

@Columbo: на что вы ссылаетесь? – dhein

+0

@Zaibis Вероятно, правило, что указатель на один элемент за концом массива действителен, т. Е. Что 'a + 3' действителен, если у вас есть' int a [3] ', несмотря на то, что он указывает на отсутствие действительного элемента. Но говорить о «одном байте» немного странно. – CodesInChaos

0

Как сказал Коломбо, это UB. И с точки зрения адвоката языка это окончательный ответ.

Однако все ++ реализаций компилятор C я знаю, даст тот же результат:

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 
+7

Я бы не стал доверять современному оптимизирующему компилятору с этим. Например, вполне правдоподобно, что он решает, что в p2 = p + 1; if (p == nullptr) {...} 'условие никогда не будет выполнено (поскольку p == null привел бы к UB для первого оператора) и удалит весь оператор' if'. – CodesInChaos

+0

Как я уже говорил в первой строке, это ** определенно UB. Но я не мог найти пример программы, компилятора и параметров, чтобы показать проблему, которую вы даете. –

+0

@SergeBallesta: GCC и даже по умолчанию (поэтому существует флаг '-fno-delete-null-pointer-checks') – MSalters

-2

Учитывая, что вы можете увеличивать любой указатель четко определенного размера (так что все, что не является пустотой указатель), а значение любого указателя - это просто адрес (нет специальной обработки указателей 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)); 

also_j была математикой с ней сделали, но он равен J, так что это нулевой указатель.

Поэтому я бы предположил, что это четко определено, просто бесполезно.

(и нулевой указатель появляется, чтобы иметь нулевое значение, когда printfed не имеет значения. Значение нулевого указателя зависит от платформы. Использование нуля в языке для инициализации переменных указателей является определением языка.)

+1

Хорошая реализация должна помешать любой попытке добавить или вычесть любое целое из нулевого указателя во время выполнения (большая часть вреда от «ошибки в миллиард долларов» проистекает из сбоев платформ для этого).Существует очень мало платформ, где такое поведение когда-либо понадобится, и на практике такое поведение почти всегда сопровождается доступом к беспризорной памяти. Единственный раз, когда подразумеваемая арифметика на нулевом указателе была бы полезна, была бы в случаях, которые полностью компилируются во времени, как разница между двумя указателями, которые имеют постоянные смещения от общей базы. – supercat

+2

Ваше предположение, что это правильно, поскольку оно интуитивно или работает в вашей системе, неверно. C++ управляется языковыми правилами, которые описываются стандартом. Некоторые правила противоречат интуиции, но аргументация заключается в том, что он позволяет реализациям выполнять определенные оптимизации, которые иначе были бы невозможны. –

+0

@LuchianGrigore: Проблема также в том, что если приращение нулевого указателя не является неопределенным поведением, оно должно быть каким-то образом определено. Ну, я бы не хотел отвечать за определение того, что делает инкремент нулевого указателя. Микропроцессор Cat утверждает, что результат определяется как «то, что дает нулевой указатель, если вы его уменьшаете». – gnasher729

1

Получается, что это на самом деле не определено. Есть системы, для которых это верно

int *p = NULL; 
if (*(int *)&p == 0xFFFF) 

Поэтому ++ р будет срабатывать неопределенное правило переполнения (получается, что SizeOf (INT *) == 2)). Указатели не гарантируют отсутствие целых чисел без знака, поэтому правило обнуления без знака не применяется.

+2

Это берет адрес p, я не вижу, насколько это важно ... –

+0

Преобразует значение p в целое число. Странное выражение требуется, чтобы компилятор не генерировал код для замены NULL на 0. Фактическое побитовое значение имеет значение здесь. – Joshua

+0

Это не увеличивает NULL. Он переназначает значение указателя. – Peter

14

Существует, по-видимому, довольно слабое понимание того, что означает «неопределенное поведение».

В C, C++ и родственных языках, таких как Objective-C, существует четыре вида поведения: существует поведение, определенное стандартом языка. Существует определенное поведение реализации, что означает, что в стандарте языка явно указано, что реализация должна определять поведение. Существует неопределенное поведение, когда в языковом стандарте указано, что возможны несколько вариантов поведения. И есть неопределенное поведение, , где языковой стандарт ничего не говорит о результате. Поскольку языковой стандарт ничего не говорит о результате, что-либо вообще может произойти с неопределенным поведением.

Некоторые люди предполагают, что «неопределенное поведение» означает «что-то плохое». Это неверно. Это означает, что «все может случиться», и это включает в себя «что-то плохое может случиться», а не «что-то плохое должно произойти». На практике это означает: «ничего плохого не происходит, когда вы проверяете свою программу, но как только она отправляется клиенту, все ад разрывается». Поскольку все может случиться, компилятор действительно может предположить, что в вашем коде нет неопределенного поведения - потому что либо оно истинно, либо оно ложно, и в этом случае что-то может случиться, а это значит, что все, что происходит из-за неправильного предположения компилятора, по-прежнему верный.

Кто-то утверждал, что когда 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; 
} 

Оценка р + 4 не определено поведение, если р указывает на [0], но если он указывает на б [0]. Поэтому компилятору разрешено считать, что p указывает на b [0]. Поэтому компилятору разрешено считать, что x! = 0, потому что x == 0 приводит к неопределенному поведению. Поэтому компилятору разрешено удалить проверку x == 0 в операторе return и просто вернуть 1000000/x. Это означает, что ваша программа вылетает, когда вы вызываете f (0) вместо возврата 0.

Другое предположение состояло в том, что если вы увеличиваете нулевой указатель и затем уменьшаете его снова, результат снова является нулевым указателем. Неправильно. Помимо возможности того, что приращение нулевого указателя может просто сработать на каком-то оборудовании, как насчет этого: поскольку приращение нулевого указателя не определено, компилятор проверяет, является ли указатель нулевым и только увеличивает указатель, если он не является нулевым указателем , поэтому p + 1 снова является нулевым указателем. И, как правило, он будет делать то же самое для декремента, но, будучи умным компилятором, замечает, что p + 1 всегда является неопределенным поведением, если результат был нулевым указателем, поэтому можно предположить, что p + 1 не является нулевым указателем, поэтому можно исключить проверку нулевого указателя. Это означает, что (p + 1) - 1 не является нулевым указателем, если p был нулевым указателем.

+1

Это, кажется, единственный ответ на странице, который действительно знает, что означает «неопределенное поведение». – Fattie

+4

Эта напыщенная речь даже не пытается адресовать эту вещь вверху - аххх - что она называется ... о да - *** вопрос ***, который *** не *** * ", как может быть неопределенным проявление поведения "*, но имеет ли код в вопросе неопределенное поведение или нет. (Справедливости ради, он в конечном итоге доходит до обсуждения, которое * предполагается, при добавлении нулевого указателя, которое не определено без указания или обоснования такого.) Этот ответ был бы лучше перенесен на соответствующий вопрос .... –

-1

Назад в удовольствие C дней, если p был указателем на что-то, p ++ эффективно добавлял размер p к значению указателя, чтобы сделать p точкой следующего. Если вы установите указатель p на 0, то разумно, что p ++ все равно укажет его на следующее, добавив к нему размер p.

Более того, вы можете делать что-то вроде добавления или вычитания чисел из p для перемещения по памяти (p + 4 будет указывать на четвертое что-то мимо p.) Это были хорошие времена, которые имели смысл. В зависимости от компилятора вы можете пойти в любом месте в своем пространстве памяти. Программы выполнялись быстро, даже на медленном оборудовании, потому что C просто сделал то, что вы ему сказали, и разбился, если вы слишком сумасшедшие/неряшливые.

Таким образом, реальный ответ заключается в том, что установка указателя на 0 хорошо определена и приращение указателя хорошо определено. Любые другие ограничения могут быть установлены разработчиками компилятора, разработчиками и разработчиками оборудования.

+2

Разве это не так? круглый? Стандарт C не определяет его, но поставщики компилятора могут определить его для конкретной платформы. – Kos

+0

Я просто помню, что сказал мой старый K & R, когда он говорил о указателях, что так они должны были работать. Если бы производители компилятора не работали интуитивно, я бы, вероятно, не использовал этот компилятор, если бы моя рука не была скручена грубо. :-) –

+1

OTOH, вы тогда не получали этот уровень оптимизации от компиляторов. Компромиссы, компромиссы ... – Kos

0

От ISO IEC 14882-2011 §5.2.6:

Значения выражения Постфиксного ++ является значением операнда. [Примечание: полученное значение является копией оригинального значения -end note] Операнд должен быть модифицируемым значением lvalue. Тип операнда должен быть арифметическим типом или указателем на полный тип объекта.

Поскольку nullptr является указателем на полный тип объекта. Поэтому я не понимаю, почему это было бы неопределенным поведением.

Как уже было сказано ранее в этом же документе также говорится в §5.2.6/1:

Если оба указателя операнд и точка результат для элементов одного и того же объекта массива, или один прошлое последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение не определено.

Это выражение кажется несколько неоднозначным. В моей интерпретации неопределенная часть вполне может быть оценкой объекта. И я думаю, что никто не согласился бы с этим. Однако для арифметики указателя требуется только полный объект.

Конечно, постфиксные [] операторы и вычитания или умножения на указатель на объекты массива только четко определены, если они фактически указывают на один и тот же массив. В основном важно, потому что у вас может возникнуть соблазн подумать, что 2 массива, определенные последовательно в 1 объекте, можно повторить, как если бы они были единым массивом.

Итак, мой вывод состоял бы в том, чтобы операция была четко определена, но оценки не было бы.

Смежные вопросы