Записи структуры множества битовые поля. Битовые поля
В языке С++ есть возможность задавать элементам структур определённое количество памяти в битах. Например, если необходимо создать структуру данных, соответствующую размеру регистра в каком-либо устройстве. Типом элемента (его называют битовым полем) такой структуры может быть целочисленное (чаще всего типа unsigned ) или перечислимое (enum ) .
Синтаксически битовое поле в структуре определяется следующим образом:
Например:
Битовые поля в С++
struct fieldbite { unsigned short first: 2; unsigned short second: 2; unsigned short third: 4; } field;
struct fieldbite unsigned short first : 2 ; unsigned short second : 2 ; unsigned short third : 4 ; } field ; |
Мы определили структуру, в которой переменные будут занимать указанное количество бит. 2 + 2 + 4 дает 8 бит (мы выровняли до размера байта). Если в эту структуру дописать еще unsigned short fifth : 5 ; – уже будет задействовано 2 байта. Во втором байте естественно будет мусор 8 – 5 = 3 бита, которые будут невостребованными.
В отличии от объединений (union) размер битовых полей варьируется, в зависимости от того, сколько бит программист заказал. Если заказано 7 бит (скажем две переменные по 3 бита, и одна – 1 бит), то С++ отведет один байт (8 бит) под эти три переменные.
Если программист закажет 11 бит, то С++ отведет два байта (16 бит). Причем во втором байте будут задействованы только 5 бит, а остальные скорее всего будут, как бесполезный хвост. Поэтому при описании битовых полей следует учитывать такое “выравнивание” до байта. Т.е. распределять в нем переменные так, чтоб каждый бит был востребован. Для выравнивания занимаемой памяти можно использовать неименованные битовые поля.
Приведем еще один короткий пример, в котором битовые поля отводятся под дату и время для демонстрации этой технологии.
Битовые поля в C++
#include
#include using namespace std ; struct DateTime //Опишем структуру с битовыми полями unsigned short Day : 5 ; //5 бит для дня unsigned short Month : 4 ; //4 для месяца unsigned short Year : 7 ; //7 для года от 0 до 99 unsigned short Hour : 5 ; //5 бит для 24-х часов unsigned short Minute : 6 ; //6 для минут unsigned short Second : 6 ; //6 для секунд int main () DateTime d ; //объявляем переменную этого типа с битовыми полями int i ; //И еще одну, в которую будет поступать ввод данных //Введем дату cout << "Input day (1-31):" << "\t" ; cin >> i ; d . Day = i ; cout << "Input month (1-12):" << "\t" ; cin >> i ; d . Month = i ; cout << "Input Year (00-99) :" << "\t" ; cin >> i ; d . Year = i ; //Введем время cout << endl << "Input Hour (0-24):" << "\t" ; cin >> i ; d . Hour = i ; cout << "Input Minute (0-60):" << "\t" ; cin >> i ; d . Minute = i ; cout << "Input Seconds (0-60):" << "\t" ; cin >> i ; d . Second = i ; |
В противоположность другим компьютерным языкам С имеет возможность, называемую битовыми полями, позволяющую работать с отдельными битами. Битовые поля полезны по нескольким причинам. Ниже приведены три из них:
- Если ограничено место для хранения информации, можно сохранить несколько логических (истина/ложь) переменных в одном байте.
- Некоторые интерфейсы устройств передают информацию, закодировав биты в один байт.
- Некоторым процедурам кодирования необходимо получить доступ к отдельным битам в байте.
Хотя все эти функции могут выполняться с помощью битовых операторов, битовые поля могут внести большую ясность в программу.
Метод использования битовых полей для доступа к битам основан на структурах. Битовое поле, на самом деле, - это просто особый тип структуры, определяющей, какую длину имеет каждый член. Стандартный вид объявления битовых полей следующий:
struct имя структуры {
тип имя1: длина;
тип имя2: длина;
...
тип имяN: длина;
}
Битовые поля должны объявляться как int, unsigned или signed. Битовые поля длиной 1 должны объявляться как unsigned, поскольку 1 бит не может иметь знака. Битовые поля могут иметь длину от 1 до16 бит для 16-битных сред и от 1 до 32 бит для 32-битных сред. В Borland С++ самый левый бит является знаковым.
Рассмотрим приведенное ниже определение структуры:
Struct device {
unsigned active: 1;
unsigned ready: 1;
unsigned xmt_error: 1;
} dev_code;
Данная структура определяет три переменные по одному биту каждая. Структурная переменная dev_code может, например, использоваться для декодирования информации из порта ленточного накопителя. Для такого гипотетического ленточного накопителя следующий фрагмент кода записывает байт информации на ленту и проверяет на ошибки, используя dev_code:
Void wr_tape(char с)
{
while(!dev_code.ready) rd(&dev_code); /* ждать */
wr_to__tape (с); /* запись байта */
while(dev_code.active) rd(&dev_code); /* ожидание окончания записи информации */
if(dev_code.xmt error) printf("Write Error");
}
Здесь rd() возвращает статус ленточного накопителя wr_to_tape(), записывает данные. Рисунок показывает, как выглядит переменная dev_code в памяти.
Как можно видеть из предыдущего примера, к каждому полю происходит обращение с помощью оператора "точка". Тем не менее если обращение к структуре происходит с помощью указателя, то следует использовать оператор ->.
Нет необходимости обзывать каждое битовое поле. К полю, имеющему название, легче получить доступ. Например, если ленточный накопитель возвращает информацию, о наступлении конца ленты в пятом бите, следует изменить структуру device следующим образом:
Struct device {
unsigned active: 1;
unsigned ready: 1;
unsigned xmt_error: 1;
unsigned: 2;
unsigned EOT: 1;
} dev_code;
Битовые поля имеют некоторые ограничения. Нельзя получить адрес переменной битового поля. Переменные битового поля не могут помещаться в массив. Переходя с компьютера на компьютер нельзя быть уверенным в порядке изменения битов (слева направо или справа налево). Любой код, использующий битовые поля, зависит от компьютера.
Наконец, можно смешивать различные структурные переменные в битовых полях. Например:
Struct emp {
struct addr address;
float pay;
unsigned lay_off:1;
unsigned hourly:1;
unsigned deductions:3;
};
Определяет запись служащего, использующую только один байт для хранения трех частей информации - статуса служащего, получил ли он зарплату и размер удержаний. Без использования битовых полей данная информация заняла бы три байта.
Недавно познакомился со структурами C/C++ - struct. Господи, да «что же с ними знакомиться» скажете вы? Тем самым вы допустите сразу 2 ошибки: во-первых я не Господи, а во вторых я тоже думал что структуры - они и в Африке структуры. А вот как оказалось и - нет. Я расскажу о нескольких жизненно-важных подробностях, которые кого-нибудь из читателей избавят от часовой отладки…
Выравнивание полей в памяти
Обратите внимание на структуру:Struct Foo
{
char ch;
int value;
};
Ну во-первых какой у этой структуры размер в памяти? sizeof(Foo) ?
Размер этой структуры в памяти зависит от настроек компилятора и от директив в вашем коде…
В общем выравниваются в памяти поля по границе кратной своему же размеру. То есть 1-байтовые поля не выравниваются, 2-байтовые - выравниваются на чётные позиции, 4-байтовые - на позиции кратные четырём и т.д. В большинстве случаев (или просто предположим что сегодня это так) выравнивание размера структуры в памяти составляет 4 байта. Таким образом, sizeof(Foo) == 8 . Где и как прилепятся лишние 3 байта? Если вы не знаете - ни за что не угадаете…
- 1 байт: ch
- 2 байт: пусто
- 3 байт: пусто
- 4 байт: пусто
- 5 байт: value
- 6 байт: value
- 7 байт: value
- 8 байт: value
Struct Foo
{
char ch;
short id;
int value;
};
Оно выглядит вот так:
- 1 байт: ch
- 2 байт: пусто
- 3 байт: id
- 4 байт: id
- 5 байт: value
- 6 байт: value
- 7 байт: value
- 8 байт: value
Struct Foo
{
char ch;
short id;
short opt;
int value;
};
Посмотрим на размещение полей в памяти:
- 1 байт: ch
- 2 байт: пусто
- 3 байт: id
- 4 байт: id
- 5 байт: opt
- 6 байт: opt
- 7 байт: пусто
- 8 байт: пусто
- 9 байт: value
- 10 байт: value
- 11 байт: value
- 12 байт: value
#pragma pack(push, 1)
struct Foo
{
// ...
};
#pragma pack(pop)
Мы установили размер выравнивания в 1 байт, описали структуру и вернули предыдущую настройку. Возвращать предыдущую настройку - категорически рекомендую. Иначе всё может закончиться очень плачевно. У меня один раз такое было - падало Qt. Где-то заинклюдил их.h-ник ниже своего.h-ника…
Битовые поля
В комментариях мне указали на то, что битовые поля в структурах по стандарту являются «implementation defined» - потому их использования лучше избежать, но для меня соблазн слишком велик...Мне становится не то что неспокойно на душе, а вообще становится хреново, когда я вижу в коде заполнение битовых полей при помощи масок и сдвигов, например так:
Unsigned field = 0x00530000;
// ...
field &= 0xFFFF00FF;
field |= (id) << 8;
// ...
field &= 0xFFFFFF83;
field |= (proto) << 2;
Всё это пахнет такой печалью и такими ошибками и их отладкой, что у меня сразу же начинается мигрень! И тут из-за кулис выходят они - Битовые Поля. Что самое удивительное - были они ещё в языке C, но кого ни спрашиваю - все в первый раз о них слышат. Этот беспредел надо исправлять. Теперь буду давать им всем ссылку, ну или хотя бы ссылку на эту статью.
Как вам такой кусок кода:
#pragma pack(push,1)
struct IpHeader
{
uint8_t header_length:4;
uint8_t version: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 !
Также порядок размещения битовых болей в байте зависит от порядка байтов. При порядке LITTLE_ENDIAN битовые поля раздаются начиная со первых байтов, при BIG_ENDIAN - наоборот…
Порядок байтов
Меня также печалят в коде вызовы функций htons() , ntohs() , htonl() , nthol() в коде на C++. На C это ещё допустимо, но не на С++. С этим я никогда не смирюсь! Внимание всё нижесказанное относится к C++!Ну тут я буду краток. Я в одной из своих предыдущих статей уже писал что нужно делать с порядками байтов. Есть возможность описать структуры, которые внешне работают как числа, а внутри сами определяют порядок хранения в байтах. Таким образом наша структура IP-заголовка будет выглядеть так:
#pragma pack(push,1)
struct IpHeader
{
uint8_t header_length:4;
uint8_t version: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. Отговорите - пока не поздно!
Как вы уже наверное догадались, битовое поле - это просто массив битов фиксированного размера. Битовые поля удобно использовать для решения задач связанных с булевой логикой. Например, представление цветов или шифрование данных, ну или просто перевод чисел в двоичную систему исчисления, все это так или иначе связано с булевой логикой, а значит легко реализуемо с помощью битовых полей. В С++ для реализации булевой логики, раньше использовались побитовые операции & , | , ~ и тип данных int , но это было неэффективно, по нескольким причинам: неоправданный расход памяти для хранения одного бита и конечно же не всегда удобно использовать поразрядные логические операции.
Итак, чтобы воспользоваться классом bitset , достаточно подключить заголовочный файл
#include
Класс bitset это шаблон, который принимает один параметр - размер поля в битах. Отсюда следует запомнить, что любой объект типа bitset всегда имеет фиксированную длину. Вот пример:
Bitset<8> number(34);
В этом примере объявлено битовое поле - number размером 8 бит, которое инициализировано значением 34 . Давайте рассмотрим полноценный пример в котором показываются преимущества класса bitset при вводе/выводе битовых полей.
#includeСразу смотрим результат работы программы:
Двоичное представление некоторых чисел: 0 = 00000000 1 = 00000001 2 = 00000010 3 = 00000011 4 = 00000100 5 = 00000101 6 = 00000110 7 = 00000111 8 = 00001000 9 = 00001001 10 = 00001010 11 = 00001011 12 = 00001100 13 = 00001101 14 = 00001110 15 = 00001111 16 = 00010000 17 = 00010001 18 = 00010010 19 = 00010011 20 = 00010100
В строке 2 , мы как всегда подключаем заголовочный файл bitset , для работы с битовыми полями. В восьмой строке, мы объявили битовое поле, размером 8 бит или - один байт. Далее в цикле мы выводим на экран двоичное представление чисел, начиная с 0 и заканчивая числом 20. Обратите внимание, что для вывода в двоичном формате никаких методов вызывать не надо, так как числа в битовых полях при инициализации сразу переводятся в двоичный формат. Также стоит обратить внимание на метод to_ulong() , он переводит двоичное представление числа в десятичное. Это хорошо видно в выводе результата программы.
Вот еще один пример программы, демонстрирующий некоторые операции класса bitset:
#include
Из нового функционала стоит отметить метод flip() , он инвертирует все биты поля на противоположные, строка 15 . Так же в программе показаны пример использования логических операций | & ^ , смотреть строки 19, 22, 25 . Результат программы показан ниже:
битовых полей на языке C++.В отличие от других языков программирования С++ имеет структуру, называемую битовыми полями ( полями битов ), которая позволяет работать с отдельными битами. Битовое поле – это особый компонент структуры, определяющий длину отдельного ее элемента. Битовые поля полезны по нескольким причинам.
- Если ограничено место для хранения информации, можно сохранить несколько логических переменных в одном байте.
- Некоторые интерфейсы устройств передают информацию, закодировав биты в один байт.
- Некоторым процессам кодирования необходимо получить доступ к отдельным битам в байте.
Хотя все эти функции могут выполняться с помощью битовых операторов , битовые поля могут внести большую ясность в программу. Метод использования битовых полей для доступа к битам основан на структурах.
Битовое поле – это элемент структуры, определенный как некоторое число битов, обычно меньшее, чем число битов в целом числе (оно по величине не превосходит машинного слова и зависит от реализации компилятора). Они предназначены для экономного размещения в памяти данных небольшого диапазона, обеспечивают удобный доступ к отдельным битам данных. Кроме того, с помощью битовых полей можно формировать объекты с длиной внутреннего представления, не кратной байту.
Битовые поля обычно применяются в низкоуровневом программировании.
Объявление битовых полей
Элементом структуры может быть битовое поле , обеспечивающее доступ к отдельным битам памяти. Вне структур или объединений битовые поля объявлять нельзя. Нельзя также организовывать массивы битовых полей и нельзя применять к полям операцию определения адреса или получить ссылку на них.
Синтаксис объявления типа структуры с битовыми полями :
struct [ИмяСтруктуры] { Тип1 ИмяПоля1: ШиринаПоля1 Тип2 ИмяПоля2: ШиринаПоля2 ........................ ТипN ИмяПоляN: ШиринаПоляN } ИмяСтруктуры;
где struct – спецификатор типа;
ИмяСтруктуры – идентификатор ;
Тип1, ... ТипN – тип поля, который может быть только int , возможно, со спецификатором unsigned или signed ;
ШиринаПоля (длина) – целое неотрицательное десятичное число, значение которого обычно (в зависимости от реализации компилятора) не должно превышать длины машинного слова.
Например:
struct { int c1: 4; int c2: 12; } ab;
Битовые поля длиной 1 должны объявляться как unsigned , поскольку 1 бит не может иметь знака. Битовые поля могут иметь длину от 1 до 16 бит для 16-битных сред и от 1 до 32 бит для 32-битных сред.
Разрешается поле без имени (для этого надо указать только двоеточие и ширину), с помощью которого в структуру вводятся неиспользуемые биты (промежуток между значимыми полями). Нулевая ширина поля вводится, когда необходимо, чтобы следующее в данной структуре поле разместилось с начала очередного машинного слова.
Например, если нам нужны только биты cts и dsr , то можно объявить структуру status_type следующим образом:
struct status_type { unsigned: 4; unsigned cts:1; unsigned dsr:4; } status;
В структуре можно смешивать "обычные" элементы с битовыми полями .
Например:
struct emp { struct addr address; float pay; unsigned lay_off: 1; //работает или нет unsigned hourly: 1;//почасовая оплата или оклад unsigned deductions: 3; //удержание налога };
Эта структура определяет запись по каждому служащему, в которой используется только один байт для хранения трех элементов информации: статус служащего, характер оплаты его труда и налоговая ставка. Без использования битовых полей для хранения этой информации пришлось бы занять несколько байтов.
Вместо служебного слова struct можно употреблять