Разница в указателях между членами структуры?
Стандарт C99 гласит, что:
Когда вычтены два указателя, оба должны указывать на элементы одного и того же объекта массива или один за последним элементом последнего объекта массива.
Рассмотрим следующий код:
struct test {
int x[5];
char something;
short y[5];
};
...
struct test s = { ... };
char *p = (char *) s.x;
char *q = (char *) s.y;
printf("%td\n", q - p);
Это явно нарушает вышеуказанное правило, так как p
а также q
указатели указывают на разные "объекты массива", и, согласно правилу, q - p
Разница не определена.
Но на практике, почему такая вещь должна приводить к неопределенному поведению? В конце концов, элементы структуры располагаются последовательно (как и элементы массива), с любым потенциальным заполнением между элементами. Правда, величина заполнения будет варьироваться в зависимости от реализации, и это повлияет на результат вычислений, но почему этот результат должен быть "неопределенным"?
Мой вопрос заключается в том, можем ли мы предположить, что стандарт просто "неосведомлен" в этом вопросе, или есть веская причина не расширять это правило? Разве вышеприведенное правило не может быть перефразировано так: "оба должны указывать на элементы одного и того же объекта массива или члены одной и той же структуры"?
Мое единственное подозрение - это сегментированные архитектуры памяти, в которых члены могут оказаться в разных сегментах. Это тот случай?
Я также подозреваю, что это причина, почему GCC определяет свои собственные __builtin_offsetof
для того, чтобы иметь "соответствие стандартам" определение offsetof
макро.
РЕДАКТИРОВАТЬ:
Как уже указывалось, арифметика на пустых указателях не допускается стандартом. Это расширение GNU, которое выдает предупреждение только при прохождении GCC -std=c99 -pedantic
, Я заменяю void *
указатели с char *
указатели.
5 ответов
Вычитание и реляционные операторы (по типу char*
) между адресами членов одной и той же структуры четко определены.
Любой объект может рассматриваться как массив unsigned char
,
Цитирую N1570 6.2.6.1 пункт 4:
Значения, хранящиеся в объектах без битового поля любого другого типа объекта, состоят из n ×
CHAR_BIT
биты, где n - размер объекта этого типа в байтах. Значение может быть скопировано в объект типаunsigned char [
N]
(например,memcpy
); результирующий набор байтов называется объектным представлением значения.
...
Мое единственное подозрение - это сегментированные архитектуры памяти, в которых члены могут оказаться в разных сегментах. Это тот случай?
Нет. Для системы с архитектурой сегментированной памяти компилятор обычно накладывает ограничение, согласно которому каждый объект должен помещаться в один сегмент. Или он может разрешить объекты, которые занимают несколько сегментов, но он все еще должен гарантировать, что арифметика указателей и сравнения работают правильно.
Арифметика указателей требует, чтобы два указателя были добавлены или вычтены, чтобы быть частью одного и того же объекта, потому что иначе это не имеет смысла. Указанный раздел стандарта конкретно относится к двум не связанным объектам, таким как int a[b];
а также int b[5]
, Арифметика указателя требует знать тип объекта, на который указывают указатели (я уверен, что вы уже знаете об этом).
т.е.
int a[5];
int *p = &a[1]+1;
Вот p
рассчитывается, зная, что &a[1]
относится к int
объект и, следовательно, увеличивается до 4 байтов (при условии sizeof(int)
это 4).
Что касается примера структуры, я не думаю, что он может быть определен таким образом, чтобы сделать арифметику указателей между членами структуры легальной.
Давайте возьмем пример,
struct test {
int x[5];
char something;
short y[5];
};
Арифметика указателя не допускается с void
указатели по стандарту C (Сборка с gcc -Wall -pedantic test.c
поймал бы это). Я думаю, что вы используете GCC, который предполагает void*
похож на char*
и позволяет это. Так,
printf("%zu\n", q - p);
эквивалентно
printf("%zu", (char*)q - (char*)p);
арифметика указателя хорошо определена, если указатели указывают на один и тот же объект и являются символьными указателями (char*
или же unsigned char*
).
Используя правильные типы, это будет:
struct test s = { ... };
int *p = s.x;
short *q = s.y;
printf("%td\n", q - p);
Теперь, как можно q-p
быть выполненным? основанный на sizeof(int)
или же sizeof(short)
? Как можно размер char something;
что в середине этих двух массивов будет рассчитано?
Это должно объяснить, что невозможно выполнять арифметику указателей для объектов разных типов.
Даже если все члены имеют одинаковый тип (таким образом, нет проблемы с типом, как указано выше), тогда лучше использовать стандартный макрос offsetof
(от <stddef.h>
) чтобы получить разницу между членами структуры, которая имеет эффект, аналогичный арифметике указателей между членами:
printf("%zu\n", offsetof(struct test, y) - offsetof(struct test, x));
Поэтому я не вижу необходимости определять арифметику указателей между членами структуры стандартом Си.
Я считаю, что ответ на этот вопрос проще, чем кажется, ОП спрашивает:
но почему этот результат должен быть "неопределенным"?
Что ж, давайте посмотрим, что определение неопределенного поведения находится в разделе проекта стандарта C99 3.4.3
:
поведение при использовании непереносимой или ошибочной программной конструкции или ошибочных данных, для которых настоящий международный стандарт не предъявляет никаких требований
это просто поведение, для которого стандарт не предъявляет требования, которое идеально соответствует этой ситуации, результаты будут различаться в зависимости от архитектуры, и попытка указать результаты, вероятно, была бы трудной, если не невозможной, переносимым способом. Это оставляет вопрос, почему они выбирают неопределенное поведение, а не, скажем, реализацию неопределенного поведения?
Скорее всего, было сделано неопределенное поведение, чтобы ограничить количество способов создания недопустимого указателя, это согласуется с тем фактом, что нам предоставляют offsetof
убрать единственную потенциальную потребность в вычитании указателя из несвязанных объектов.
Хотя стандарт на самом деле не определяет термин недействительный указатель, мы получили хорошее описание в Обосновании международного стандарта - Языки программирования - C, который в разделе 6.3.2.3
Указатели говорят (выделение мое):
В Стандарте подразумевается понятие недействительных указателей. При обсуждении указателей Стандарт обычно ссылается на "указатель на объект" или "указатель на функцию" или "нулевой указатель". Особый случай в адресной арифметике позволяет указателю находиться сразу за концом массива. Любой другой указатель недействителен.
Обоснование C99 далее добавляет:
Независимо от того, как создается недопустимый указатель, любое его использование приводит к неопределенному поведению. Даже присваивание, сравнение с константой нулевого указателя или сравнение с самим собой в некоторых системах может привести к исключению.
Это настоятельно говорит нам о том, что указатель на заполнение будет недопустимым указателем, хотя трудно доказать, что заполнение не является объектом, определение объекта говорит:
область хранения данных в среде исполнения, содержимое которой может представлять значения
и примечания:
При ссылке объект может интерпретироваться как имеющий определенный тип; см. 6.3.2.1.
Я не понимаю, как мы можем рассуждать о типе или значении заполнения между элементами структуры, и поэтому они не являются объектами или, по крайней мере, явно указывают на то, что заполнение не должно рассматриваться как объект.
Да, вам разрешено выполнять арифметику указателя на байтах структуры:
N1570 - 6.3.2.3. Указатели p7:
... Когда указатель на объект преобразуется в указатель на тип символа, результат указывает на младший адресуемый байт объекта. Последовательные приращения результата, вплоть до размера объекта, дают указатели на оставшиеся байты объекта.
Это означает, что для программиста байты структуры должны рассматриваться как непрерывная область, независимо от того, как она могла быть реализована в оборудовании.
Не с void*
указатели, тем не менее, это нестандартное расширение компилятора. Как указано в абзаце стандарта, он применяется только к указателям на символьные типы.
Редактировать:
Как указал в комментариях мафсо, вышеизложенное верно только до тех пор, пока тип результата вычитания ptrdiff_t
, имеет достаточный диапазон для результата. С ассортиментом size_t
может быть больше чем ptrdiff_t
и, если структура достаточно велика, возможно, что адреса расположены слишком далеко друг от друга.
Из-за этого предпочтительнее использовать offsetof
макрос на элементы структуры и рассчитать результат из тех.
Я должен указать на следующее:
из стандарта C99, раздел 6.7.2.1:
Внутри объекта структуры члены без битовых полей и блоки, в которых находятся битовые поля, имеют адреса, которые увеличиваются в порядке их объявления. Указатель на объект структуры, соответствующим образом преобразованный, указывает на его начальный элемент (или, если этот элемент является битовым полем, то на модуль, в котором он находится), и наоборот. Внутри объекта структуры может быть безымянный отступ, но не в его начале.
Мало того, что результат вычитания указателя между членами не определен настолько, насколько он ненадежен (то есть не гарантируется, что он будет одинаковым в разных экземплярах одного и того же типа структуры, когда применяется одна и та же арифметика).