Понимание языка C для встраиваемых систем: Что такое структуры?
Данная статья предоставляет основную информацию о структурах в программировании для встраиваемых систем на языке C.
После введения в структуры мы рассмотрим некоторые важные применения этого мощного объекта данных. Затем мы рассмотрим синтаксис языка C, позволяющий объявлять структуры. Наконец, мы кратко обсудим требование по выравниванию данных. Мы увидим, что мы можем уменьшить размер структуры, просто изменив порядок ее членов.
Структуры
Ряд переменных одного типа, которые логически связаны друг с другом, может быть сгруппирован в виде массива. Работа с группой, а не с набором независимых переменных, позволяет нам упорядочивать данные и использовать их более удобным способом. Например, мы можем определить следующий массив для хранения последних 50 отсчетов АЦП, который оцифровывает голосовой ввод:
uint16_t voice[50];
Обратите внимание, что uint16_t
– это целочисленный тип без знака с шириной ровно 16 бит. Он определен в стандартной библиотеке C stdint.h, которая предоставляет типы данных определенной длины в битах, не зависящие от спецификаций системы.
Массивы могут быть использованы для группирования нескольких переменных одного типа данных. Но что если есть связь между переменными разных типов данных? Можем ли мы рассматривать эти переменные в нашей программе как группу? Например, предположим, что нам нужно указать частоту дискретизации АЦП, который генерирует голосовой массив, объявленный выше. Мы можем определить переменную типа float
для хранения значения частоты дискретизации:
float sample_rate;
Хотя переменные voice
и sample_rate
связаны друг с другом, они определены как две независимые переменные. Чтобы связать эти две переменные друг с другом, мы можем использовать мощную конструкцию данных языка C, которая называется структурой. Структуры позволяют нам группировать различные типы данных и работать с ними как с одним объектом данных. Структура может включать в себя различные виды типов переменных, такие как другие структуры, указатели на функции, указатели на структуры и так далее. Для примера с голосом мы можем использовать следующую структуру:
struct record {
uint16_t voice[50];
float sample_rate;
};
В этом случае у нас есть структура с именем record
, которая имеет два члена или поля: первый член – это массив элементов uint16_t
, а второй – переменная типа float
. Синтаксис начинается с ключевого слова struct
. Слово после ключевого слова struct
является необязательным именем, используемым позже для ссылки на структуру. Другие детали определения и использования структур мы обсудим в оставшейся части статьи.
Почему структуры важны?
Приведенный выше пример указывает на важное применение структур, то есть определение зависящих от приложения объектов данных, которые могут связывать друг с другом отдельные переменные разных типов. Это не только приводит к эффективному способу манипулирования данными, но также позволяет нам реализовывать специализированные структуры, называемые структурами данных.
Структуры данных могут использоваться для различных применений, таких как обмен сообщениями между двумя встроенными системами и хранение данных, собранных с датчика, в несмежных ячейках памяти.
Кроме того, структуры являются полезными объектами данных, когда программе требуется доступ к регистрам периферийного устройства микроконтроллера с отображаемой памятью.
Объявление структуры
Чтобы использовать структуры, нам сначала нужно задать шаблон структуры. Рассмотрим пример кода, приведенный ниже:
struct record {
uint16_t voice[4];
float sample_rate;
};
Он указывает макет или шаблон для создания будущих переменных этого типа. Этот шаблон включает в себя массив uint16_t
и переменную типа float
. Имя шаблона – record
, оно идет после ключевого слова struct
. Стоит отметить, что для хранения шаблона структуры память не выделяется. Выделение памяти происходит только определения переменной структуры на основе этого шаблона. Следующий код объявляет переменную mic1
приведенного выше шаблона:
struct record mic1;
Теперь для переменной mic1
выделен раздел памяти. В нем есть место для хранения четырех элементов uint16_t
массива и одной переменной float
.
Доступ к членам структуры можно получить с помощью оператора члена (.
). Например, следующий код присваивает значение 100 первому элементу массива и копирует значение sample_rate
в переменную fs
(которая должна быть типа float
).
mic1.voice[0]=100;
fs=mic1.sample_rate;
Другие способы объявления структуры
В предыдущем разделе мы рассмотрели один из способов объявления структур. Язык C поддерживает и другие форматы, которые будут рассмотрены в этом разделе. Вы, вероятно, во всех своих программах будете придерживаться одного формата, но иногда знакомство с остальными может быть полезным.
Общий синтаксис объявления шаблона структуры:
struct tag_name {
type_1 member_1;
type_2 member_2;
…
type_n member_n;
} variable_name;
tag_name
и variable_name
являются необязательными идентификаторами. Обычно мы видим хотя бы один из этих двух идентификаторов, но есть случаи, когда мы можем убрать их обоих.
Синтаксис 1. Когда присутствуют tag_name
и variable_name
, мы определяем переменную структуры сразу после шаблона. Используя этот синтаксис, мы можем переписать предыдущий пример следующим образом:
struct record {
uint16_t voice[4];
float sample_rate;
} mic1;
Теперь, если нам нужно определить другую переменную (mic2
), мы можем написать:
struct record mic2;
Синтаксис 2. Включено только variable_name
. Используя этот синтаксис, мы можем переписать пример из предыдущего раздела следующим образом:
struct {
uint16_t voice[4];
float sample_rate;
} mic1;
В этом случае мы должны определить все свои переменные сразу после шаблона, и определить другие переменные позже в нашей программе мы не сможем (потому что шаблон не имеет имени, и далее мы не сможем ссылаться на него).
Синтаксис 3. В этом случае нет ни tag_name
, ни variable_name
. Шаблоны структур, определенные таким образом, называются анонимными структурами. Анонимная структура может быть определена в другой структуре или объединении. Пример приведен ниже:
struct test {
// анонимная структура
struct {
float f;
char a;
};
} test_var;
Чтобы получить доступ к членам показанной выше анонимной структуры, мы можем использовать оператор члена (.
). Следующий код присваивает значение 1.2 члену f
.
test_var.f=1.2;
Поскольку структура является анонимной, мы получаем доступ к ее членам, используя оператор члена только один раз. Если бы она имела имя, как в следующем примере, нам пришлось бы использовать оператор члена дважды:
struct test {
struct {
float f;
char a;
} nested;
} test_var;
В этом случае мы должны использовать следующий код, чтобы присвоить значение 1.2 для f
:
test_var.nested.f=1.2;
Как видите, анонимные структуры могут сделать код более читабельным и менее многословным. Также можно вместе со структурой использовать ключевое слово typedef
, чтобы определить новый тип данных. Этот метод мы рассмотрим в следующей статье.
Распределение памяти для структуры
Стандарт C гарантирует, что члены структуры будут располагаться в памяти один за другим в порядке, в котором они объявлены в структуре. Адрес памяти первого члена будет таким же, как адрес самой структуры. Рассмотрим следующий пример:
struct Test2{
uint8_t c;
uint32_t d;
uint8_t e;
uint16_t f;
} MyStruct;
Для хранения переменных c
, d
, e
и f
будут выделены четыре области памяти. Порядок расположения в памяти будет соответствовать порядку объявления членов: область для c
будет иметь наименьший адрес, затем идут d
, e
и, наконец, f
. Сколько байт нам нужно для хранения этой структуры? Учитывая размер переменных, мы знаем, что, по крайней мере, 1+4+1+2=8 байт требуется хранения этой структуры. Однако, если мы скомпилируем этот код для 32-разрядной машины, мы неожиданно заметим, что размер MyStruct
составляет 12 байтов, а не 8! Это связано с тем, что компилятор имеет определенные ограничения при выделении памяти для разных элементов структуры. Например, 32-разрядное целое число может храниться только в областях памяти, адрес которых делится на четыре. Такие ограничения, называемые требованиями выравнивания данных, реализованы для более эффективного доступа процессора к переменным. Выравнивание данных приводит к некоторой потере места (или заполнению) в распределении памяти. Здесь дается только краткое представление этой темы; подробности мы рассмотрим в следующей статье серии.
Зная о требованиях к выравниванию данных, мы можем изменить в структуре порядок членов и повысить эффективность использования памяти. Например, если мы перепишем приведенную выше структуру, как показано ниже, ее размер на 32-разрядной машине уменьшится до 8 байт.
struct Test2{
uint32_t d;
uint16_t f;
uint8_t c;
uint8_t e;
} MyStruct;
Для встраиваемой системы с ограниченным объемом памяти это уменьшение размера объекта данных с 12 до 8 байт является существенной экономией, особенно когда программе требуется много таких объектов данных.
В следующей статье мы обсудим выравнивание данных более подробно и рассмотрим некоторые примеры использования структур во встраиваемых системах.
Подведем итоги
- Структуры позволяют нам определять зависящие от приложения объекты данных, которые могут связывать друг с другом отдельные переменные разных типов. Это приводит к эффективным средствам манипулирования данными.
- Специализированные структуры, называемые структурами данных, могут использоваться для различных применений, таких как обмен сообщениями между двумя встроенными системами и хранение данных, собранных с датчика, в несмежных областях памяти.
- Структуры полезны, когда нам необходим доступ к регистрам периферийного устройства микроконтроллера с отображаемой памятью.
- Возможно, мы можем повысить эффективность использования памяти, изменив порядок членов в структуре.