Структуры, битовые поля, перечисления и объединения

Часто возникает необходимость работать не с одной переменной, а с несколькими, которые описывают один объект. Например, точка — у нее есть координаты X, Y и Z, если мы говорим о трехмерном пространстве. В этом случае удобно использовать такую сущность, как структура.

Структура — это совокупность нескольких переменных, чаще всего различных типов, сгруппированных под одним именем.

struct point3d {
int x;
int y;
int z;
};

Доступ к элементам структуры осуществляется с помощью оператора точки или, если работаем с указателем на структуру, оператором ->.

point3d.x = 0;
(*point3d)->x = 2;

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

struct point3d pt;
// ..
struct point3d get_center(void);
uint32_t distance(struct point3d pt1, struct point3d p2);

Объявления со словом struct по сути вводят новый, составной, тип данных. Тем не менее такая запись не очень удобна, так как приходится писать два слова вместо одного. Чуть позже мы посмотрим, как решить и эту проблему.

В ядре ОС Linux стараются избегать абстрагирования структур через typedef. По мнению Л. Торвальдса, такие переопределения могут сказаться на качестве кода: программист может не осознавать, что работает со структурой. Переписку можно прочитать здесь: http://yarchive.net/comp/linux/typedefs.html

Когда память на вес золота, приходит в голову упаковывать в одно машинное слово несколько переменных, например, флаги каких-то событий. В Си существует особый вид структуры, который называется битовым полем (англ. bit field). Запись такого поля выглядит следующим образом:

struct {
uint8_t flag_a : 1; // 1 bit
uint8_t flag_b : 1; // 1 bit
uint8_t flag_c : 1; // 1 bit
uint8_t flag_d : 1; // 1 bit
uint8_t value : 4; // 4 bits
} bit_field;            // 8 bits
// ...

bit_field.flag_a = 0;
bit_field.flag_b = 1;
bit_field.flag_c = 1;
bit_field.flag_d = 0;
bit_field.value  = 4;

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

Существует еще один тип для создания констант, называемый перечислением. В отличии от define, который позволяет также создавать ассоциации символьных названий констант, перечисление может генерировать значения автоматически.

enum turn {
ON,
OFF,
};

По умолчанию первому элементу присваивается значение 0, второму — 1, и т.д. Значение при этом можно указать вручную.

enum mode {
MODE_1 = MODE_PIN_1,  // 1 0 0
MODE_2 = MODE_PIN_2,  // 0 1 0
MODE_3 = MODE_PIN_3,  // 0 0 1
};

Пример выше использует возможность вручную вбивать значения элементов и сопоставляет им маски ножек микроконтроллера, к которым подключены движковые переключатели. Дополнительным преимуществом использования перечисления над простыми define является то, что допущенную ошибку можно отследить на этапе компиляции.

void set_mode(enum mode m);
enum mode get_mode(void);

Последний тип данных называется объединением. Его суть заключается в том, что его тип определяется программистом, естественно, с некоторым ограничением. Грубо говоря, объединение — это особый вид структуры, все элементы которого имеют нулевое смещение от начального адреса. Таким образом, размер объединения равен большему размеру включенных в него переменных. Т.е. размер объединения позволяет хранить в нем любой из указанных типов, но следить за тем, что записывается и что считывается, должен программист.

union index {
uint8_t i;
uint32_t j;
} ind;
// ...
#ifdef STM8
ind.i = 250;
#elif STM32
ind.j = 1027;
#endif

В том случае, если вы записываете int, а потом считываете float, результат получится непредсказуемым. Вспомните, как хранятся оба эти типа в памяти.

В стандарте c11 появилась возможность использовать анонимные структуры, т.е. структуры без имени. Это удобно, если одна структура вложена в другую:

struct object22 {
   int age;
   union {
       float x;
       int n;
  };
};

либо при передачи оной в качестве аргумента функции (не нужно создавать временную переменную):

function((struct x) {1, 2})

Дабы забить уже последний гвоздь в крышку типов данных, осталось разобраться, как создать свой тип. Делается это при помощи ключевого слова typedef. С его же помощью можно вводить синонимы для уже существующих типов данных. Собственно, для uint8_t имеется синоним u8 в файле stm32f10x.h, а uint8_t сам является синонимом для unsigned int.

// stm32f10x.h
typedef uint8_t u8;

Используя такую конструкцию, можно ввести собственные типы. Перепишем enum mode:

typedef enum {
MODE_1 = MODE_PIN_1,  // 1 0 0
MODE_2 = MODE_PIN_2,  // 0 1 0
MODE_3 = MODE_PIN_3,  // 0 0 1
} MODE_t;
// ...
void set_mode(MODE_t m);
MODE_t get_mode(void);

Существуют разные соглашения, например, начинать название с заглавной буквы или писать всё название заглавными буквами и добавлять в конце суффикс _t (сокращение от type). Так код становится понятнее.


Изменено: