Структуры. Структуры в си и их передача

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

Обычно все члены структуры связаны друг с другом. Например, информация об имени и адресе, находящаяся в списке рассылки, обычно представляется в виде структуры. Следующий фрагмент кода объявляет шаблон структуры, определяющий имя и адрес. Ключевое слово struct сообщает компилятору об объявлении структуры.

Struct addr {
char name;
char street ; char city;
char state;
unsigned long int zip;
};

Объявление завершается точкой с запятой, поскольку объявление структуры - это оператор. Имя структуры addr идентифицирует структуру данных и является спецификатором типа. Имя структуры часто используют как ярлык.

На данный момент на самом деле не создано никакой переменной. Определена только форма данных. Для объявления настоящей переменной, соответствующей данной структуре, следует написать:

Struct addr addr_info;

В данной строке происходит объявление переменной addr_info типа addr. При объявлении структуры определяется переменная смешанного типа. До тех пор, пока не будет объявлена переменная данного типа, она не будет существовать.

Когда объявлена структурная переменная, компилятор автоматически выделяет необходимый участок памяти для размещения всех ее членов. Рис. показывает размещение addr_info в памяти.

Рисунок: Размещение структуры addr_info в памяти

При объявлении структуры можно одновременно объявить одну или несколько переменных.

Например:

Struct addr {
char name;
char street;
char city;
char state;
unsigned long int zip;
} addr_info; binfo, cinfo;

объявляет структуру addr и объявляет переменные addr_info, binfo, cinfo данного типа.

Важно понять, что каждая вновь создаваемая структурная переменная содержит свои собственный копии переменных, образующих структуру. Например, поле zip переменной binfo отделено от поля zip переменной cinfo. Фактически, единственная связь между binfo и cinfo заключается в том, что они обе являются экземплярами одного типа структуры. Больше между ними нет связи.

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

Struct {
char name;
char street;
char city;
char state;
unsigned long int zip;
} addr_info;

Объявляет одну переменную addr_info с типом, определенным предшествующей ей структурой. Стандартный вид объявления структуры следующий:

struct ярлык {
тип имя переменной;
тип имя переменной;
тип имя переменной;
} структурные переменные;

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

Последнее обновление: 02.10.2018

Наряду с классами структуры представляют еще один способ создания обственных типов данных в C#. Более того многие примитивные типы, например, int, double и т.д., по сути являются структурами.

Например, определим структуру, которая представляет человека:

Struct User { public string name; public int age; public void DisplayInfo() { Console.WriteLine($"Name: {name} Age: {age}"); } }

Как и классы, структуры могут хранить состояние в виде переменных и определять поведение в виде методов. Так, в данном случае определены две переменные - name и age для хранения соответственно имени и возраста человека и метод DisplayInfo для вывода информации о человеке.

Используем эту структуру в программе:

Using System; namespace HelloApp { struct User { public string name; public int age; public void DisplayInfo() { Console.WriteLine($"Name: {name} Age: {age}"); } } class Program { static void Main(string args) { User tom; tom.name = "Tom"; tom.age = 34; tom.DisplayInfo(); Console.ReadKey(); } } }

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

Конструкторы структуры

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

User tom;

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

User tom; int x = tom.age; // Ошибка tom.DisplayInfo(); // Ошибка

Также мы можем использовать для создания структуры конструктор по умолчанию, при вызове которого полям структуры будет присвоено значение по умолчанию (например, для числовых типов это число 0):

User tom = new User(); tom.DisplayInfo(); // Name: Age: 0

Также мы можем определить свои конструкторы. Например, изменим структуру User:

Using System; using System.Reflection; namespace HelloApp { struct User { public string name; public int age; public User(string name, int age) { this.name = name; this.age = age; } public void DisplayInfo() { Console.WriteLine($"Name: {name} Age: {age}"); } } class Program { static void Main(string args) { User tom = new User("Tom", 34); tom.DisplayInfo(); User bob = new User(); bob.DisplayInfo(); Console.ReadKey(); } } }

Важно учитывать, что если мы определяем конструктор в структуре, то он должен инициализировать все поля структуры, как в данном случае устанавливаются значения для переменных name и age.

Также, как и для класса, можно использовать инициализатор для создания структуры:

User person = new User { name = "Sam", age = 31 };

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

Struct User { public string name = "Sam"; // ! Ошибка public int age = 23; // ! Ошибка public void DisplayInfo() { Console.WriteLine($"Name: {name} Age: {age}"); } }

Структура - это удобное хранилище для разнородных данных, которые хочется объединить. К примеру, вы можете создать структуру, описывающую параметры вашего устройства - сетевые настройки, таймаут спящего режима, его идентификатор и прочее подобное, типа какой-нибудь строки приветствия и состояния светодиода. Раз все параметры будут храниться в одном месте - они всегда будут на виду, да и нормальные 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++, мы рассмотрим тип данных подобный классу — структуры. Структуры полезны, когда нам надо объединить несколько переменных с разными типами под одним именем. Это делает программу более компактной и более гибкой для внесения изменений. Также структуры незаменимы, когда необходимо сгруппировать некоторые данные, например, запись из базы данных или контакт из книги адресов. В последнем случае структура будет содержать такие данные контакта как имя, адрес, телефон и т.п.

Синтаксис

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

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(); }

Структуры

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

Для разрешения подобных затруднений в C# предусмотрена структура , которая подобна классу, но относится к типу значения, а не к ссылочному типу данных. Т.е. структуры отличаются от классов тем, как они сохраняются в памяти и как к ним осуществляется доступ (классы - это ссылочные типы, размещаемые в куче, структуры - типы значений, размещаемые в стеке), а также некоторыми свойствами (например, структуры не поддерживают наследование). Из соображений производительности вы будете использовать структуры для небольших типов данных. Однако в отношении синтаксиса структуры очень похожи на классы.

Главное отличие состоит в том, что при их объявлении используется ключевое слово struct вместо class. Ниже приведена общая форма объявления структуры:

struct имя: интерфейсы { // объявления членов }

где имя обозначает конкретное имя структуры.

Как и у классов, у каждой структуры имеются свои члены: методы, поля, индексаторы, свойства, операторные методы и события. В структурах допускается также определять конструкторы, но не деструкторы. В то же время для структуры нельзя определить конструктор, используемый по умолчанию (т.е. конструктор без параметров). Дело в том, что конструктор, вызываемый по умолчанию, определяется для всех структур автоматически и не подлежит изменению. Такой конструктор инициализирует поля структуры значениями, задаваемыми по умолчанию. А поскольку структуры не поддерживают наследование, то их члены нельзя указывать как abstract, virtual или protected.

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

Давайте рассмотрим пример использования структур:

Using System; namespace ConsoleApplication1 { // Создадим структуру struct UserInfo { public string Name; public byte Age; public UserInfo(string Name, byte Age) { this.Name = Name; this.Age = Age; } public void WriteUserInfo() { Console.WriteLine("Имя: {0}, возраст: {1}",Name,Age); } } class Program { static void Main() { UserInfo user1 = new UserInfo("Alexandr", 26); Console.Write("user1: "); user1.WriteUserInfo(); UserInfo user2 = new UserInfo("Elena",22); Console.Write("user2: "); user2.WriteUserInfo(); // Показать главное отличие структур от классов user1 = user2; user2.Name = "Natalya"; user2.Age = 25; Console.Write("\nuser1: "); user1.WriteUserInfo(); Console.Write("user2: "); user2.WriteUserInfo(); Console.ReadLine(); } } }

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

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

Назначение структур

В связи с изложенным выше возникает резонный вопрос: зачем в C# включена структура, если она обладает более скромными возможностями, чем класс? Ответ на этот вопрос заключается в повышении эффективности и производительности программ. Структуры относятся к типам значений, и поэтому ими можно оперировать непосредственно, а не по ссылке. Следовательно, для работы со структурой вообще не требуется переменная ссылочного типа, а это означает в ряде случаев существенную экономию оперативной памяти.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: