Продолжаем работать со структурами.
Оказывается, кроме полей размером, кратным байту, мы можем в структурах (а также, конечно, и в объединениях) работать с битами, то есть мы можем объявить поле в какое-то количество бит. Хотя язык C не предусматривает операции с отдельным битом, но с помощью битовых полей это ограничение можно обойти. Это, конечно же, не является полной заменой битовых операций ассемблера, но для удобства работы с кодом, для его читабельности, мы это вполне можем применять.
Также, если мы и не будем в своих кодах применять битовые поля, то нам такое может попасться в чужих кодах, и после данного урока мы будем хотя бы знать, что это такое.
Также битовые поля нам могут потребоваться для каких-то флагов, для каких-то значений, диапазон которых отличается от диапазона стандартных типов.
Битовое поле в структуре объявляется следующим образом
В различной литературе я читал про разные ограничения типов для битовых полей. Видимо, всё это исходит из различных стандартов, выходящих время от времени. Поэкспериментировав немного, я понял, что самое главное, чтобы этот тип был целочисленным.
Если в структуре есть и битовые поля и обычные поля, то лучше битовые поля располагать подряд и стараться их не чередовать с обычными для того, чтобы меньше потребовалось памяти под структуру. Также по возможности выравнивание полей тоже лучше отключить. Можно и оставить, если, конечно, у нас в сумме размер всех битовых полей, идущих подряд в структуре будет стремиться к 32.
Вот пример объявления битовых полей в структуре, которой есть и обычные поля
#pragma pack(push, 1)
typedef struct
{
unsigned char diad0:2;
unsigned char tri0:3;
unsigned char bit0:1;
unsigned char bit1:1;
unsigned char a;
unsigned short b;
} my_arg_t;
#pragma pack(pop)
Так как мы отключили выравнивание, то под переменную такой структуры будет выделена память в 4 байта.
Небольшое выравнивание в данном случае все равно произойдёт, так как если посчитать суммарное количество битов в битовых полях, то их у нас 7, а не 8, то есть если количество битов в непрерывно следующих битовых полях в структуре не кратно восьми, то следующее за ним небитовое обычное поле будет выравниваться по биту следующего адреса, так как адресоваться в битах мы не можем.
В случае конкретной структуры, которую мы только что объявили поля её расположатся в памяти следующим образом
Самые младшие два бита младшего байта заняло битовое поле (диада) diad0, следующие три бита этого же байта заняло битовое поле (триада) tri0, следующий бит этого байта — битовое поле размером в 1 бит bit0, следующий бит — такое же поле bit1. 7-й бит данного байта у нас не участвует в данных вообще, так как следующее поле не битовое. Поэтому следующее поле заняло следующий байт, а последнее поле — следующие 2 байта, сначала — младший байт поля, а затем — старший.
Думаю, теперь немного прояснилась картина по битовым полям.
Ну а чтобы она совсем прояснилась, давайте поработаем с ними на практике.
Поэтому давайте приступим к проекту, который мы сделаем, как всегда, из проекта прошлого урока с именем MYPROG31 и присвоим ему имя MYPROG32.
Откроем наш проект в Eclipse, произведём его первоначальную настройку и удалим весь наш код из функции main() за исключением возврата. Функция main() приобретёт вот такой вид
int main()
{
return 0; //Return an integer from a function
}
Глобальное объединение my_arg_t и одноимённую закомментированную структуру также удалим и добавим вместо них структуру с битовыми полями, точь в точь такую же, как в теоретической части
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
float yf, zf; //---------------------------------------------- #pragma pack(push, 1) typedef struct { unsigned char diad0:2; unsigned char tri0:3; unsigned char bit0:1; unsigned char bit1:1; unsigned char a; unsigned short b; } my_arg_t; #pragma pack(pop) //---------------------------------------------- |
В функции main() уточним размер, который займёт в памяти переменная типа такой структуры
1 2 3 |
int main() { printf("sizeof data is %d\n", sizeof(my_arg_t)); |
Соберём код и проверим, сколько байтов занимает в памяти наша структура
Как мы и считали, структура занимает в памяти 4 байта.
Объявим локальный символьный массив
1 2 3 |
int main() { char str1[35]; |
Проинициализируем поля структуры какими-нибудь значениями и выведем их в консоль
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
printf("sizeof data is %d\n", sizeof(my_arg_t)); my_arg_t my_arg; my_arg.diad0 = 0b10; my_arg.tri0 = 0b101; my_arg.bit0 = 0b1; my_arg.bit1 = 0b1; my_arg.a = 0x98; my_arg.b = 0x7654; int_to_binary(my_arg.diad0, str1); printf("diad0= %s\n", str1); int_to_binary(my_arg.tri0, str1); printf("tri0 = %s\n", str1); int_to_binary(my_arg.bit0, str1); printf("bit0 = %s\n", str1); int_to_binary(my_arg.bit1, str1); printf("bit1 = %s\n", str1); printf("a = 0x%02X\n", my_arg.a); printf("b = 0x%04X\n", my_arg.b); |
Соберём код и посмотрим результат
Значения всех полей, в том числе и битовых, вывелись в консоли.
Теперь поставим breakpoint вот здесь
Перейдём в отладку и посмотрим, каким образом появляются по мере присвоения данные полей в памяти.
Перейдём в дизассемблер и выполним программу до точки останова. Мы видим, что первое значение укладывается по адресу esp + 0x19 (кстати все последующие битовые поля укладываются туда же)
Откроем дамп памяти с адреса стека и увидим, что по данному адресу у нас уже что-то есть (это также важно)
Сделаем один шаг в основном окне (где код на C, а не на ассемблере) и увидим следующие изменения нашего байта в памяти
Если кто не знает, как выглядит данное шестнадцатеричное число в двоичном выражении, может воспользоваться калькулятором в режиме «программист».
Выставим в настройках шестнадцатеричный ввод и размер 1 байт и введём данное число
Даже не переключаясь в двоичный режим мы видим число 11111110. Мы знаем, что наша диада должна попасть в два младшие бита — так оно и есть
Сделаем следующий шаг и посмотрим наше число в памяти
Аналогичным способом введём данное число в калькулятор и увидим, что наша триада также попала в следующие три бита за диадой
Следующие два битовых поля у нас представляют собой однобитовые величины и мы их инициализировали единицами, а так как в данном числе стека в тех местах, куда они должны попасть уже единицы, то число после следующих двух шагов просто не изменится
Следующие поля (небитовые), если шагать дальше, попадут в следующие байты
Остановим отладку. Мы увидели теперь воочию, как именно хранятся битовые поля в памяти компьютера. С ассемблерным кодом мы не будем разбираться, каким способом данные поля туда укладываются, так как это не тема нашего урока.
Теперь мы вот как ещё поиграем с битовыми полями.
В принципе, битовые поля можно использовать для изменения битов многобитовых чисел. Как именно это можно использовать?
Можно придумать такую структуру, сумма битов полей которой, включая битовые, но уже без пропусков займёт нужный нам размер, например 32 бита и мы в любой момент можем нашу структуру представить как слово (черырёхбайтовое число).
Как именно это можно сделать?
А сейчас разберёмся. У нас же есть адреса, как располагаются поля, мы знаем. Применив нехитрое преобразование типов данных, можно добиться в принципе всего.
Давайте закомментируем весь наш предыдущий код кроме объявления символьного массива, вывода размера структуры и объявления переменной типа структуры, закомментируем также и структуру, включая команды запрета и разрешения выравнивания и добавим вот такую глобальную структуру
1 2 3 4 5 6 7 8 9 10 11 12 13 |
*/ #pragma pack(push, 1) typedef struct { unsigned short tetr0:4; unsigned short tetr1:4; unsigned short diad1:2; unsigned short diad2:2; unsigned short triad1:3; unsigned short bit1:1; unsigned short b; } my_arg_t; #pragma pack(pop) |
Сделав нехитрый подсчёт. можно убедиться, что сумма битов всех полей структуры равна 32. Соберём код и проверим размер структуры в байтах
Проинициализируем поля структуры различными числами в main()
1 2 3 4 5 6 7 8 |
*/ my_arg.tetr0 = 0xA; my_arg.tetr1 = 0xB; my_arg.diad1 = 0b10; my_arg.diad2 = 0b01; my_arg.triad1 = 0b101; my_arg.bit1 = 0b1; my_arg.b = 0x7654; |
Теперь давайте, обратившись по адресу переменной структуры, применив приведение типа, выведем полностью значение всех полей, как единого 32-битного числа
1 2 |
my_arg.b = 0x7654; printf("my_arg = %08X\n", *(unsigned int*)&my_arg); |
В данном случае второй параметр в вызове функции printf мы читаем справа налево.
Сначала мы взяли адрес переменной структуры, преобразовали его к указателю типа unsigned int, а затем его разыменовали. Тем самым получили значение 32-битного числа из памяти.
Посмотрим результат нашей операции в консоли, собрав предварительно код
То есть мы соорудили число по кусочкам из полей структуры. А если нам объявить вообще 32 поля по одному биту и назвав их определённым образом, то мы вообще можем спокойно управлять каждым битом числа. Конечно, так не делается, но вполне можно.
Давайте проверим наше число. С первыми двумя тетрадами всё ясно, они расположились с права в самом младшем байте. С последним 16-битным полем тоже всё ясно. Оно красуется в двух старших байтах. А вот с байтом 1 сейчас разберёмся. Давайте представим его в двоичном виде
Мы получили вот такой байт
Вот таким образом в нём и улеглись наши битовые поля
Всё сходится.
Теперь поработаем с нашей структурой наоборот. Мы присвоим ей сразу общее 32-битное число, также применив разыменование и приведение типа
1 2 |
printf("my_arg = %08X\n", *(unsigned int*)&my_arg); *(unsigned int*)&my_arg = 0x89ABCDEF; |
В файле utils.c добавим ещё одну функцию, подобную той, какая есть, которая также будет преобразовывать целочисленное значение в строковый двоичный вид, но работать будет уже с беззнаковой целочисленной переменной
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//---------------------------------------------- void uint32_to_binary(unsigned int x, char* in_str) { char str_tmp[9] = {}; unsigned char i, j, k; unsigned char bt[4] = {0}; for (j=0; j<4; j++) { if(((x >> j*8)==0) && (j>0)) break; bt[j] = (unsigned char) (x >> j*8); } strcpy(in_str,"0b"); for (k=0; k<j; k++) { for (i=0; i<8; i++) { switch ((bt[j-k-1] >> i) & 0b00000001) { case 1: str_tmp[7-i] = '1'; break; case 0: str_tmp[7-i] = '0'; break; } } strcat(in_str,str_tmp); } } //---------------------------------------------- |
Создадим для этой функции прототип в заголовочном файле, вернёмся в файл main.c и в функции main() покажем в консоли значение памяти переменной структуры в двоичном виде
1 2 3 |
*(unsigned int*)&my_arg = 0x89ABCDEF; uint32_to_binary(*(unsigned int*)&my_arg, str1); printf("%s\n",str1); |
Соберём код и посмотрим наше значение
Теперь выведем значение всех полей в консоли
1 2 3 4 5 6 7 8 |
printf("%s\n",str1); printf("tetr0 = %01X\n", my_arg.tetr0); printf("tetr1 = %01X\n", my_arg.tetr1); printf("diad1 = %01X\n", my_arg.diad1); printf("diad2 = %01X\n", my_arg.diad2); printf("triad1 = %01X\n", my_arg.triad1); printf("bit1 = %01X\n", my_arg.bit1); printf("b = 0x%04X\n", my_arg.b); |
Соберём код и посмотрим результат
С тетрадами и последним 16-битным полем у нас опять всё ясно, а вот остальные битовые поля мы сейчас разложим побитно, а потом всё вместе преобразуем в шестнадцатеричный вид
Всё опять сходится, байт CD — это как раз байт 1 в нашем присвоенном числе.
Также вот он в его полном двоичном выражении
То есть, мы видим, что с помощью битовых полей мы можем получать также доступ к битам целых чисел, менять их узнавать и, в принципе, даже применять какие-то операции.
Итак, на данном занятии мы познакомились с битовыми полями в структурах, да и не просто познакомились, а также узнали, как именно данные таких полей располагаются в памяти, отведённой под переменную структуры.
Всем спасибо за внимание!
Предыдущий урок Программирование на C Следующий урок
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий