C инициализация структуры при объявлении. Структуры. Что ещё важно знать
Последнее обновление: 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}"); } }
Структуры
Как вам должно быть уже известно, классы относятся к ссылочным типам данных. Это означает, что объекты конкретного класса доступны по ссылке, в отличие от значений простых типов, доступных непосредственно. Но иногда прямой доступ к объектам как к значениям простых типов оказывается полезно иметь, например, ради повышения эффективности программы. Ведь каждый доступ к объектам (даже самым мелким) по ссылке связан с дополнительными издержками на расход вычислительных ресурсов и оперативной памяти.
Для разрешения подобных затруднений в 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# включена структура, если она обладает более скромными возможностями, чем класс? Ответ на этот вопрос заключается в повышении эффективности и производительности программ. Структуры относятся к типам значений, и поэтому ими можно оперировать непосредственно, а не по ссылке. Следовательно, для работы со структурой вообще не требуется переменная ссылочного типа, а это означает в ряде случаев существенную экономию оперативной памяти.
Пока мы рассматривали одну сложную структуру (сложный тип) - массив; одним из основных свойств массива является однотипность его компонент. Многие информационно-логические задачи связаны с обработкой документов, содержащих в себе информация разного типа (числовую, символьную и т. д.) Примеры таких документов: платежные ведомости (фамилии и имена - символьная информация, денежные суммы - числовая), карточки больных в поликлинике, библиотечная информация. Для программирования алгоритмов обработки такой информации необходимо иметь сложный тип, объединяющий разнотипные компоненты. Таким типом является структура в Си (в Паскале запись).
Структурная переменная, или просто структура, состоит из нескольких переменных (называемых полями), возможно, разного типа.
Структура
тип "структура" (шаблон) |
переменная типа "структура " |
Описание шаблона : |
Описание структурной переменной |
typedef struct { Тип1 Список1ИменПолей; |
struct ИмяШаблона ИмяПеременной |
Тип2 Список2ИменПолей; | |
ТипN СписокNИменПолей; |
ключевое struct слово не нужно пpи |
} ИмяШаблона |
использовании typedef |
или struct ИмяШаблона | |
{ Тип1 Список1ИменПолей;¦ | |
Тип2 Список2ИменПолей; | |
ТипN СписокNИменПолей; | |
typedef struct {char author; char title;/*описание*/
int year; float price} BOOK; /*шаблона BOOK*/
/*или можно описать тот же самый шаблон так:
struct BOOK {char author; char title;
int year; float price} ;*/
struct BOOK b;/*описание структурной переменной b*/
Память, занимаемая структурой, равна сумме объемов памяти полей (если исключить из рассмотрения особенности, связанные с выравниванием). В любом случае для определения размера памяти структуры можно использовать операцию sizeof(). Шаблон ВООК, например, описывает структуру размером памяти 70.
Обращение к полю структурной переменной:
ИмяСтруктуры.ИмяПоля или АдресСтруктуры ->ИмяПоля
. (точка) и ->являются операциями, соответственно, прямого и косвенного выбора компоненты структурированной переменной.
Например,
struct BOOK a,*pnta=&a;...
a.author="Byron"; pnta->author="Byron"; /*эквивалентные операторы*/
Пример. Задача примера 2 п.3.1.4 в нижеприведенной программе выполнена с использованием структур (а не строк).
#include
#include
#include
{ /*структура сведений об игрушках*/
typedef struct {int nu;/*номер*/
char name;/*наименование*/
int minage,maxage;/*мин. и макс. возраст ребенка*/
double rub /*стоимость*/;}TOYS;
TOYS toy;/*переменная типа записьTOYS */
double max; /*максимальная стоимость*/
char namemax;/*название самого дорогого конструктора*/
int n /*число игрушек*/,i/*номер игрушки*/;
puts("Введите число наименований игpушек");
for (i=0;
i /*в
цикле ввод сведений об игрушках и
проверка условий*/ fflush(stdin); /*очистка буфера
устройства ввода послеscanf
*/ printf("
Введите сведения об игpушке с номеpом
%2d\n",toy.nu); puts("наименование"); puts("мин. и макс. возpаст и стоимость"); scanf("%d%d%lf",&toy.minage,&toy.maxage,&toy.rub); if
((strstr(toy.name,"констpуктоp")!=NULL || strstr(toy.name,"Констpуктоp")!=NULL) && (toy.maxage <= 7) && (toy.rub>max)) strcpy(namemax,toy.name); puts(" Констpуктоpов для детей до семи
лет нет"); {
printf("Cамый доpогой констpуктоp для
детей до семи лет\n"); printf(" %s стоит %8.0f pублей\n",namemax,max); В Си существует еще один сложный тип,
описание которого формально похоже на
структуру. Это тип (и переменная)
объединение
. Объединение - это переменная, содержащая
поля разного типа, помещаемые в одно и
то же место памяти. По существу объединение
дает способ различной интерпретация
содержимого памяти. Описание шаблона
(типа) объединения и переменной этого
типа выполняется также, как для структуры,
только вместо ключевого слова struct
используетсяunion
. Размер памяти,
занимаемой объединением, равен
максимальному из размеров полей. Последнее обновление: 09.10.2017 Ранее для определения классов мы использовали ключевое слово class
. Однако C++ предоставляет еще один способ
для определения пользовательских типов, который заключается в использовании структур. Данный способ был унаследован языком С++ еще от языка Си. Структура в языке C++ представляет собой производный тип данных, который представляет какую-то определенную сущность, также как и класс.
Нередко структуры применителько к С++ также называют классами. И в реальности различия между ними не такие большие. Для определения структуры применяется ключевое слово struct
, а сам формат определения выглядит следующим образом: Struct имя_структуры
{
компоненты_структуры
};
Имя_структуры
представляет произвольный идентификатор, к которому применяются те же правила, что и при наименовании переменных. После имени структуры в фигурных скобках помещаются Компоненты_структуры
, которые представляют набор описаний объектов и функций, которые составляют структуру. Например, определим простейшую структуру:
#include Здесь определена структура person , которая имеет два элемента: age (представляет тип int) и name
(представляет тип string). После определения структуры мы можем ее использовать. Для начала мы можем определить объект структуры - по сути обычную переменную, которая будет
представлять выше созданный тип. Также после создания переменной структуры можно обращаться к ее элементам - получать их значения или, наоборот, присваивать им новые значения.
Для обращения к элементам структуры используется операция "точка": Имя_переменной_структуры.имя_элемента
По сути структура похожа на класс, то есть с помощью структур также можно определять сущности для использования в программе. В то же время
все члены структуры, для которых не используется спецификатор доступа (public, private), по умолчанию являются открытыми (public).
Тогда как в классе все его члены, для которых не указан спецификатор доступа, являются закрытыми (private). Кроме того мы можем инициализировать структуру, присвоив ее переменным значения с помощью синтаксиса инициализации: Person tom = { 34, "Tom" };
Инициализация структур аналогична инициализации массивов: в фигурных скобках передаются значения для элементов структуры по порядку. Так как
в структуре person первым определено свойство, которое представляет тип int - число, то в фигурных скобках вначале идет число.
И так далее для всех элементов структуры по порядку. При этом любой класс мы можем представить в виде структуры и наоборот. Возьмем, к примеру, следующий класс: Class Person
{
public:
Person(std::string n, int a)
{
name = n; age = a;
}
void move()
{
std::cout << name << " is moving" << std::endl;
}
void setAge(int a)
{
if (a > 0 && a < 100) age = a;
}
std::string getName()
{
return name;
}
int getAge()
{
return age;
}
private:
std::string name;
int age;
};
Данный класс определяет сущность человека и содержит ряд приватных и публичных переменных и функции. Вместо класса для определения той же сущности мы могли
бы использовать структуру:
#include И в плане конечного результата программы мы не увидели бы никакой разницы. Когда использовать структуры? Как правило, структуры используются для описания таких данных, которые имеют только набор публичных атрибутов - открытых переменных. Например, как та же структура person, которая
была определена в начале статьи. Иногда подобные сущности еще называют аггрегатными классами
(aggregate classes). Структура - это удобное хранилище для разнородных данных, которые хочется объединить. К примеру, вы можете создать структуру, описывающую параметры вашего устройства - сетевые настройки, таймаут спящего режима, его идентификатор и прочее подобное, типа какой-нибудь строки приветствия и состояния светодиода. Раз все параметры будут храниться в одном месте - они всегда будут на виду, да и нормальные 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
. Компилятор заменяет все записи вида sizeof X на значение длины Х. В качестве X может выступать как тип, так и экзмепляр типа, т.е. в нашем случае можно подставить в sizeof и тип структуры (если мы его заводили с помощью typedef), и саму переменную структуры так: sizeof params_struct или sizeof params. Она пройдёт по всем полям структуры, сложит их длины и отдаст сумму, которая и будет длиной структуры. offsetof
- настоящий макрос, который принимает два параметра (структуру _s_ и поле _m_ в ней) и отдаёт положение этого поля в структуре, его смещение относительно начала структуры. Выглядит этот макрос очень просто: Offsetof(s, m) (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))Хранение, передача и синхронизация структур
sizeof и offsetof
Работа с массивом из структуры
Хранение и передача структуры
Приём и восстановление структуры
Хранение/восстановление отдельных полей