А теперь только представьте — вы сами можете создавать, своего рода, типы данных, которые вам необходимы и с которыми вам будет удобно работать! И это несложно!

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

Если вы только приступаете к знакомству со структурами в С++, сначала, вам необходимо ознакомиться с синтаксисом структур в языке С++ . Рассмотрим простой пример, который поможет познакомиться со структурами и покажет, как с ними работать. В этой программе мы создадим структуру, создадим объект структуры, заполним значениями элементы структуры (данные об объекте) и выведем эти значения на экран. Ну что же, приступим!

#include using namespace std; struct building //Создаем структуру! { char *owner; //здесь будет храниться имя владельца char *city; //название города int amountRooms; //количество комнат float price; //цена }; int main() { setlocale (LC_ALL, "rus"); building apartment1; //это объект структуры с типом данных, именем структуры, building apartment1.owner = "Денис"; //заполняем данные о владельце и т.д. apartment1.city = "Симферополь"; apartment1.amountRooms = 5; apartment1.price = 150000; cout << "Владелец квартиры: " << apartment1.owner << endl; cout << "Квартира находится в городе: " << apartment1.city << endl; cout << "Количество комнат: " << apartment1.amountRooms << endl; cout << "Стоимость: " << apartment1.price << " $" << endl; return 0; }

В строках 4 — 10 мы создаем структуру. Чтобы ее объявить используем зарезервированное слово struct и даем ей любое, желательно логичное, имя. В нашем случае — building . С правилами именования переменных, вы можете ознакомиться в этой статье . Далее открываем фигурную скобку { , перечисляем 4 элемента структуры через точку с запятой; , закрываем фигурную скобку } и в завершении ставим точку с запятой; . Это будет нашим шаблоном (формой) структуры.

В строке 16 объявляем объект структуры. Как и для обычных переменных, необходимо объявить тип данных. В этом качестве выступит имя нашей созданной структуры — building .

Как же заполнить данными (инициализировать) элементы структуры? Синтаксис таков: Имя объекта далее оператор точка. и имя элемента структуры. Например: apartment1.owner . Таким образом, в строках 18-21 присваиваем данные элементам структуры.

И так, данные мы внесли. Следующий вопрос: «Как к ним обратиться, как работать и использовать их в программе?» Ответ — «Очень просто — так же, как и при инициализации, используя точку. и имя элемента структуры». В строках 23 — 26 выводим заполненные элементы структуры на экран.

И вот что мы увидим в результате, когда скомпилируем нашу программу:

Владелец квартиры: Денис Квартира находится в городе: Симферополь Количество комнат: 5 Стоимость: 150000 $

Что ещё важно знать:

  • Объект структуры можно объявить до функции main() . Это выглядело бы так:
struct building { char *owner char *city; int amountRooms; float price; }apartment1; //объявление объекта типа building
  • Инициализировать структуру можно и таким способом:
building apartment1 = {"Денис", "Симферополь", 5, 150000};

но так делают крайне редко;

  • Структуру можно вкладывать в другие структуры (это мы рассмотрим в следующем примере).

Дополним предыдущий пример, чтобы увидеть дополнительные возможности работы со структурами.

Пример:

#include using namespace std; struct date //создаем еще одну структуру, чтобы вложить ее в структуру building // дата постройки { char *month; // Месяц постройки дома int year; // Год }; struct building { char *owner; char *city; int amountRooms; float price; date built; //вкладываем одну структуру в определение второй }; void show(building object) //создаем функцию, которая принимает структуру, как параметр { cout << "Владелец квартиры: " << object.owner << endl; cout << "Квартира находится в городе: " << object.city << endl; cout << "Количество комнат: " << object.amountRooms << endl; cout << "Стоимость: " << object.price << " $" << endl; cout << "Дата постройки: " << object.built.month << " " << object.built.year << endl; } int main() { setlocale (LC_ALL, "rus"); building apartment1; apartment1.owner = "Денис"; apartment1.city = "Симферополь"; apartment1.amountRooms = 5; apartment1.price = 150000; apartment1.built.month = "январь"; apartment1.built.year = 2013; struct building *pApartment; //это указатель на структуру pApartment = &apartment1; //Обратите внимание, как нужно обращаться к элементу структуры через указатель //используем оператор -> cout << "Владелец квартиры: " << pApartment->owner << endl; cout << "Квартира находится в городе: " << pApartment->city << endl; cout << "Количество комнат: " << pApartment->amountRooms << endl; cout << "Стоимость: " << pApartment->price << " $" << endl; cout << "Дата постройки: " << pApartment->built.month << " " << pApartment->built.year << "\n\n\n"; building apartment2; //создаем и заполняем второй объект структуры apartment2.owner = "Игорь"; apartment2.city = "Киев"; apartment2.amountRooms = 4; apartment2.price = 300000; apartment2.built.month = "январь"; apartment2.built.year = 2012; building apartment3 = apartment2; //создаем третий объект структуры и присваиваем ему данные объекта apartment2 show(apartment3); cout << endl << endl; return 0; }

Коментарии по коду программы:

Строка 17 — создание объекта built типа date в определении структуры building . Строки 42 — 43 : создаем указатель на структуру struct building *pApartment; и далее присваиваем ему адрес уже созданного и заполненного данными объекта pApartment = &apartment1; . Обращаясь к элементам структуры через указатель мы используем оператор -> (тире и знак >) . Это видно в строках 47 — 51.

В строке 62 показано, как можно инициализировать структуру. А именно, можно создать новый объект структуры и присвоить ему одним целым, уже созданный и заполненный данными, объект. В функцию show() передаем объект структуры, как параметр — строка 64. Результат:

Владелец квартиры: Денис
Квартира находится в городе: Симферополь
Количество комнат: 5
Стоимость: 150000 $
Дата постройки: январь 2013
Владелец квартиры: Игорь
Квартира находится в городе: Киев
Количество комнат: 4
Стоимость: 300000 $
Дата постройки: январь 2012
Для продолжения нажмите любую клавишу. . .

Разобрав этот пример, мы увидели на практике следующее:

  • структуру можно вкладывать в другую структуру;
  • увидели, как создаётся указатель на структуру;
  • как нужно обращаться к элементу структуры через указатель. А именно, используя оператор -> ; В примере это было так: apartment0->owner , но можно и так (*apartment0).owner . Круглые скобки, во втором случае, обязательны.
  • данные одной структуры можно присвоить другой структуре;
  • можно структуру передать в функцию, как параметр (кстати, элементы структуры так же можно передавать в функцию, как параметры).

В дополнение ко всему, следует отметить, что функции могут так же возвращать структуры в результате своей работы. Например:

Building Set() { building object; // формирование объекта //... код функции return object; }

Вот так, вкратце, мы познакомились со структурами в языке С++, попрактиковались на примерах и узнали основы. Это только начало!

Перед тем как приступить к изучению классов в C++, мы рассмотрим тип данных подобный классу — структуры. Структуры полезны, когда нам надо объединить несколько переменных с разными типами под одним именем. Это делает программу более компактной и более гибкой для внесения изменений. Также структуры незаменимы, когда необходимо сгруппировать некоторые данные, например, запись из базы данных или контакт из книги адресов. В последнем случае структура будет содержать такие данные контакта как имя, адрес, телефон и т.п.

Синтаксис

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

Int x_coor; int y_coor; string names;

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

Формат объявления структуры выглядит так:

Struct Car { int x_coor; int y_coor; string name; };

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

StructName variableName;

structName — имя структуры, variableName — имя переменной.

x_coor, y_coor и name — поля нашей структуры. При объявлении структуры мы создаем составной тип данных, с помощью которого можно создавать переменные, которые сочетают в себе несколько значений (например, координаты и имя). Внутри структуры каждому полю мы даем имя, чтобы потом обращаться к этому значению по его имени.

Для доступа к полям структуры используется точка:

// объявляем переменную Car myCar; // и используем её myCar.x_coor = 40; myCar.y_coor = 40; myCar.name = "Porche";

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

Рассмотрим пример, демонстрирующий сочетание массивов и структур.

#include using namespace std; struct PlayerInfo { int skill_level; string name; }; using namespace std; int main() { // как и с обычными типами, вы можете объявить массив структур PlayerInfo players; for (int i = 0; i < 5; i++) { cout << "Please enter the name for player: " << i << "\n"; // сперва получим доступ к элементу массива, используя // обычный синтаксис для массивов, затем обратимся к полю структуры // с помощью точки cin >> players[ i ].name; cout << "Please enter the skill level for " << players[ i ].name << "\n"; cin >> players[ i ].skill_level; } for (int i = 0; i < 5; ++i) { cout << players[ i ].name << " is at skill level " << players[i].skill_level << "\n"; } }

Так же как и с простыми типами (int, например), вы можете создавать массивы структур. А с каждым элементом этого массива работать так же как и с отдельной переменной. Для доступа к полю name первого элемента массива структур, просто напишите:

Players[ 0 ].name

Структуры и функции

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

Struct EnemySpaceShip { int x_coordinate; int y_coordinate; int weapon_power; }; EnemySpaceShip getNewEnemy();

Функция getNewEnemy должна возвращать структуру с инициализированными полями:

EnemySpaceShip getNewEnemy () { EnemySpaceShip ship; ship.x_coordinate = 0; ship.y_coordinate = 0; ship.weapon_power = 20; return ship; }

На самом деле эта функция вернет копию созданной локальной переменной ship. Это значит, что каждое поле структуры будет скопировано в новую переменную. В нашем случае копирование малого количества полей не заметно, но когда вы работаете с большими объемами данных нужно избегать лишних действий, подробнее об этом поговорим в статье про указатели.

Таким образом, для получения новой переменной будем использовать следующий код:

EnemySpaceShip ship = getNewEnemy();

Теперь эту переменную можно использовать как обычную структуру.

Передавать структуры в функцию можно так:

EnemySpaceShip upgradeWeapons (EnemySpaceShip ship) { ship.weapon_power += 10; return ship; }

Когда мы передаем структуру в функцию, она копируется, так же как и при возвращении структуры. Поэтому любые изменения сделанные внутри функции будут потеряны, поэтому мы возвращаем структуру после изменения.

Использование функции:

Ship = upgradeWeapons(ship);

Когда вызывается функция, переменная ship копируется и изменяется в функции, а когда переменная возвращается, она снова копируется и перезаписывает поля оргинальной переменной.

И наконец, программа для создания и улучшения одного корабля:

Struct EnemySpaceShip { int x_coordinate; int y_coordinate; int weapon_power; }; EnemySpaceShip getNewEnemy() { EnemySpaceShip ship; ship.x_coordinate = 0; ship.y_coordinate = 0; ship.weapon_power = 20; return ship; } EnemySpaceShip upgradeWeapons(EnemySpaceShip ship) { ship.weapon_power += 10; return ship; } int main() { EnemySpaceShip enemy = getNewEnemy(); enemy = upgradeWeapons(enemy); }

Указатели

Если вы работаете с на структуру, то для доступа к переменным надо использовать оператор «->» вместо точки. Все свойства указателей не изменяются. Пример:

#include using namespace std; struct xampl { int x; }; int main() { xampl structure; xampl *ptr; structure.x = 12; ptr = &structure; cout<< ptr->x; cin.get(); }

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

Массивы

Прежде чем говорить о структуре в Си, нужно описать массив.

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

Обычный массив в Си записывается так: int a = {1, 2, 3, 4}.

Видим, что a - имя, int - тип данных, внутри фигурных скобок { } находятся значения, между квадратными скобками указывается длина, то есть количество элементов. Количество элементов является статическим, равняется 4. Это означает, что если в этом примере пользователь добавит пятое значение, компилятор выдаст ошибку. Если изначально не известно количество, они могут быть добавлены позже, но в квадратных скобках не ставится значение.

Двумерный объявляется похожим образом. Например, массив, который содержит 5 элементов-массивов, при этом каждый содержит по 3 элемента объявляется так: int a.По аналогии с одномерным добавлять ничего нельзя, чтобы не получить ошибку компилирования.

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

Классы

Класс и структура похожи по между собой, но отличаются некоторыми нюансами. Что это такое? Это абстракция, описывающая методы еще не существующего объекта. После создания объект или, как он называется по-другому, экземпляр класса имеет конкретные свойства. Методы могут использоваться внутри, снаружи или при наследовании.

Класс объявляется так:

class /*class name*/

/* спецификатор доступа private обозначает, что управление методами возможно только внутри класса*/

/* делает свойства доступными для других частей кода */

/* наследуемые классы получают возможность использовать эти свойства */

Что такое структура в языке Си

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

  • дата издания;
  • номер выпуска;
  • название;
  • стоимость.

Для решения этой задачи можно было бы применить массивы.

Объявляем массив с датами int date, номерами int number, названиями char title, стоимостью int price.

Обращаясь по индексу, мы получаем требуемую информацию. Вывод информации о произведении под номером 3 выглядит так: cout << “дата выпуска: ” date “, номер: ” number “, название: ” title “, стоимость: “ price).

Структура упрощает запись, описывается следующим образом:

Видим одно из главных преимуществ - присутствуют разные типы переменных. Программист не просто экономит время - он упрощает код, в дальнейшем ему будет намного проще работать.

Объявление

Структуры в Си играют очень важную роль - объединение данных различного типа.

Для начала нужно указать имя структуры и свойства.

Struct - ключевое слово, оно начинает объявление, name - имя, type - тип данных, member - имя элемента.

Объявляется так:

name name2, где name - заданное при создании структуры имя, а name2 - имя переменной.

Объявить переменные можно на этапе создания.

Первый и второй пример равносильны друг другу.

Если есть необходимость объявить несколько переменных, они перечисляются через запятую.

} name2, name3, name4.

Инициализация

После объявления структуру в Си необходимо инициализировать.

name2.member=”a”;

Инициазация может происходить при создании.

char member = “a”;

У структуры такой же синтаксис, как у класса. У них практически одинаковое поведение, возможности. Все, что находится в теле класса, по умолчанию недоступно для использования другими объектами.

У структуры все наоборот - все поля и методы являются публичными. Вручную можно задать модификатор доступа private и таким образом открыть доступ другим функциям или классам.

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

У одномерного только одна строка и n-e количество элементов. Объявление выглядит так:

Массив структур в Си объявляется так:

В этом примере мы создали MyStruct с элементом целочисленного типа под именем "а". Объявляем переменную obj1 - она является массивом, состоит из 10 элементов.

При объявлении нескольких массивов одного типа используется MyStruct obj1, obj2, инициализация происходит во время объявления. Выглядит так:

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

Указатель - это переменная, которая не содержит значения, а указывает на ту переменную, которая имеет какое-то значение. Соответственно, указатель содержит адрес этой переменной, на которую ссылается. Например, ptr = &var1 означает, что переменной со знаком амперсанда присвоен только адрес на переменную, но не само значение. Теперь все значения var1 доступны через переменную-указатель ptr.

Операция * отсылает к содержимому ячейки, на которую указывает переменная после этого символа. Например, *ptr говорит о том, что здесь содержатся значения, взятые из ячейки с адресом к ptr.

Чтобы выделить память для динамических переменных, используют операцию new.

У нас есть

Выделяем участок памяти, заносим туда некое значение MyStruct * point = new MyStruct;

Для удаления динамических переменных используем операцию delete. Чтобы освободить место, вводим delete p;

Доступ

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

Создаем myStruct с именем переменной b.

struct myStruct {

Обращаемся к fio:

и задаем произвольное значение. Например, b.fio = “Ivanov”.

Рассмотрим такой пример.

struct myStruct {

{ "Иванов", 456756 },

{ "Петров", 632345 }

В данном примере у нас есть массив структур со строками и числами. Чтобы вывести на экран фамилию Иванов, используем следующее:

cout << myStruct tel.fio;

Когда захотим получить значение 456756, выполняем cout << myStruct tel.num.

Структура и функции

Может использоваться, как аргумент функция в структуре в Си.

struct myStruct {

Имеем переменную value, строку text на 100 символов. Создаем переменную menu типа myStruct: myStruct menu. В следующем примере функция принимает указатель на структуру как аргумент, а в теле безымянной функции происходит инициализация этих переменных.

void item(myStruct menu)

sprintf(menu.text,"One item");

Заключение

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

Создается с помощью ключевого слова struct, а внутри фигурных скобок { } указываются свойства.

Объявление происходит на этапе создания или после.

Структура - это удобное хранилище для разнородных данных, которые хочется объединить. К примеру, вы можете создать структуру, описывающую параметры вашего устройства - сетевые настройки, таймаут спящего режима, его идентификатор и прочее подобное, типа какой-нибудь строки приветствия и состояния светодиода. Раз все параметры будут храниться в одном месте - они всегда будут на виду, да и нормальные IDE будут вам подсказывать поля структуры при обращении к ним. Ещё мы рассмотрим хранение и восстановление структур из архива, а также их передачу по сети.

Объявление такой структуры:

Struct { uint32_t ID; char IP; uint16_t timeout; bool led; char text; } params;

Как это работает?

В си довольно удобный синтаксис, в том плане что многие вещи записываются как «тип_данных переменная», начиная с «int i» заканчивая «void main() {}». Так и здесь, кодовое слово struct начинает объявление структуры, и весь кусок кода «struct { … }» просто задаёт новый тип. Соответственно, params - это уже готовая переменная (экземпляр типа), которую можно использовать. Внутри фигурных скобок перечислены все поля структуры, которые потом будут доступны так: params.ID или params.IP. Длина полей должна быть фиксированной, поэтому нельзя использовать строки вида *text, только массивы вида text.

Можно было сделать немного иначе: объявить только тип, а переменную завести позже. Для этого мы использовали бы ключевое слово typedef и написали так:

Typedef struct { uint32_t ID; char IP; uint16_t timeout; bool led; char text; } params_struct; params_struct params;

Так появляется возможность оставить все объявления структурных типов в отдельном файле (header), а в главном файле просто использовать уже готовые структурные типы для объявления структур прямо по месту.

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

Struct { uint32_t ID; char IP; uint16_t timeout; bool led; char text; } params1, params2, params;

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

В принципе, ничего сложного в структурах нет, а с темой серверов и клиентов мы плавно подошли к более интересной теме:

Хранение, передача и синхронизация структур

Для многих будет удивлением то, что данные структуры хранятся в памяти в виде плоского списка, все поля структуры просто идут в памяти друг за другом. Поэтому становится возможным обращаться с этой структурой как с простым массивом байт! Проверим, создадим массив «поверх» этой структуры.

Начальное смещение получим так:

Char *Bytes = ¶ms;

мы объявили указатель char и поместили в него адрес params. Теперь Bytes указывает на первый байт структуры, и при последовательном чтении мы побайтно прочитаем всю структуру. Но сколько байт нужно прочитать? Для этого рассмотрим две интересных функции.

sizeof и offsetof

Это даже не функции, а встроенные макросы языка Си. Начнём с более простой, sizeof .

Компилятор заменяет все записи вида sizeof X на значение длины Х. В качестве X может выступать как тип, так и экзмепляр типа, т.е. в нашем случае можно подставить в sizeof и тип структуры (если мы его заводили с помощью typedef), и саму переменную структуры так: sizeof params_struct или sizeof params. Она пройдёт по всем полям структуры, сложит их длины и отдаст сумму, которая и будет длиной структуры.

offsetof - настоящий макрос, который принимает два параметра (структуру _s_ и поле _m_ в ней) и отдаёт положение этого поля в структуре, его смещение относительно начала структуры. Выглядит этот макрос очень просто:

Offsetof(s, m) (size_t)&(((s *)0)-›m).

Как он работает?

  1. Берём число 0
  2. Преобразуем его к типу «указатель на структуру s»: (s*)0
  3. Обращаемся к полю m из этой структуры: ((s*)0)->m
  4. Вычисляем его адрес: &(((s*)0)->m)
  5. Преобразуем адрес к целому числу: (size_t)&(((s*)0)->m)

Магия именно в первом шаге, в котором мы берём 0. Благодаря этому на четвёртом шаге абсолютный адрес поля, вычисленный компилятором, оказывается отсчитан относительно начала структуры - структуру-то мы положили в адрес 0. Таким образом, после выполнения этого макроса мы реально имеем смещение поля относительно начала структуры. Понятно, что этот макрос правильно определит смещения даже в сложных и вложенных структурах.

Здесь нужно сделать небольшое отступление. Дело в том, что я рассматривал самый простой случай, когда поля упакованы точно вслед друг за другом. Есть и другие методы упаковки, которые называются «выравнивание». К примеру, можно выдавать каждому полю «слот», кратный 4 байтам, или 8 байтам. Тогда даже char будет занимать 8 байт, и общий размер структуры вырастет, а все смещения сдвинутся и станут кратны выравниванию. Эта штука полезна при программировании для компьютера, поскольку из-за грануляции ОЗУ процессор гораздо быстрее умеет извлекать из памяти выровненные данные, ему требуется на это меньше операций.

Работа с массивом из структуры

Окей, теперь мы умеем представлять любую структуру в виде массива байт, и обратно. Вы поняли фишку? У нас теперь одна и та же область памяти имеет роли «структура» и «массив». Изменяем что-то в структуре - меняется массив, меняем массив - меняется структура.

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

Теперь осталось лишь научиться удобно с этим всем работать.

Хранение и передача структуры

Чтобы создать архивную копию структуры, для передачи по сети или для складывания её в надёжное место - отдайте в вашу функцию передачи данных адрес этого массива. К примеру, моя функция записи массива данных в EEPROM выглядит так: I2C_burst_write (I2Cx, HW_address, addr, n_data, *data). Вам просто нужно вместо n_data передать sizeof params, а вместо *data - ¶ms:

I2C_burst_write (I2Cx, HW_address, addr, sizeof params, ¶ms)

Функции передачи данных по сети обычно выглядят примерно так же. В качестве данных передавайте ¶ms, а в качестве длины данных - sizeof params.

Приём и восстановление структуры

Всё точно так же. Моя функция чтения массива из EEPROM: I2C_burst_read (I2Cx, HW_address, addr, n_data, *data). n_data = sizeof params, *data = ¶ms:

I2C_burst_read (I2Cx, HW_address, addr, sizeof params, ¶ms)

Не забывайте, что вы сразу пишете принятые байты непосредственно в структуру. При медленной или ненадёжной передаче имеет смысл записать данные во временный буфер, и после их проверки передать их в структуру через

Memcpy(¶ms, &temp_buffer, sizeof params).

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

Хранение/восстановление отдельных полей

И зачем же мы так долго рассматривали макрос offsetof? Его очень удобно использовать для чтения и записи отдельных полей структуры, например так:

I2C_burst_write (I2Cx, HW_address, addr + offsetof(params, IP), sizeof params.IP, ¶ms.IP) I2C_burst_read (I2Cx, HW_address, addr + offsetof(params, IP), sizeof params.IP, ¶ms.IP)

Ну и вообще, было бы неплохо сделать удобные макросы-обёртки для этой цели.

#define store(structure, field) I2C_burst_write (I2Cx, HW_address, addr + offsetof(structure, field), sizeof(structure.field), &(structure.field)) #define load(structure, field) I2C_burst_read (I2Cx, HW_address, addr + offsetof(structure, field), sizeof(structure.field), &(structure.field))

Недавно познакомился со структурами C/C++ - struct. Господи, да «что же с ними знакомиться» скажете вы? Тем самым вы допустите сразу 2 ошибки: во-первых я не Господи, а во вторых я тоже думал что структуры - они и в Африке структуры. А вот как оказалось и - нет. Я расскажу о нескольких жизненно-важных подробностях, которые кого-нибудь из читателей избавят от часовой отладки…

Выравнивание полей в памяти

Обратите внимание на структуру:

Struct Foo { char ch; int value; };
Ну во-первых какой у этой структуры размер в памяти? sizeof(Foo) ?
Размер этой структуры в памяти зависит от настроек компилятора и от директив в вашем коде…

В общем выравниваются в памяти поля по границе кратной своему же размеру. То есть 1-байтовые поля не выравниваются, 2-байтовые - выравниваются на чётные позиции, 4-байтовые - на позиции кратные четырём и т.д. В большинстве случаев (или просто предположим что сегодня это так) выравнивание размера структуры в памяти составляет 4 байта. Таким образом, sizeof(Foo) == 8 . Где и как прилепятся лишние 3 байта? Если вы не знаете - ни за что не угадаете…

  • 1 байт: ch
  • 2 байт: пусто
  • 3 байт: пусто
  • 4 байт: пусто
  • 5 байт: value
  • 6 байт: value
  • 7 байт: value
  • 8 байт: value
Посмотрим теперь размещение в памяти следующей структуры:

Struct Foo { char ch; short id; int value; };
Оно выглядит вот так:

  • 1 байт: ch
  • 2 байт: пусто
  • 3 байт: id
  • 4 байт: id
  • 5 байт: value
  • 6 байт: value
  • 7 байт: value
  • 8 байт: value
То есть, то что можно впихнуть до выравнивания по 4 байта - впихивается на ура (без увеличения размера структуры в памяти), добавим ещё одно поле:

Struct Foo { char ch; short id; short opt; int value; };
Посмотрим на размещение полей в памяти:

  • 1 байт: ch
  • 2 байт: пусто
  • 3 байт: id
  • 4 байт: id
  • 5 байт: opt
  • 6 байт: opt
  • 7 байт: пусто
  • 8 байт: пусто
  • 9 байт: value
  • 10 байт: value
  • 11 байт: value
  • 12 байт: value
Всё это ой как печально, но есть способ бороться с этим прямо из кода:

#pragma pack(push, 1) struct Foo { // ... }; #pragma pack(pop)
Мы установили размер выравнивания в 1 байт, описали структуру и вернули предыдущую настройку. Возвращать предыдущую настройку - категорически рекомендую. Иначе всё может закончиться очень плачевно. У меня один раз такое было - падало Qt. Где-то заинклюдил их.h-ник ниже своего.h-ника…

Битовые поля

В комментариях мне указали на то, что битовые поля в структурах по стандарту являются «implementation defined» - потому их использования лучше избежать, но для меня соблазн слишком велик...

Мне становится не то что неспокойно на душе, а вообще становится хреново, когда я вижу в коде заполнение битовых полей при помощи масок и сдвигов, например так:

Unsigned field = 0x00530000; // ... field &= 0xFFFF00FF; field |= (id) << 8; // ... field &= 0xFFFFFF83; field |= (proto) << 2;
Всё это пахнет такой печалью и такими ошибками и их отладкой, что у меня сразу же начинается мигрень! И тут из-за кулис выходят они - Битовые Поля. Что самое удивительное - были они ещё в языке C, но кого ни спрашиваю - все в первый раз о них слышат. Этот беспредел надо исправлять. Теперь буду давать им всем ссылку, ну или хотя бы ссылку на эту статью.

Как вам такой кусок кода:

#pragma pack(push,1) struct IpHeader { uint8_t header_length:4; uint8_t version:4; uint8_t type_of_service; uint16_t total_length; uint16_t identificator; // Flags uint8_t _reserved:1; uint8_t dont_fragment:1; uint8_t more_fragments:1; uint8_t fragment_offset_part1:5; uint8_t fragment_offset_part2; uint8_t time_to_live; uint8_t protocol; uint16_t checksum; // ... }; #pragma pack(pop)
А дальше в коде мы можем работать с полями как и всегда работаем с полями в C/C++. Всю работу по сдвигам и т.д. берет на себя компилятор. Конечно же есть некоторые ограничения… Когда вы перечисляете несколько битовых полей подряд, относящихся к одному физическому полю (я имею ввиду тип который стоит слева от имени битового поля) - указывайте имена для всех битов до конца поля, иначе доступа к этим битам у вас не будет, иными словами кодом:

#pragma pack(push,1) stuct MyBitStruct { uint16_t a:4; uint16_t b:4; uint16_t c; }; #pragma pack(pop)
Получилась структура на 4 байта! Две половины первого байта - это поля a и b . Второй байт не доступен по имени и последние 2 байта доступны по имени c . Это очень опасный момент. После того как описали структуру с битовыми полями обязательно проверьте её sizeof !

Также порядок размещения битовых болей в байте зависит от порядка байтов. При порядке LITTLE_ENDIAN битовые поля раздаются начиная со первых байтов, при BIG_ENDIAN - наоборот…

Порядок байтов

Меня также печалят в коде вызовы функций htons() , ntohs() , htonl() , nthol() в коде на C++. На C это ещё допустимо, но не на С++. С этим я никогда не смирюсь! Внимание всё нижесказанное относится к C++!

Ну тут я буду краток. Я в одной из своих предыдущих статей уже писал что нужно делать с порядками байтов. Есть возможность описать структуры, которые внешне работают как числа, а внутри сами определяют порядок хранения в байтах. Таким образом наша структура IP-заголовка будет выглядеть так:

#pragma pack(push,1) struct IpHeader { uint8_t header_length:4; uint8_t version:4; uint8_t type_of_service; u16be total_length; u16be identificator; // Flags uint8_t _reserved:1; uint8_t dont_fragment:1; uint8_t more_fragments:1; uint8_t fragment_offset_part1:5; uint8_t fragment_offset_part2; uint8_t time_to_live; uint8_t protocol; u16be checksum; // ... }; #pragma pack(pop)
Внимание собственно обращать на типы 2-байтовых полей - u16be . Теперь поля структуры не нуждаются ни в каких преобразованиях порядка байт. Остаются проблемы с fragment_offset , ну а у кого их нет - проблем-то. Тем не менее тоже можно придумать шаблон, прячущий это безобразие, один раз его оттестировать и смело использовать во всём своём коде.

«Язык С++ достаточно сложен, чтобы позволить нам писать на нём просто» Как ни странно - Я

З.Ы. Планирую в одной из следующих статей выложить идеальные, с моей точки зрения, структуры для работы с заголовками протоколов стека TCP/IP. Отговорите - пока не поздно!