При работе со структурами стоит имеет представление о том, как структуры располагаются в памяти во время выполнения. От этого может зависеть размер программы/обрабатываемых данных а также ряд других аспектов. В частности, чтобы код, который мы компилируем, мог взаимодействовать с другим кодом, использующим структуры, важно располагать структуры точно так, как указано в соответствующих стандартах. Так, для Unix-подобных систем (в том числе для Linux) расположение структур частично определено стандартом C и частично стандартом System V ABI.
Поля структуры появляются в памяти в том же порядке, в котором они идут вобъявлении структуры. Первое поле имеет тот же адрес, что и структура в целом. Каждое последующее поле располагается после предыдущего с учетом выравнивания.
Выравнивание указывает, что поле должно располагаться по адресу, которое кратно (делится без остатка) на выравнивание. Для примитивных типов и указателей выравнивание соответствует размеру типа. Например, размер типа int - 4 байт, соответственно поле age должно быть находиться по адресу, который кратен 4. Размер типа char - 1 байт, поэтому его выравание равно 1 байт, а поле типа char может располагаться по любому адресу. Размер типа long - 8 байт, поэтому его выравнивание равно 8. Размер указателей занимает 8 байт на 64-разрядных системах и 4 байта на 32-разрядных архитектурах. Соответственно выравнивание указателей - 8 или 4 байта в зависимости от разрядности системы.
Рассмотрим следующий пример.
#include <stdio.h>
struct person {
int age;
char* name;
};
int main(void){
struct person tom = {.age = 40, .name ="Tom"};
// получаем размер структуры person
printf("sizeof(struct person): %lu\n", sizeof(struct person));
// получаем адрес переменной tom
printf("tom address: %p\n", &tom);
// получаем адрес поля age в переменной tom
printf("age address: %p\n", &tom.age);
// получаем адрес поля name в переменной tom
printf("name address: %p\n", &tom.name);
return 0;
}
Здесь определена структура person, а в функции main определена переменная этой структуры - переменная tom. Затем мы получаем размер структуры person, а также адреса самой переменной tom и ее полей. Консольный вывод программы, к примеру, может выглядеть следующим образом:
sizeof(struct person): 16 tom address: 0x7ffde016cdb0 age: 0x7ffde016cdb0 name address: 0x7ffde016cdb8
Здесь мы видим, что размер структуры person занимает 16 байт. Также мы видим, что адрес первого поля - поля age совпадает с адресом структуры, а поле name находится на 8 байт дальше от поля age. Схематически это можно выразить следующим образом:
Поле | age | отступ | name | ------------------------------------ Байты | 0-3 | 4-7 | 8-15 |
Итак, первым идет поле age. Это поле представляет тип int и поэтому занимает 4 байта. Однако следующее поле - name располагается не сразу после поля age, а через отступ в 4 байта. Почему? Потому что при размещении полей структуры учитывается выравнивание этих полей. Это значит, что поле должно располагаться по адресу, которое кратно (делится без остатка) на выравнивание. Для примитивных типов и указателей выравнивание соответствует размеру типа. Например, размер типа int - 4 байт, соответственно поле age должно быть находиться по адресу, который кратен 4.
Размер поля name и соответственно размер указателя - 8 байт на 64-разрядных системах и 4 байта на 32-разрядных архитектурах. Поэтому поле name должно располагаться по адресу, который кратен 8. Сразу после поля age на примере консольного вывода выше идет адрес 0x7ffde016cdb4, но этот адрес не кратен 8, поэтому добавляется необходимое смещение в 4 байта.
В итоге вся структура занимает 16 байт: размер поля age + отступ + размер поля name.
Выше уже упоминалось, что выравнивание примитивных типов и указателей соответствует их размеру. А что с выравниванием структуры? Согласно System V ABI, размер структуры должен быть кратен его выравниванию. ABI также утверждает, что структура принимает то же выравнивание, что и ее наибольшее поле. Так, в примере выше наибольшее поле структуры person - поле name занимает 8. Следовательно, вся структура должна быть выровнена по 8 байтов, а ее размер должен быть кратен 8. Рассмотрим следующий пример:
#include <stdio.h>
struct person {
char* name;
int age;
};
int main(void){
struct person tom = {.age = 40, .name ="Tom"};
// получаем размер структуры person
printf("sizeof(struct person): %lu\n", sizeof(struct person));
// получаем адрес переменной tom
printf("tom address: %p\n", &tom);
// получаем адрес поля name в переменной tom
printf("name address: %p\n", &tom.name);
// получаем адрес поля age в переменной tom
printf("age address: %p\n", &tom.age);
return 0;
}
В отличие от предыдущего примеры здесь я просто поменял местами поля name и age. То есть по идее у нас теперь не должно быть отступа между полями name и age. Так как, следующий за полем name адрес будет кратен 8 и соответственно и кратен 4, поэтому между полями name и age не будет никаких отступов. НО размер структуры person все равно будет 16 байт. К примеру, консольный вывод мог бы быть следующим:
sizeof(struct person): 16 tom address: 0x7ffd22dfd830 name address: 0x7ffd22dfd830 age address: 0x7ffd22dfd838
Поскольку согласно ABI размер структуры должен быть кратен выравниванию наибольшего поля, то здесь структура person имеет выравнивание 8, а размер 16. Но так как совокупный размер полей name и age равен 12 байт (8 байт поля name + 4 байта поля age), то в конце структуры добавляется отступ в 4 байта:
Поле | name | age | отступ | ------------------------------------- Байты | 0 -7 | 8-11 | 12-15 |
Если поле представляет массив, то для него используется выравнивание типа его элементов. Например:
#include <stdio.h>
struct example {
char field1;
int field2[2];
char field3[5];
};
int main(void){
struct example exmp;
printf("sizeof(struct example): %lu\n", sizeof(struct example));
printf("exmp address: %p\n", &exmp);
printf("field1 (char) address: %p\n", &exmp.field1);
printf("field2 (int[2]) address: %p\n", &exmp.field2);
printf("field3 (char[5]) address: %p\n", &exmp.field3);
return 0;
}
Здесь определена структура example, которая имеет три поля:
Поле field1 имеет тип char и поэтому занимает 1 байт и имеет выравнивание 1
Поле field2 представляет массив int[2], который занимает 8 байт. Поскольку тип элементов массива - int, то имеет выравнивание 4
Поле field3 имеет тип char[5], занимает 5 байт и имеет выравнивание 1 (так как хранит элементы типа char), соответственно для него не требуется никаких отступов
Поскольку адрес, который идет сразу же за первым полем field1, не кратен 4, то между полями field1 и field2 добавляется отступ в 3 байта.
Кроме того, поскольку поле с наибольшим выравниванием - field2 имеет выравнивание 4, то вся структура в целом имеет также выравнивание 4, поэтому ее размер должен быть кратен 4. Если мы сложим размер всех полей и отступ между полями field1 и field2, то мы получим:
1 (размер field1) + 3 (отступ между field1 и field1) + 8 (размер field2) + 5 (размер field3) = 17 байт
Однако размер структуры должен быть кратен 4, поэтому в конце к структуре добавляется еще 3 байта, а вся структура в целом занимает 20 байт.
Поле | field1 | отступ | field2 | field3 | отступ | ----------------------------------------------------------- Байты | 0 | 1-3 | 4-11 | 12-17 | 18-19 |
Возможный консольный вывод:
sizeof(struct example): 20 exmp address: 0x7ffe27226560 field1 (char) address: 0x7ffe27226560 field2 (int[2]) address: 0x7ffe27226564 field3 (char[5]) address: 0x7ffe2722656c
Зная, как поля структуры располагаются в памяти, мы можем применить это знание для оптимизации и уменьшить размер структуры. Например, возьмем следующую программу:
#include <stdio.h>
struct person{
int age;
char* name;
int salary;
};
int main(void){
struct person tom = {.age = 40, .name = "Tom", .salary = 125};
printf("sizeof(struct person): %lu\n", sizeof(struct person));
printf("age address: %p\n", &tom.age);
printf("name address: %p\n", &tom.name);
printf("salary address: %p\n", &tom.salary);
return 0;
}
Здесь расположение полей в структуре person выглядит следующим образом:
Поле | age | отступ | name | salary | отступ | --------------------------------------------------------- Байты | 0-3 | 4-7 | 8-15 | 16-19 | 20-23 |
Здесь поле с наибольшим выравниванием - name, выравнивание и размер которого равны 8. Соответственно вся структура имеет выравнивание 8 и размер, кратный 8 - 24 байта. Однако данное расположение не оптимально, поскольку мы имеет ненужный отступ между полями age и name, а также отступ после поля salary для обеспечения должного размера структуры. Но мы могли бы переупорядочить поля следующим образом:
#include <stdio.h>
struct person{
char* name;
int age;
int salary;
};
int main(void){
struct person tom = {.age = 40, .name = "Tom", .salary = 125};
printf("sizeof(struct person): %lu\n", sizeof(struct person));
printf("age address: %p\n", &tom.age);
printf("name address: %p\n", &tom.name);
printf("salary address: %p\n", &tom.salary);
return 0;
}
Теперь поле с наибольшим выравниванием располагается в самом начале, а расположение полей теперь выглядит следующим образом:
Поле | name | age | salary |
Байты | 0-7 | 8-11 | 12-15 |
Поле | name | age | salary | -------------------------------------- Байты | 0-7 | 8-11 | 12-15 |
В итоге общий размер структуры составит 16 байт.
Распространенный принцип определения полей структуры заключается в следующем: поля с наибольшим выравниваем располагаются в начале, а после них поля с меньшим выравниванием. Это позволяет более экономно распределить память и уменьшить размер структуры.