Объединения и битовые поля

Теги: Си объединения, си битовые поля.



Объединения

Объединения в си похожи на структуры, с той разницей, что все поля начинаются с одного адреса. Это значит, что размер объединения равен размеру самого большого его поля. Так как все поля начинаются с одного адреса, то их значения перекрываются. Рассмотрим пример:

#include <conio.h>
#include <stdio.h>

union Register32 {
	struct {
		unsigned char byte1;
		unsigned char byte2;
		unsigned char byte3;
		unsigned char byte4;
	} bytes;
	struct {
		unsigned short low;
		unsigned short high;
	} words;
	unsigned dword;
};

typedef union Register32 EAX;

void main() {
	EAX reg;
	reg.dword = 0x0000C0FF;
	printf("    dword \t%08x\n", reg.dword);
	printf(" low word \t%04x\n", reg.words.low);
	printf("high word \t%04x\n", reg.words.high);
	printf("    byte1 \t%02x\n", reg.bytes.byte1);	
	printf("    byte2 \t%02x\n", reg.bytes.byte2);	
	printf("    byte3 \t%02x\n", reg.bytes.byte3);	
	printf("    byte4 \t%02x\n", reg.bytes.byte4);	
	getch();
}

Здесь было создано объединение, которое содержит три поля – одно поле целого типа (4 байта), два поля типа short int (2 байта каждое) и 4 поля по одному байту. После того, как значение было присвоено полю dword, оно также стало доступно и остальным полям.

Объединение в си.
Объединение в си.

Напоминаю, что на x86 байты располагаются справа налево. Все поля объединения "обладают" одинаковыми данными, но каждое поле имеет доступ только до своей части.
Вот ещё один пример: рассмотрим представление числа с плавающей точкой:

#include <conio.h>
#include <stdio.h>

union floatint{
	float f;
	int i;
};

void main() {
	union floatint u = {10.f};
	printf("%f\n", u.f);
	printf("%x\n", u.i);
	getch();
}

Обратите внимание, что объединение можно инициализировать, как и структуру. При этом значение будет приводиться к типу, который имеет самое первое поле. Сравните результаты работы

#include <conio.h>
#include <stdio.h>

union floatint{
	int i;
	float f;
};

void main() {
	union floatint u = {10.f};
	printf("%f\n", u.f);
	printf("%x\n", u.i);
	getch();
}

Битовые поля

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

struct <имя> {
	<тип> <имя>: <размер>;
	...
}
#include <conio.h>
#include <stdio.h>

struct byte {
	unsigned a0: 1;
	unsigned a1: 1;
	unsigned a2: 1;
	unsigned a3: 1;
	unsigned a4: 1;
	unsigned a5: 1;
	unsigned a6: 1;
	unsigned a7: 1;
};

void main() {
	struct byte x = {0, 0, 0, 1, 0, 0, 0, 0};
	x.a1 = 1;
	printf("sizeof byte = %d\n", sizeof(struct byte));
	printf("x.a1 = %d\n", x.a1);
	printf("x.a3 = %d\n", x.a3);
	printf("x.a5 = %d", x.a5);
	getch();
}

В этом примере каждое поле структуры обозначено как битовое поле, длина каждого поля равна единице. Обращаться к каждому полю можно также, как и к полю обычной структуры. Битовые поля имеют тип unsigned int, так как имеют длину один бит. Если длина поля больше одного бита, то поле может иметь и знаковый целый тип.

#include <conio.h>
#include <stdio.h>

struct byte {
	signed char a: 4;
	signed char b: 4;
};

void main() {
	struct byte x = {-1, 3};
	printf("hex a = %x\n", x.a);
	printf("dec a = %d\n", x.a);
	printf("hex b = %x\n", x.b);
	printf("dec b = %d\n", x.b);
	getch();
}

Размер структуры, содержащей битовые поля, всегда кратен 8. То есть, если одно поле содержит 5 бит, а второе 4, то второе поле начинается с восьмого бита и три бита остаются неиспользованными.
Неименованное поле может иметь нулевой размер. В этом случае следующее за ним поле смещается так, чтобы добрать до 8 бит.
Если же адрес поля уже кратен 8 битам, то нулевое поле не добавит сдвига.
Кроме того, если имеются обычные поля и битовые поля, то первое битовое поле будет сдвинуто так, чтобы добрать до 8 бит.

#include <conio.h>
#include <stdio.h>

struct byte1 {
	char a;
	char b;
	char c;
};

struct byte2 {
	char a;
	char b;
	char c;
	unsigned d: 1;
};

struct byte3 {
	char a;
	char b;
	char c;
	unsigned d: 1;
	unsigned: 0;    //неименованое поле нулевого размера
	unsigned e: 1;
};

void main() {
	printf("sizeof byte1 = %d\n", sizeof(struct byte1));
	printf("sizeof byte2 = %d\n", sizeof(struct byte2));
	printf("sizeof byte3 = %d", sizeof(struct byte3));
	getch();
}

В этих примерах видно, что структура добирает даже не до 8 бит, а больше - до адреса, кратного 4 байтам. Работать подобным образом, инициализируя каждое поле по отдельности, неудобно. Поэтому структуры с битовыми полями делают полем объединения, например:

#include <conio.h>
#include <stdio.h>

struct byte {
	unsigned a0: 1;
	unsigned a1: 1;
	unsigned a2: 1;
	unsigned a3: 1;
	unsigned a4: 1;
	unsigned a5: 1;
	unsigned a6: 1;
	unsigned a7: 1;
};

union Byte {
	unsigned char value;
	struct byte bitfield;
};

void main() {
	union Byte x;
	x.value = 10;
	printf("%d%d%d%d%d%d%d%d", x.bitfield.a7, x.bitfield.a6, x.bitfield.a5, x.bitfield.a4, x.bitfield.a3, 
							   x.bitfield.a2, x.bitfield.a1, x.bitfield.a0);
	getch();
}

Те же самые действия можно было сделать и с помощью обычного сдвига

#include <conio.h>
#include <stdio.h>

void main() {
	unsigned char c = 10;
	int i;
	for (i = 7; i > -1; i--) {
		printf("%d", (c >> i) & 1);
	}
	getch();
}

Рассмотрим ещё один пример – знакопостоянный сдвиг вправо. Сдвиг вправо (>>) выталкивает самый левый бит и справа записывает ноль. Из-за этого операцию сдвига вправо нельзя применить, например, для чисел со знаком, так как будет потерян бит знака. Исправим ситуацию, сделаем знакопостоянный сдвиг: будем проверять последний бит числа (напомню, что мы работаем с архитектурой x86 и биты расположены «задом наперёд»)

int int_signed_shiftr(int32_t x) {
	union {
		int32_t int_f;
		struct {
			unsigned char: 8;
			unsigned char: 8;
			unsigned char: 8;
			unsigned char: 7;
			unsigned char le_sign: 1;
		} bitfield;
	} iss;
	unsigned sign_bit;
	iss.int_f = x;
	//Сохраняем бит перед сдвигом
	sign_bit = iss.bitfield.le_sign;
	iss.int_f = iss.int_f >> 1;
	//Возвращаем значение бита обратно
	iss.bitfield.le_sign = sign_bit;

	return iss.int_f;
}

Здесь я специально использовал тип int32_t (библиотека stdint.h), чтобы гарантировать размер переменных в 32 бита. Теперь можно вызвать функцию и посмотреть результат.

void main() {
	printf("%d\n", int_signed_shiftr(22));
	printf("%d\n", int_signed_shiftr(-22));
	printf("%d\n", int_signed_shiftr(25));
	printf("%d\n", int_signed_shiftr(-25));
	printf("%d\n", int_signed_shiftr(27));
	printf("%d\n", int_signed_shiftr(-27));

	getch();
}

Вывод

11
-11
12
-13
13
-14
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Быстрое выделение памяти под многомерные массивы