Menu

Структуры в C: объявление, инициализация и использование struct

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Массив хранит много значений одного типа. struct делает противоположное: он хранит несколько значений разных типов и обращается с ними как с одной вещью. Точка — это x и y. Сотрудник — это имя, идентификатор и зарплата. Без структур вы носили бы всё это как три отдельные переменные и надеялись, что никогда не перепутаете, чья зарплата чья.

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

Объявление структуры называет тег и перечисляет поля в фигурных скобках:

struct Point {
    int x;
    int y;
};

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

Это объявляет тип, а не переменную. В C полное имя типа — struct Point, вместе с ключевым словом:

Поля читаются и пишутся через оператор точка: p.x. Переменная-структура — обычная переменная: она живёт на стеке, её можно присвоить другой структуре того же типа через =, и она исчезает в конце своей области видимости.

Писать struct Point повсюду утомительно, поэтому почти весь реальный код на C оборачивает объявление в typedef, чтобы тип назывался просто Point.

Инициализация структуры

Структуру можно заполнить прямо в момент объявления. Позиционная форма перечисляет значения в порядке объявления полей:

struct Point p = {3, 4};   // x = 3, y = 4

Именованный инициализатор вместо этого называет каждое поле, что яснее и безопаснее:

Три момента, на которые стоит обратить внимание:

  • Именованная форма может перечислять поля в любом порядке, поэтому добавление или перестановка поля позже не сдвинет значения в чужие ячейки.
  • Любое пропущенное поле обнуляется; {0} — идиоматический способ обнулить всю структуру целиком.
  • Поле char name[32] — это настоящий массив внутри структуры, поэтому строка хранится в самой структуре, а не где-то ещё. Отсюда же следует, что a.name = "Ada"; задним числом недопустимо: нужен strcpy (см. функции для строк).

Структура без инициализатора не инициализирована, и чтение её полей до записи — неопределённое поведение. = {0} не стоит ничего и снимает весь этот класс багов.

Присваивание и сравнение

Присваивание структуры копирует каждое поле:

a осталась нетронутой: b получила собственную копию. Но обратите внимание, чего C не даёт: для структур нет ==. if (a == b) не скомпилируется. Сравнение означает сравнение полей:

if (a.x == b.x && a.y == b.y) { /* равны */ }

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

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

Поскольку структура — такой же тип, как и любой другой, из них можно сделать массив: это стандартный способ хранить таблицу записей.

staff[i].salary читается как «элемент i, а затем его поле salary»: индексация связывает сильнее точки, поэтому скобки не нужны.

Структуры копируются при передаче в функции

Это то правило, на котором спотыкаются. Когда вы передаёте структуру в функцию, C её копирует. Функция работает со своей собственной копией:

p по-прежнему (3, 4). Читать структуру по значению, как это делает distance_from_origin, совершенно нормально и для небольших структур часто самый понятный вариант. Но если функция должна изменить структуру вызывающего — или если структура настолько велика, что копировать её расточительно, — передавайте указатель. Этому посвящена статья структуры и указатели.

Возврат структуры по значению работает так же и совершенно законен:

struct Point make_point(int x, int y) {
    struct Point p = {x, y};
    return p;
}

Вложенные структуры

Поле структуры само может быть структурой. Чтобы добраться внутрь, соединяйте точки в цепочку:

b.published.year заходит внутрь по одному уровню за раз. Вложенность — то, как в C строят настоящие модели данных, причём внутренняя структура хранится внутри внешней, а не по указателю, поэтому вся Book — один непрерывный блок памяти.

Размер, выравнивание и порядок полей

sizeof сообщает размер структуры, и ответ часто больше суммы размеров её полей:

На типичной машине Wasteful занимает 12 байт, а Tidy — 8, хотя обе хранят одни и те же три поля. Компилятор вставляет выравнивающие байты, чтобы каждое поле оказалось по адресу, который нравится его типу: int обычно хочет адрес, кратный 4. Группировка более крупных полей перед мелкими обычно упаковывается плотнее.

Это редко имеет значение (массив на миллион элементов, раскладка сетевого пакета) и никогда не влияет на корректность вашего собственного кода. Зато объясняет, почему sizeof вас удивляет и почему memcmp на структурах — плохая идея.

Часто задаваемые вопросы

Как объявить структуру в C?

Напишите struct, затем имя тега и список полей в фигурных скобках: struct Point { int x; int y; };. Это объявляет тип, а не переменную. Чтобы завести переменную, пишут struct Point p; — слово struct в C является частью имени типа, поэтому в большинстве кода его сочетают с typedef.

Чем отличаются . и -> для структур?

Точка используется, когда у вас есть сама структура (p.x), а стрелка — когда у вас указатель на неё (ptr->x). Стрелка — сокращение для (*ptr).x. Подробности — в статье структуры и указатели.

Структуры в C передаются по значению или по ссылке?

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

Как инициализировать структуру в C?

Либо по позиции — struct Point p = {3, 4};, либо именованными инициализаторами — struct Point p = {.y = 4, .x = 3};. Именованная форма называет каждое поле, поэтому переживает последующую перестановку полей в структуре, а неупомянутые поля обнуляются.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ