Битовое поле. Операции над знаковыми полями в дополнительном коде

Многие задачи требуют создания минимальных по размеру программ. Один из
способов экономии памяти в программах на Си заключается в использовании битовых
полей для представления совокупности данных целого типа. Битовое поле может
иметь тип либо signed int либо unsigned int и занимать от 1 до 16 битов.

Битовые поля могут описываться только в качестве элементов структурного
шаблона аналогично объявлению целой знаковой или беззнаковой переменной, после
имени которой через двоеточие записывается целая константа, определяющая размер
битового поля. В описании допускаются неименованные битовые поля (для них имя
опускается, а указывается только двоеточие и размер), которые используются как
заполнители пространства битового поля. Битовые поля размещаются в направлении
от младших к старшим битам в слове. Например,
описание:

обеспечивает размещение в памяти структурной переменной flags,
включающей в себя битовые поля с именами a, b, c и d, как показано на рис. 9.3.

Рис. 9.3. Размещение в памяти битовых полей.

Поля не могут переходить через границы памяти целого числа. Например,
если бы поле d имело размер более 5-ти битов, то оно было бы размещено в
следующем слове. Для принудительного выравнивания к границе следующего слова
используется размер 0. Если в рассматриваемой структуре BITFLDS описать поле
без имени int:3; как int:0; , то поле d будет размещено в новом слове.
Битовые поля обрабатываются как целые укороченного размера. Их можно
использовать в любых выражениях и с любыми операциями за исключением операции
взятия адреса. Обращение к полям осуществляется аналогично обращению к другим
элементам структуры, то есть с использованием операции точка, например так:

if(flags.b == flags.d)...

Наиболее частым применением битовых полей является использование их в
качестве набора переключателей, характеризующих состояние некоторого объекта
программы. Например, структурнаяпеременная key:

struct { unsigned shift:1;

unsigned cups:1;

позволяет хранить
информацию о состоянии клавиатуры компьютера. При этом включение и выключение
признаков осуществляется понятной операцией присваивания (в другом случае
пришлось бы пользоваться логическими операциями):

а проверка состояния
переключателей - условным оператором:

if(key.shift==1 && key.cups==1)...

Заметим, что битовое поле внутри структурного шаблона не может быть
массивом, хотя описание массива структур, каждый элемент которого может
содержать битовые поля, вполне обосновано и не противоречит синтаксису Турбо
Си. Используя ранее приведенный структурный шаблон BITFLDS, можно сделать
описание:

struct BITFLDS system;

Структурные переменные не обязательно должны включать в себя только
битовые поля, как это имело место в приводимых примерах, а, наряду с битовыми
полями, в них могут содержаться элементы и других типов:

struct COORDINATES {

float a; /* Длина */

float b; /* Ширина */

float c; /* Высота */

char name; /* Шифризделия */

struct COORDINATES point;

struct DETAL rez, /* Резисторы */

cond; /* Конденсаторы */

и доступ к битовому
полю осуществляется также, как и в случае вложенных структурных переменных,
например, rez.point.x

В противоположность другим компьютерным языкам С имеет возможность, называемую битовыми полями, позволяющую работать с отдельными битами. Битовые поля полезны по нескольким причинам. Ниже приведены три из них:

  1. Если ограничено место для хранения информации, можно сохранить несколько логических (истина/ложь) переменных в одном байте.
  2. Некоторые интерфейсы устройств передают информацию, закодировав биты в один байт.
  3. Некоторым процедурам кодирования необходимо получить доступ к отдельным битам в байте.

Хотя все эти функции могут выполняться с помощью битовых операторов, битовые поля могут внести большую ясность в программу.

Метод использования битовых полей для доступа к битам основан на структурах. Битовое поле, на самом деле, - это просто особый тип структуры, определяющей, какую длину имеет каждый член. Стандартный вид объявления битовых полей следующий:

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;
};

Определяет запись служащего, использующую только один байт для хранения трех частей информации - статуса служащего, получил ли он зарплату и размер удержаний. Без использования битовых полей данная информация заняла бы три байта.

Как вы уже наверное догадались, битовое поле - это просто массив битов фиксированного размера. Битовые поля удобно использовать для решения задач связанных с булевой логикой. Например, представление цветов или шифрование данных, ну или просто перевод чисел в двоичную систему исчисления, все это так или иначе связано с булевой логикой, а значит легко реализуемо с помощью битовых полей. В С++ для реализации булевой логики, раньше использовались побитовые операции & , | , ~ и тип данных int , но это было неэффективно, по нескольким причинам: неоправданный расход памяти для хранения одного бита и конечно же не всегда удобно использовать поразрядные логические операции.

Итак, чтобы воспользоваться классом bitset , достаточно подключить заголовочный файл :

#include

Класс bitset это шаблон, который принимает один параметр - размер поля в битах. Отсюда следует запомнить, что любой объект типа bitset всегда имеет фиксированную длину. Вот пример:

Bitset<8> number(34);

В этом примере объявлено битовое поле - number размером 8 бит, которое инициализировано значением 34 . Давайте рассмотрим полноценный пример в котором показываются преимущества класса bitset при вводе/выводе битовых полей.

#include #include // заголовочный файл битовых полей #include // для манипулятора setw() using namespace std; int main() { bitset<8> number; cout << "Двоичное представление некоторых чисел:\n"; for(int i = 0; i < 21; i++) { number = i; cout << setw(2) << number.to_ulong() << " = " << number << endl; } return 0; }

Сразу смотрим результат работы программы:

Двоичное представление некоторых чисел: 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 #include // заголовочный файл битовых полей using namespace std; int main() { int number; cout << "Введите целое число от 1 до 255: "; cin >> number; bitset<8> message(number); cout << number << " = " << message << endl; bitset<8> bit2 = message; message = message.flip(); // поменять все биты на противоположные cout << "Инвертированное число: " << message << endl; bitset<8> bit3 = bit2 | message; cout << bit2 << " | " << message << " = " << bit3 << endl; // операция логического ИЛИ bitset<8> bit4 = bit3 & message; cout << bit3 << " & " << message << " = " << bit4 << endl; // операция логического И bitset<8> bit5 = bit3 ^ message; cout << bit3 << " ^ " << message << " = " << bit5 << endl; // операция исключающего ИЛИ return 0; }

Из нового функционала стоит отметить метод 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 можно употреблять

Рассматривается реализация структурных типов данных union , enum , и struct . Так же рассматривается заполнение структур и реализацию битового поля.

Unions (обьединение)

Когда используется доступ к полю union другого типа, результат может быть предсказан на основе исходного типа.

Enumerations (перечисление)

Обьект enum реализуется наименьшим числовым типом который содержит диапазон перечисления enum .

В C режиме, и в C++ режиме без --enum_is_int , если enum перечисление содержит только положительные значения перечислителя, тип хранения перечисления является первым беззнаковым типом из следующего списка в соответствии с диапазоном перечислений в перечислении enum .В других режимах, и в случаях когда enum содержит отрицательные значения перечислителя,тип хранения enum является первым из следующего, согласно диапазону перечисления в перечислителе enum :

  • unsigned char если не используется --enum_is_int
  • signed char если не используется --enum_is_int
  • unsigned short если не используется --enum_is_int
  • signed short если не используется --enum_is_int
  • signed int
  • unsigned int кроме Си с --strict
  • signed long long кроме Си с --strict
  • unsigned long long кроме Си с --strict .

Примечание

  • В RVCT 4.0, тип хранилища enum являющийся первым беззнаковым типом из списка применяется только в режиме GNU (--gnu).
  • В ARM ® Compiler 4.1 и выше, тип хранения перечисления enum тип хранения перечисления, являющийся первым беззнаковым типом из списка, применяется независимо от режима.

Реализация enum таким образом может уменьшать размер данных. Опция командной строки --enum_is_int lделает основной тип enum наименьшой шириной int .

See the description of C language mappings in the Procedure Call Standard for the ARM ® Architecture specification for more information.

Примечание

Care must be taken when mixing translation units that have been compiled with and without the --enum_is_int option, and that share interfaces or data structures.

In strict C, enumerator values must be representable as int s. That is, they must be in the range -2147483648 to +2147483647, inclusive. A warning is issued for out-of-range enumerator values:

#66: enumeration value is out of "int" range

Such values are treated the same way as in C++, that is, they are treated as unsigned
int , long long , or unsigned long
long .

To ensure that out-of-range Warnings are reported, use the following command to change them into Errors:

armcc --diag_error=66 ...

Structures (структуры)

Следующие пункты относятся к:

  • все структуры Си
  • все C++ структуры и классы не использующие виртуальные или базовые классы.

Структуры могут содержать заполнение для правильного выравнивания полей и корректного выравнивания структуры.

На графике 1 показан пример классической неупакованной структуры.

Байты 1, 2, и 3 заполняются чтобы обеспечить правильное выравнивание поля.

Байты 11 и 12 заполняются чтобы обеспечить правильное выравнивание структуры.

Функция sizeof() возвращает размер структуры включающая заполнение.

График 1 Пример обычной неупакованной структуры

Компилируются структуры одним из способов в зависимости от ее определения:

  • Структуры которые определены как static или extern заполняются нулями.
  • Структуры в стеке или куче, например которые определены с помощью malloc() или auto , заполняются тем что хранилось в памяти ранее. Вы не можете использовать функцию memcmp() для сравнения содержимого структур определенных таким образом.

Используйте опцию --remarks для просмотра сообщений генерируемых компилятором когда он вставляет дополнения в структуру struct .

Структуры с пустой инициализацией разрешены в C++:

Struct { int x; } X = { };

Однако, если вы компилируете C или C++ с параметрами — cpp и — c90, генерируется ошибка.

Bitfields (битовое поле)

В неупакованных структурах, ARM компилятор выделяет битовые поля в контейнеры.

Контейнер как корректно выравненный обьект декларируемого типа.

Битовые поля распределяются таким образом, чтобы указанное первое поле занимало младшие биты слова в зависимости от конфигурации::

Little-endian Самые низкие адресации являются наименее значимыми. Big-endian Самые низкие адресации являются наиболее значимыми.

Контейнер битового поля может быть любым целым типом.

Примечание

In strict 1990 ISO Standard C, the only types permitted for a bit field are int , signed int , and unsigned int . For non- int bitfields, the compiler displays an error.

В строгом стандарте ISO стандартного стандарта 1990 года единственными типами, разрешенными для битового поля, являются int , signed int , и unsigned int . Для не — int битового поля компилятор отображает ошибку.

Простое битовое поле, обьявленное без signed или unsigned квалификаторов, рассматривается как unsigned . Для примера, int x:10 выделяется как целое число без знака размером 10 бит.

Битовое поле присваивается первому контейнеру правильного типа, который имеет достаточное количество нераспределенных битов, например:

Struct X { int x:10; int y:20; };

В первом объявлении создается целочисленный контейнер и выделяется 10 бит в x . Во втором объявлении компилятор находит существующий целочисленный контейнер с достаточным количеством нераспределенных битов и выделяет y в том же контейнере, что и x .

Битовое поле полностью содержится в контейнере. Битовое поле, которое не помещается в контейнер, помещается в следующий контейнер того же типа. Например, декларация z переполняет контейнер, если для структуры объявлено дополнительное битовое поле:

Struct X { int x:10; int y:20; int z:5; };

Компилятор заполняет оставшиеся два бита для первого контейнера и назначает новый целочисленный контейнер для z .

Контейнеры битого поля могут перекрывать друг друга, например:

Struct X { int x:10; char y:2; };

Первое объявление создает целочисленный контейнер и выделяет 10 бит в x . Эти 10 бит занимают первый байт и два бита второго байта целочисленного контейнера. Во втором объявлении компилятор проверяет контейнер типа char . Не существует подходящего контейнера, поэтому компилятор выделяет новый правильно выровненный контейнер char .

Поскольку естественное выравнивание символа равно 1, компилятор выполняет поиск первого байта, который содержит достаточное количество нераспределенных битов, чтобы полностью содержать бит. В структуре примера второй байт контейнера int имеет два бита, выделенных для x , и 6 бит нераспределены. Компилятор выделяет контейнер char начиная со второго байта предыдущего int контейнера, пропускает первые два бита, которые выделены для x , и выделяет два бита в y .

Если y объявлен char y: 8 , компилятор заполняет второй байт и выделяет новый контейнер символов в третий байт, потому что битовое поле не может переполнять его контейнер. На следующем рисунке показано распределение битового поля для следующего примера структуры:

Struct X { int x:10; char y:8; };

Распределение битового поля

Примечение

Те же основные правила применяются к объявлениям битового поля с разными типами контейнеров. Например, добавим битовое поле int к структуре и она примет следующий вид:

Struct X { int x:10; char y:8; int z:5; }

Компилятор выделяет контейнер int , начинающийся в том же месте, что и контейнер int x:10 , и выделяет байт-выровненный char и 5-битное битовое поле как показано ниже:

Распределение битового поля

Вы можете явно помещать контейнер битового поля, объявляя неопределенное битовое поле размером 0. Битовое поле нулевого размера заполняет контейнер до конца, если контейнер не пуст. Последующее объявление битового поля запускает новый пустой контейнер.

Примечание

В качестве оптимизации компилятор может перезаписывать биты дополнений в контейнере с неопределенными значениями при записи битового поля. Это не влияет на обычное использование битовых полей.

Packing and alignment of bitfields (упаковка и выравнивание битовых полей)

Использование __attribute__((aligned(n))) делает битовое поле n-байт выравненным, не только его контейнер, but the bitfield is aligned to the packed alignment at most. It is ignored on bitfields in __packed and __attribute__((packed)) structs.

The alignment of a bitfield member’s container is the same as the alignment of that bitfield member. The size of a bitfield container is the least multiple of the alignment that fully covers the bitfield, but no larger than the size of the container-type. The following code examples show this:

#pragma pack(2) /* Контейнер b должен начинаться с границы выравнивания 2 байта и должен * иметь размер не больше, чем тип контейнера, в этом случае размер * короткий. Контейнер b не может начинаться со смещения 0 (перекрывается с a) *, так как бит b будет начинаться со смещения 2 и не будет полностью лежать * внутри контейнера. Поэтому контейнер для b должен начинаться со смещения * 2. * * Data layout: 0x11 0x00 0x22 0x22 * Container layout:| a | | b | */ struct { char a; short b: 16; } var1 = { 0x11, 0x2222 }; /* контейнер b может быть до 4 байт. Его размер должен быть 2 или 4 байта, так как они являются кратны* ми выравниванию, которые не больше размера контейнера. При использовании 4 байтового контейнера *, начинающегося с 0, битовое поле b может начинаться со смещения 1 и полностью лежать * внутри контейнера. * * Data layout: 0x11 0x22 0x22 0x00 * Container layout:| a | * | b | */ struct { char a; int b: 16; } var2 = { 0x11, 0x2222 };

Упакованные контейнеры битовых полей, включая все контейнеры битовых полей в упакованных структурах, имеют выравнивание 1. Поэтому максимальное битовое дополнение, вставленное для выравнивания упакованного контейнера битовых полей, составляет 7 бит.

Для неупакованного контейнера битового поля, максимальное заполнение битов 8*sizeof(container-type)-1 .

Заполнение хвоста всегда вставляется в структуру по мере необходимости, чтобы гарантировать, что массивы структуры правильно выровнены.

A packed bitfield container is only large enough (in bytes) to hold the bitfield that declared it. Non-packed bitfield containers are the size of their type.

Следующие примеры иллюстрируют эти взаимодействия.

Struct A { int z:17; }; // sizeof(A) = 4, alignment = 4 struct A { __packed int z:17; }; // sizeof(A) = 3, alignment = 1 __packed struct A { int z:17; }; // sizeof(A) = 3, alignment = 1 struct A { char y:1; int z:31; }; // sizeof(A) = 4, alignment = 4 struct A { char y:1; __packed int z:31; }; // sizeof(A) = 4, alignment = 1 __packed struct A { char y:1; int z:31; }; // sizeof(A) = 4, alignment = 1 struct A { char y:1; int z:32; }; // sizeof(A) = 8, alignment = 4 struct A { char y:1; __packed int z:32; }; // sizeof(A) = 5, alignment = 1 __packed struct A { char y:1; int z:32; }; // sizeof(A) = 5, alignment = 1 struct A { int x; char y:1; int z:31; }; // sizeof(A) = 8, alignment = 4 struct A { int x; char y:1; __packed int z:31; }; // sizeof(A) = 8, alignment = 4 __packed struct A { int x; char y:1; int z:31; }; // sizeof(A) = 8, alignment = 1 struct A { int x; char y:1; int z:32; }; // sizeof(A) = 12, alignment = 4 struct A { int x; char y:1; __packed int z:32; }; // sizeof(A) = 12, alignment = 4 __packed struct A { int x; char y:1; int z:32; }; // sizeof(A) = 9, alignment = 1

Note that and are not identical; the location of z within the structure and the tail-padding differ.

Struct example1 { int a: 8; /* 4-byte container at offset 0 */ __packed int b: 8; /* 1-byte container at offset 1 */ __packed int c: 24; /* 3-byte container at offset 2 */ }; /* Total size 8 (3 bytes tail padding) */; struct example2 { __packed int a: 8; /* 1-byte container at offset 0 */ __packed int b: 8; /* 1-byte container at offset 1 */ int c: 8; /* 4-byte container at offset 0 */ }; /* Total size 4 (No tail padding) */ struct example3 { int a: 8; /* 4-byte container at offset 0 */ __packed int b: 32; /* 4-byte container at offset 1 */ __packed int c: 32; /* 4-byte container at offset 5 */ int d: 16; /* 4-byte container at offset 8 */ int e: 16; /* 4-byte container at offset 12 */ int f: 16; /* In previous container */ }; /* Total size 16 (No tail padding) */