Массив хранит много значений одного типа. 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};. Именованная форма называет каждое поле, поэтому переживает последующую перестановку полей в структуре, а неупомянутые поля обнуляются.