Является ли преобразование указателя корректным указателем на пустоту?
Предположим, что у меня есть некоторые структуры, определенные как:
struct foo { int a; };
struct bar { struct foo r; int b; };
struct baz { struct bar z; int c; };
Является ли стандарт C гарантией того, что следующий код строго соответствует?
struct baz x;
struct foo *p = (void *)&x;
assert(p == &x.z.r);
Мотивация для этой конструкции состоит в том, чтобы обеспечить согласованную идиому программирования для приведения к типу указателя, который, как известно, совместим.
Теперь, это то, что C говорит о том, как структуры и его исходные элементы конвертируются:
Внутри объекта структуры члены небитного поля и единицы, в которых расположены битовые поля, имеют адреса, которые увеличиваются в том порядке, в котором они объявлены. Указатель на объект структуры, соответствующим образом преобразованный, указывает на его первоначальный член (или если этот элемент является битовым полем, а затем блоку, в котором он находится) и наоборот. В структурном объекте может быть неназванное дополнение, но не в его начале.
C.11 & sect; 6.7.2.1 & para; 15
Это то, что он говорит о преобразованиях void
указателей:
Указатель на void
может быть преобразован в или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void
и обратно; результат сравнивается с исходным указателем.
C.11 & sect; 6.3.2.3 & para; 1
И вот что он говорит об конвертации между типами указателей объектов:
Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен 68) для ссылочного типа, поведение undefined. В противном случае, если преобразовать обратно, результат будет сравниваться с исходным указателем. 68) В общем, понятие "правильно выровнено" транзитивно: если указатель на тип A правильно выровнен для указатель на тип B, который, в свою очередь, правильно выровнен для указателя на тип C, тогда указатель на тип A правильно выровнен для указателя на тип C.
C.11 & sect; 6.3.2.3 & para; 7
Мое понимание из вышеизложенного заключается в том, что преобразование указателя объекта в указатель объекта другого типа с помощью преобразования void *
отлично. Но у меня есть комментарий, который предполагает иное.
Ответы
Ответ 1
Ваш пример строго соответствует.
Предложение 2 nd из раздела 6.7.2.1 и para 15 (указатель на
структурный объект, соответствующим образом преобразованный, указывает на его начальный член... и наоборот.) гарантирует следующие равенства:
(sruct bar *) &x == &(x.z)
(struct foo *) &(x.z) == &(x.z.r)
Поскольку вы находитесь в начале struct
, никакого дополнения не может быть, и мое понимание стандарта заключается в том, что адрес struct
и его первого элемента одинаковый.
Итак, struct foo *p = (void *) &x;
является правильным, как было бы struct foo *p = (struct foo *) &x;
В этом конкретном случае выравнивание гарантировано будет правильным в соответствии с пунктом 6.7.2.1 и пунктом 15. И всегда разрешается проходить через void *
, но это необязательно, потому что & sect; 6.3.2.3 и para.7 позволяет преобразовать указатели в разные объекты, при условии, что проблема выравнивания
И следует отметить, что в разделе & sect; 6.2.3.2 и пункте 7 также говорится: Когда указатель на объект преобразуется в указатель на тип символа,
результат указывает на младший адресный байт объекта, что означает, что все эти указатели фактически указывают на младший адресный байт x.r.z.a
. Таким образом, вы также можете передать указатели на char
, потому что у нас также есть:
(char *) &x == (char *) &(x.z) == (char *) &(x.z.r) == (char *) &(x.z.r.a)
Ответ 2
Чтобы завершить анализ, необходимо увидеть определение того, как определено равенство указателя:
... Если один операнд является указателем на тип объекта, а другой является указателем на квалифицированную или неквалифицированную версию void
, то первый преобразуется в тип последнего.
Два указателя сравнивают одинаковые, если и только если оба являются нулевыми указателями, оба являются указателями на один и тот же объект (включая указатель на объект и подобъект в начале) или функцию, оба являются указателями на один из последних элементов один и тот же объект массива, или один - указатель на один конец конца одного объекта массива, а другой - указатель на начало другого объекта массива, который происходит сразу после первого объекта массива в адресном пространстве.
C.11 & sect; 6.5.9 & para; 5-6
Итак, вот аргумент, что он хорошо определен:
(void *)&x == (void *)&x.z
§6.7.2.1¶15, §6.5.9¶5-6
(void *)&x.z == &x.z.r
§6.7.2.1¶15, §6.5.9¶5-6
(void *)&x == &x.z.r
транзитивное равенство
(struct foo *)(void *)&x == (void *)&x
§6.3.2.3¶1, §6.5.9¶5-6
(struct foo *)(void *)&x == &x.z.r
транзитивное равенство
Последний шаг выше - это суть инициализации p
и утверждение из кода в вопросе.
Ответ 3
IMHO да, кроме того, какие строгие интерпретации стандарта могут поставить под сомнение, выделение объектов в памяти следует тому же правилу в одном и том же компиляторе: укажите адрес, подходящий для любой переменной. Поскольку каждая структура начинается с первой переменной для транзитивного свойства, сама структура будет выровнена с адресом, который подходит для любой переменной. Последние закрывают сомнение в том, что разные структуры имеют разные адреса, никакие изменения не могут быть сделаны между преобразованиями, потому что определение адреса соответствует тем же правилам. Это, конечно, неверно для следующих полей структуры, которые могут быть непересекающимися, чтобы соответствовать требованиям выравнивания следующих полей.
Если вы работаете с первым элементом структуры, это гарантирует, что это то же самое, что и первое поле самой структуры.
Теперь взгляните на один из самых распространенных программных продуктов: независимая группа JPEG JPEGlib.
Все программное обеспечение, скомпилированное на многих процессорах и машинах, использует технику, которая напоминает структуры управления передачей С++, начало которых всегда одно и то же, но оно содержит много других и разных подструктур и полей между вызовами.
Этот код компилируется и работает на чем угодно: от игрушек до ПК до планшетов и т.д.
Ответ 4
Да, с точки зрения стандартного языка, ваш пример строго соответствует, таким образом, совершенно законным. Это в основном происходит из 2 предоставленных вами котировок (важно подчеркнуть). Первый:
Указатель на void может быть преобразован в или из указатель на любой тип объекта.
Это означает, что в вашем присваивании в коде мы имеем удачный перевод из указателя struct baz в указатель void и, после этого, удаляются из указателя void в struct из-за того, что оба указателя выравниваются одинаково. Если бы это было не так, у нас было бы поведение undefined из-за несоблюдения к 6.3.2.3, которое вы предоставили.
И второй:
68) В целом, понятие "правильно выровнено" транзитивно: если указатель на тип A правильно выровнен для указателя на тип B, который, в свою очередь, правильно выровнен для указателя на тип C, тогда указатель на тип A правильно выровнен для указателя на тип C.
И это важнее. Он не указывает (и не должен), что типы A и C должны быть одинаковыми, что, в свою очередь, позволяет им не делать этого. Единственным ограничением является сглаживание.
Это в значительной степени.
Конечно, однако, такие манипуляции опасны по очевидным причинам.