Посмотр рубрик

Структуры

В C/C++ есть возможность создавать составные пользовательские типы данных - struct (структура). Структура позволяет объединить под одним именем несколько переменных любых типов, в том числе вложенных структур и массивов.

Объявление #

Объявить структуру можно где угодно: глобально, внутри функции или внутри класса. Синтаксис такой:

struct имя_типа {
    тип_данных переменная;
    // ...
};

Переменная внутри структуры называется членом структуры (member), иногда также называют свойством (property) структуры

Такая запись создаёт именно тип данных - переменные не создаются, память не выделяется. Это - просто каркас, шаблон, по нему уже можно создать экземпляр структуры по обычным правилам создания переменных:

имя_типа имя_экземпляра;

Обратиться ко вложенным членам структуры можно при помощи оператора точка .:

имя_экземпляра.переменная;

Пример:

struct MyStruct {
    int a;
    float f;
    long arr[5];
};

MyStruct s;
s.a = 123;
s.f = 3.14;
s.arr[0] = 12345;

Сразу создать экземпляр #

Можно создать экземпляр сразу после объявления структуры, указав имя после завершающей }:

struct Struct {
    int a;
} myStruct;

myStruct.a = 123;

Именно поэтому в конце объявления структуры ставится ; - объявление структуры - это создание и сразу использование нового типа данных. По аналогии например с типом int, строка int приведёт к ошибке компиляции, а int; - нет, ровно как и int a;. Объявление структуры работает аналогичным образом.

Анонимная структура #

Синтаксис позволяет создавать некоторые сущности (структуры, перечисления, объединения) анонимно, то есть без названия имени типа - сразу сделать переменную: struct {} имя_переменной. Пример:

struct {
    int a;
} myStruct;

myStruct.a = 123;

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

В примере выше члены структуры не инициализированы и будут вести себя как переменные: при глобальном создании экземпляра переменные будут автоматически инициализированы стандартными значениями. При создании локально - нет.

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

struct MyStruct {
    int a = 0;
    float f = 3.14;
    long arr[5] = {0};
};

MyStruct s;     // инициализированы

int main() {
    MyStruct s2;    // инициализированы
}

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

struct MyStruct {
    int a;
    float f;
    long arr[5];
};

MyStruct s = {};    // инициализировать нулями (значениями по умолчанию для типов)

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

MyStruct s = {12, 3.14, 1, 2};
// s.a == 12
// s.f == 3.14
// arr == {1, 2, 0, 0, 0}

MyStruct s2 = {12};

Можно встретить и другой синтаксис, он тоже корректный:

struct Color {
    uint8_t r, g, b;
};

Color color1{1, 2, 3};
Color color2 = {1, 2, 3};
Color color3 = (Color){1, 2, 3};
Color color4 = {.r = 1, .g = 2, .b = 3};
Color color5 = {r: 1, g: 2, b: 3};

Примечание: последние два примера инициализации выше называются designated initializers. В языке C каких-то версий возможно указывать члены в произвольном порядке. В C++ - нельзя, члены должны идти именно в том порядке, в котором они располагаются в структуре. Такая запись просто повышает читаемость программы даже без подсказок IDE

Присвоение #

Один экземпляр структуры можно присвоить к другому - все члены перепишутся, как это и ожидается:

MyStruct s;
MyStruct s2 = s;

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

MyStruct s;
s = (MyStruct){12, 3.14};

Или пересоздать со значениями по умолчанию (которые указаны при определении самой структуры):

s = MyStruct();

Сравнение #

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

MyStruct s1, s2;
s1 == s2;   // ошибка

Сравнивать структуры нужно по отдельным членам:

struct Foo {
    int i;
    float f;
};

Foo foo1{123, 3.14};
Foo foo2{123, 3.14};
Foo foo3{456, 3.14};

(foo1.i == foo2.i && foo1.f == foo2.f); // true
(foo1.i == foo3.i && foo1.f == foo3.f); // false

Две структуры можно сравнить и через memcmp - memcmp(&foo1, &foo2, sizeof(Foo)), но для общего случая это не подходит: между членами структуры могут быть неинициализированные байты выравнивания, а одинаковые по смыслу значения не всегда имеют одинаковое представление в памяти - это касается float, указателей и некоторых других типов.

Указатель на структуру #

Как и на любой другой тип данных, на структуру можно сослаться по указателю или ссылке. Если доступ по ссылке в синтаксисе ничего не меняет (остаётся через точку), то у доступа к члену по указателю на экземпляр вместо точки используется оператор косвенного обращения - ->:

MyStruct s;

// ссылка
MyStruct& ref = s;
ref.a = 123;

// указатель
MyStruct* ptr = &s;
ptr->a = 123;
(*ptr).a = 456;     // либо так

foo->bar равносильно (*foo).bar - просто более лаконичная запись

Передача в функцию #

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

// по значению 
void byVal(MyStruct s) {
    // здесь s - независимая локальная копия структуры
}

// по ссылке
void byRef(MyStruct& s) {
    // здесь s - другое имя оригинальной структуры
}

// по указателю
void byPtr(MyStruct* s) {
    // здесь s - указатель на другую структуру
}

MyStruct s;
byVal(s);
byRef(s);
byPtr(&s);

При передаче структуры по значению создаётся независимая копия. Все действия, проделанные с ней, не отражаются на оригинальной структуре. Для крупных структур передача по ссылке или указателю обычно позволяет избежать копирования, функция при этом получает доступ к оригинальному экземпляру. Если функция не будет менять структуру, то можно передавать её как const имя_структуры& - по константной ссылке.

Вложенная структура #

Синтаксис вложенных структур весьма логичный:

struct Color {
    uint8_t r, g, b;
};

struct Foo {
    int a;
    Color col;
};

Foo foo;
foo.a = 123;
foo.col.r = 255;

Также можно делать через анонимные структуры:

struct Foo {
    int a;
    struct {
        uint8_t r, g, b;
    } col;
};

Foo foo;
foo.a = 123;
foo.col.r = 255;

Возврат из функции #

Структуру можно вернуть из функции:

MyStruct foo(int var) {
    MyStruct s;
    s.a = var;
    return s;
}

MyStruct s = foo(123);

По сути это позволяет возвращать из функции "несколько значений", чего нет в C/C++, как например в Python

Также для этого можно использовать анонимные структуры - без названия, для удобства обращаться к ним через тип auto. Это довольно грязно, но можно встретить на практике:

auto getFoo(int x) {
    return (struct { int a, b; }){x, x * 10};
}

int main() {
    auto foo = getFoo(5);
    // foo.a == 5
    // foo.b == 50
}

Массив структур #

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

struct Color {
    uint8_t r, g, b;
};

Color colors[5];
colors[0].r = 123;

Инициализировать также можно:

Color colors[5] = {
    {1, 2, 3},
    {255, 255, 0},
};

colors[0].r = 123;

Можно создать и анонимно:

struct {
    int a;
} arr[] = {1, 2, 3};

arr[0].a = 123;
arr[1].a = 456;
//arr[2].a == 3

Выравнивание* #

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

// выравнивание 2 байта
MyStruct {
    uint8_t v1;     // 0
    uint8_t v2;     // 1
    uint16_t v3;    // 2
    uint8_t v4;     // 4
    uint16_t v5;    // 6 - вот тут пропустили байт!
};

// выравнивание 4 байта
MyStruct {
    uint16_t v0;    // 0
    uint32_t v1;    // 4 - пропустили 2 байта
    uint8_t v2;     // 8
    uint32_t v3;    // 12 - пропустили 3 байта
};

Это к тому, что структура занимает в памяти не столько места, сколько кажется на первый взгляд при простом сложении весов членов. Измерить вес структуры всегда можно при помощи sizeof(имя). Понимая, как работает выравнивание, можно составить структуру так, чтобы она занимала меньше места в памяти.

Чтобы структура занимала меньше места в памяти, её члены часто выгодно располагать в порядке убывания требований к выравниванию, а проверить результат можно через sizeof

Также это означает, что на разных архитектурах данные могут храниться в памяти по разному, и при передаче структуры между устройствами в "сыром" виде данные могут записаться некорректно

Упаковка #

Можно дать команду компилятору, чтобы он принудительно упаковал структуру без пропусков. Доступ к её членам может работать медленнее, а на некоторых архитектурах потребует нескольких операций. Есть два распространённых варианта:

// для GCC/Clang
struct __attribute__((packed)) Foo1 {
    char a;
    long b;
    char c;
};
#pragma pack(push, 1)
struct Foo2 {
    char a;
    long b;
    char c;
};
#pragma pack(pop)

Обе структуры занимают в памяти 6 байт, несмотря на выравнивание.

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

typedef struct в Си #

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

struct MyStruct {
};

MyStruct s;

В Си же это не работает и есть два варианта:

struct MyStruct {
};

struct MyStruct s;  // добавляется слово struct

Либо создаётся "новый тип" через typedef:

typedef struct {    // превращается в тип через typedef
} MyStruct;

MyStruct s;

В чужом C++ коде тоже можно встретить такую запись - это не ошибка, просто автор не определился, на каком языке он пишет.

Дополнительно #

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

  • Тезисы, Задания, Примеры (Arduino)
  • 3 блоков кода

Полезные страницы #


(4 голоса)
Подписаться
Уведомить о
guest

2 комментариев
Старые
Новые Популярные
Прокрутить вверх