Что делает вызов memcpy
Вопрос плохо поставлен. Давайте сначала посмотрим на код:
char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
Сначала обратите внимание, что repr
инициализирован, поэтому всем элементам в нем присваиваются значения.
Первый memcpy
в порядке - он копирует байты из a
в repr
.
Если бы второй memcpy
был memcpy(&s, repr, sizeof repr);
, он скопировал бы байты из repr
в s
. Это приведет к записи байтов в s.a
и, из-за размера repr
, в любое заполнение между s.a
и s.b
. Согласно C 2018 6.5 7 и другим частям стандарта разрешен доступ к байтам объекта (а «доступ» означает как чтение, так и запись согласно 3.1 1). Таким образом, эта копия в s
в порядке, и в результате s.a
принимает то же значение, что и a
.
Однако memcpy
использует &(s.a)
, а не &s
. Он использует адрес s.a
, а не адрес s
. Мы знаем, что преобразование s.a
в указатель на тип символа позволит нам получить доступ к байтам s.a
(6,5 7 и более) (и передача его в memcpy
имеет тот же эффект, что и такое преобразование, поскольку memcpy
указывается, чтобы иметь эффект копирования байтов), но неясно, позволяет ли нам получить доступ к другим байтам в s
. Другими словами, у нас есть вопрос, можем ли мы использовать &s.a
для доступа к байтам, отличным от тех, что указаны в s.a
.
6.7.2.1 15 говорит нам, что если указатель на первый член структуры «соответствующим образом преобразован», результат указывает на структуру. Итак, если мы преобразовали &s.a
в указатель на struct first_member_padding_t
, он будет указывать на s
, и мы определенно можем использовать указатель на s
для доступа ко всем байтам в s
. Таким образом, это также будет хорошо определено:
memcpy((struct first_member_padding t *) &s.a, repr, sizeof repr);
Однако memcpy(&s.a, repr, sizeof repr);
преобразует только &s.a
в void *
(поскольку memcpy
объявлен как принимающий void *
, поэтому &s.a
автоматически преобразуется во время вызова функции), а не в указатель на тип структуры. Это подходящее преобразование? Обратите внимание, что если бы мы сделали memcpy(&s, repr, sizeof repr);
, он преобразовал бы &s
в void *
. 6.2.5 28 сообщает нам, что указатель на void
имеет то же представление, что и указатель на символьный тип. Итак, рассмотрим эти два утверждения:
memcpy(&s.a, repr, sizeof repr);
memcpy(&s, repr, sizeof repr);
Оба этих оператора передают void *
в memcpy
, и эти два void *
имеют то же представление, что и друг друга, и указывают на один и тот же байт. Теперь мы можем интерпретировать стандарт педантично и строго так, чтобы они отличались тем, что последний может использоваться для доступа ко всем байтам s
, а первый - нет. Тогда это странно, что у нас есть два обязательно идентичных указателя, которые ведут себя по-разному.
Такая строгая интерпретация стандарта C кажется возможной теоретически - разница между указателями могла возникнуть во время оптимизации, а не в фактической реализации memcpy
, - но я не знаю ни одного компилятора, который бы это сделал. Обратите внимание, что такая интерпретация противоречит разделу 6.2 стандарта, в котором говорится о типах и представлениях. Интерпретация стандарта таким образом, что (void *) &s.a
и (void *) &s
ведут себя по-разному, означает, что две вещи с одним и тем же значением и типом могут вести себя по-разному, что означает, что значение состоит из чего-то большего, чем его значение и тип, что, похоже, не является целью 6.2 или стандарт в целом.
Type-Punning
В вопросе говорится:
Я пытаюсь понять, как работает каламбур, когда дело доходит до сохранения значения в члене структуры или объединения.
Это не каламбур, как обычно используется этот термин. Технически код обращается к s.a
, используя lvalue другого типа, чем его определение (потому что он использует memcpy
, который определен для копирования, как если бы с символьным типом, в то время как определенный тип - int
), но байты происходят из int
и являются копируется без изменений, и такое копирование байтов объекта обычно рассматривается как механическая процедура; это делается для создания копии, а не для переинтерпретации байтов в новый тип. «Типаж» обычно относится к использованию разных lvalue с целью переинтерпретации значения, например записи unsigned int
и чтения float
.
В любом случае, каламбур на самом деле не является предметом вопроса.
Ценности в членах
Заголовок спрашивает:
Какие значения мы можем хранить в членах структуры или объединения?
Этот заголовок не соответствует содержанию вопроса. На вопрос, связанный с заголовком, легко ответить: значения, которые мы можем сохранить в члене, - это те значения, которые может представлять тип члена. Но вопрос идет дальше, чтобы исследовать отступы между участниками. Заполнение не влияет на значения в элементах.
Padding принимает неуказанные значения
Вопрос цитирует стандарт:
Когда значение хранится в объекте типа структуры или объединения, в том числе в объекте-члене, байты представления объекта, соответствующие любым байтам заполнения, принимают неопределенные значения.
и говорит:
Поэтому я интерпретировал это так, как будто у нас есть объект для хранения в члене, так что размер объекта равен sizeof(declared_type_of_the_member) + padding
байты, связанные с заполнением, будут иметь неопределенное значение ...
Цитируемый текст в стандарте означает, что если байты заполнения в s
были установлены на некоторые значения, как в случае с memcpy
, а затем мы выполняем s.a = something;
, то байты заполнения больше не требуются для хранения своих предыдущих значений.
Код в вопросе исследует другую ситуацию. Код memcpy(&(s.a), repr, sizeof(repr));
не сохраняет значение в элементе структуры в смысле, обозначенном в 6.2.6.1 6. Он не сохраняется ни в одном из элементов s.a
или s.b
. Это копирование байтов в, что отличается от того, что обсуждается в 6.2.6.1.
6.2.6.1 6 означает, что, например, если мы выполним этот код:
char repr[sizeof s] = { 0 };
memcpy(&s, repr, sizeof s); // Set all the bytes of s to known values.
s.a = 0; // Store a value in a member.
memcpy(repr, &s, sizeof s); // Get all the bytes of s to examine them.
for (size_t i = sizeof s.a; i < offsetof(struct first_member_padding_t, b); ++i)
printf("Byte %zu = %d.\n", i, repr[i]);
тогда не обязательно, чтобы все нули были напечатаны - байты в заполнении могли измениться.
person
Eric Postpischil
schedule
11.03.2019
s.a
в 10. Норма явно указывает, где добавляется отступ (извините, слишком ленив, чтобы проверить ^^)? - person bruno   schedule 11.03.2019repr[offsetof(struct first_member_padded_t, b)]
- person Some Name   schedule 11.03.2019s.a
иs.b
есть отступы, вы изменяете память за пределамиs.a
с помощью указателя, производного отs.a
. Учитывая, насколько хитрые правила в отношении них, этот код вызывает проблемы. - person user694733   schedule 11.03.2019you are modifying memory outside of s.a
Я сделал это намеренно, чтобы проверить процитированное мной правило. Я ожидал, что байты заполнения будут игнорироваться или принимать неопределенное значение. - person Some Name   schedule 11.03.2019s.a
, который не определен. - person Eugene Sh.   schedule 11.03.2019s.a
не равно 10). Ваш первый memcpy установил 4 первых байта, выделенных для поля a, следующие 4 байта заполнения - undet, вторая копия memspy из смещение поля a 4 байта со значением 10, затем следующие 4 байта с неопределенным значением, затем доступ printf к полю a, вы никогда не читайте отступы в s - person bruno   schedule 11.03.2019struct
. Указатель наstruct
может быть преобразован в указатель на его первый член. - person alx   schedule 11.03.2019int
, что даетmemcpy
выход за его пределы. - person Some Name   schedule 11.03.2019int f1; int f2; int a; long b
, чтобы иметь заполнение, пока поле не является первым. Конечно, самый простой способ для компилятора - добавить заполнение непосредственно перед требующим его полем, но является ли это нормой? - person bruno   schedule 11.03.2019memcpy
записывает за пределыs.a
, он находится в пределахs
, а стандарт C разрешает доступ к байтам объекта (которым являетсяs
) в виде массива символьного типа, в том числе черезmemcpy
. - person Eric Postpischil   schedule 11.03.2019int
для хранения значения объекта, представление которого превышаетsizeof(int)
. Не вызывает ли это неопределенного поведения? - person Some Name   schedule 11.03.2019int *
не использовался для хранения значения.int *
использовался в выражении, которое было аргументом дляmemcpy
, но семантика вызовов функций заставляет его преобразовывать вvoid *
. Затем, согласно спецификацииmemcpy
, он копирует символы из одного места в другое. Есть некоторая педантичная сложность в том, можно ли использоватьint *
, равный&s.a
, для доступа к другим байтамs
. Я готовлю ответ, который может решить эту проблему. - person Eric Postpischil   schedule 11.03.2019memset(&s, 0, sizeof s)
действительно. Так что я сомневаюсь, чтоmemset(&s.a, 0, sizeof s)
- это UB. - person 4386427   schedule 11.03.2019memcpy
astruct
, это должно быть сделано с помощью его указателя&s
, а не указателя на член, насколько мне известно. Указатели очень разборчивы. Я это проверю. - person alx   schedule 11.03.2019sizeof s
Я сомневаюсь, что это может быть UB .... но я не могу процитировать стандарт ... и я могу ошибаться, конечно :-) - person 4386427   schedule 11.03.2019s.a
меньше, ноs
больше. Макет - это int (4 байта), заполнение (4 байта), long (? Байта), поскольку OP не сообщил нам о размере long. Но sizeofs
составляет не менее 12 (вероятно, 16 байтов), а код копирует только 8 байтов. - person 4386427   schedule 11.03.2019memcpy(&(s.a), repr, sizeof(repr));
. Здесьrepr
- размерs.a
плюс отступs.a
. Итак,sizeof(repr)
большеsizeof(s.a)
. Итак, я предполагаю, что вопрос юриста здесь в том, законно ли писать за пределами объекта, который, как известно, является частью более крупного объекта. - person Eugene Sh.   schedule 11.03.2019sizeof(repr)
меньшеsizeof(s)
, поэтому код не записывает сторонуs
. - person 4386427   schedule 11.03.2019