Это связано с выравниванием данных. Большинство компиляторов будут выравнивать данные по адресным границам, что поможет с общей производительностью. Итак, в первом случае, структура с упакованным атрибутом, между char [3] и int есть дополнительный байт для выравнивания int на четырехбайтовой границе. В упакованной версии этот байт заполнения отсутствует.
byte : 0 1 2 3 4 5 6 7
st1 : opcode[0] opcode[1] opcode[2] padding |----int------|
st2 : opcode[0] opcode[1] opcode[2] |-------int--------|
Вы выделяете целое число без знака и передаете его в функцию:
byte : 0 1 2 3 4 5 6 7
alloc : |-----------int------------------| |---unallocated---|
st1 : opcode[0] opcode[1] opcode[2] padding |----int------|
st2 : opcode[0] opcode[1] opcode[2] |-------int--------|
Если вы используете систему с прямым порядком байтов, то младшие восемь битов (самые правые) сохраняются в байте 0 (0x33), байт 1 имеет 0x44, байт 2 имеет 0x33, а байт 4 имеет 0x11. В структуре st1 значение int отображается в память после конца выделенного количества, а в версии st2 младший байт int отображается в байт 4, 0x11. Таким образом, st1 выдает 0, а st2 выдает 0x11.
Вам повезло, что нераспределенная память равна нулю и у вас нет проверки диапазона памяти. Запись в int в st1 и st2 в этом случае может в худшем случае повредить память, вызвать ошибки защиты памяти или ничего не делать. Он не определен и зависит от реализации менеджера памяти во время выполнения.
В общем, избегайте void *
.