Расставим точки над структурами C/C++

в 19:28, , рубрики: c/c++, c++, struct, Программирование, С++, метки: , , ,

Недавно познакомился со структурами C/C++ — struct. Господи, да «что же с ними знакомиться» скажете вы? Тем самым вы допустите сразу 2 ошибки: во-первых я не Господи, а во вторых я тоже думал что структуры — они и в Африке структуры. А вот как оказалось и — нет. Я расскажу о нескольких жизненно-важных подробностях, которые кого-нибудь из читателей избавят от часовой отладки…

Расставим точки над структурами C/C++

Выравнивание полей в памяти

Обратите внимание на структуру:

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js