Недавно познакомился со структурами C/C++ — struct. Господи, да «что же с ними знакомиться» скажете вы? Тем самым вы допустите сразу 2 ошибки: во-первых я не Господи, а во вторых я тоже думал что структуры — они и в Африке структуры. А вот как оказалось и — нет. Я расскажу о нескольких жизненно-важных подробностях, которые кого-нибудь из читателей избавят от часовой отладки…
Выравнивание полей в памяти
Обратите внимание на структуру:
struct Foo
{
char ch;
int value;
};
Ну во-первых какой у этой структуры размер в памяти? sizeof(Foo)
?
Размер этой структуры в памяти зависит от настроек компилятора и от директив в вашем коде…
В большинстве случаев (или просто предположим что сегодня это так) выравнивание размера структуры в памяти составляет 4 байта. Таким образом, sizeof(Foo) == 8
. Где и как прилепятся лишние 3 байта? Если вы не знаете — ни за что не угадаете…
- 1 байт: ch
- 2 байт: пусто
- 3 байт: пусто
- 4 байт: пусто
- 5 байт: value[0]
- 6 байт: value[1]
- 7 байт: value[3]
- 8 байт: value[4]
Посмотрим теперь размещение в памяти следующей структуры:
struct Foo
{
char ch;
short id;
int value;
};
Оно выглядит вот так:
- 1 байт: ch
- 2 байт: id[0]
- 3 байт: id[1]
- 4 байт: пусто
- 5 байт: value[0]
- 6 байт: value[1]
- 7 байт: value[3]
- 8 байт: value[4]
То есть точто можно впихнуть до выравнивания по 4 байта — впихивается на ура, добавим ещё одно поле:
struct Foo
{
char ch;
short id;
short opt;
int value;
};
Посмотрим на размещение полей в памяти:
- 1 байт: ch
- 2 байт: id[0]
- 3 байт: id[1]
- 4 байт: opt[0]
- 5 байт: opt[1]
- 6 байт: пусто
- 7 байт: пусто
- 8 байт: пусто
- 9 байт: value[0]
- 10 байт: value[1]
- 11 байт: value[3]
- 12 байт: value[4]
Всё это ой как печально, но есть способ бороться с этим прямо из кода:
#pragma pack(push, 1)
struct Foo
{
// ...
};
#pragma pack(pop)
Мы установили размер выравнивания в 1 байт, описали структуру и вернули предыдущую настройку. Возвращать предыдущую настройку — категорически рекомендую. Иначе всё может закончиться очень плачевно. У меня один раз такое было — падало Qt. Где-то заинклюдил их .h-ник ниже своего .h-ника…
Битовые поля
Мне становится не то что неспокойно на душе, а вообще становится хреново, когда я вижу в коде заполнение битовых полей при помощи масок и сдвигов, например так:
unsigned field = 0x00530000;
// ...
field &= 0xFFFF00FF;
field |= (id) << 8;
// ...
field &= 0xFFFFFF83;
field |= (proto) << 2;
Всё это пахнет такой печалью и такими ошибками и их отладкой, что у меня сразу же начинается мигрень! И тут из-за кулис выходят они — Битовые Поля. Что самое удивительное — были они ещё в языке C, но кого ни спрашиваю — все в первый раз о них слышат. Этот беспредел надо исправлять. Теперь буду давать им всем ссылку, ну или хотя бы ссылку на эту статью.
Как вам такой кусок кода:
#pragma pack(push,1)
struct IpHeader
{
uint8_t version:4;
uint8_t header_length:4;
uint8_t type_of_service;
uint16_t total_length;
uint16_t identificator;
// Flags
uint8_t _reserved:1;
uint8_t dont_fragment:1;
uint8_t more_fragments:1;
uint8_t fragment_offset_part1:5;
uint8_t fragment_offset_part2;
uint8_t time_to_live;
uint8_t protocol;
uint16_t checksum;
// ...
};
#pragma pack(pop)
А дальше в коде мы можем работать с полями как и всегда работаем с полями в C/C++. Всю работу по сдвигам и т.д. берет на себя компилятор. Конечно же есть некоторые ограничения… Когда вы перечисляете несколько битовых полей подряд, относящихся к одному физическому полю (я имею ввиду тип который стоит слева от имени битового поля) — указывайте имена для всех битов до конца поля, иначе доступа к этим битам у вас не будет, иными словами кодом:
#pragma pack(push,1)
stuct MyBitStruct
{
uint16_t a:4;
uint16_t b:4;
uint16_t c;
};
#pragma pack(pop)
Получилась структура на 4 байта! Две половины первого байта — это поля a
и b
. Второй байт не доступен по имени и последние 2 байта доступны по имени c
. Это очень опасный момент. После того как описали структуру с битовыми полями обязательно проверьте её sizeof
!
Порядок байтов
Меня также печалят в коде вызовы функций htons()
, ntohs()
, htonl()
, nthol()
в коде на C++. На C это ещё допустимо, но не на С++. С этим я никогда не смирюсь! Внимание всё нижесказанное относится к C++!
Ну тут я буду краток. Я в одной из своих предыдущих статей уже писал что нужно делать с порядками байтов. Есть возможность описать структуры, которые внешне работают как числа, а внутри сами определяют порядок хранения в байтах. Таким образом наша структура IP-заголовка будет выглядеть так:
#pragma pack(push,1)
struct IpHeader
{
uint8_t version:4;
uint8_t header_length:4;
uint8_t type_of_service;
u16be total_length;
u16be identificator;
// Flags
uint8_t _reserved:1;
uint8_t dont_fragment:1;
uint8_t more_fragments:1;
uint8_t fragment_offset_part1:5;
uint8_t fragment_offset_part2;
uint8_t time_to_live;
uint8_t protocol;
u16be checksum;
// ...
};
#pragma pack(pop)
Внимание собственно обращать на типы 2-хбайтовых полей — u16be
. Теперь поля структуры не нуждаются ни в каких преобразованиях порядка байт. Остаются проблемы с fragment_offset
, ну а у кого их нет — проблем-то. Тем не менее тоже можно придумать шаблон, прячущий это безобразие, один раз его оттестировать и смело использовать во всём своём коде.
«Язык С++ достаточно сложен, чтобы позволить нам писать писать на нём просто» © Как ни странно — Я
З.Ы. Планирую в одной из следующих статей выложить идеальные, с моей точки зрения, структуры для работы с заголовками протоколов стека TCP/IP. Отговорите — пока не поздно!
Автор: k06a