4,7K подписчиков

Структуры в языке C

397 прочитали

Язык C довольно прост в том плане, что требует изучения всего пары основных концепций организации памяти. Это размер данных и указатель. А далее у вас есть полная свобода, чтобы манипулировать ими как угодно.

В то же время некоторые особенности синтаксиса могут вызвать недопонимание. Рассмотрим, например, структуры.

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

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

int foo;

Это мы объявили переменную foo с типом int. Собственно тип нужен, чтобы сообщить компилятору размер этой переменной (4 байта на современных машинах). Теперь начнём пошагово, чтобы было понятно, вводить тип-структуру:

struct foo;

Это мы как бы объявили переменную foo с типом "структура". Как бы – потому что такой тип тоже должен иметь размер, поэтому уточняем, из чего конкретно он состоит:

struct { int x; int y; } foo;

Тип struct { int x; int y; } это прямо вот целиком такой тип, который описывает структуру с двумя полями x и y и имеет размер 8 байт. Просто представьте его вместо int. Обычно пишут так:

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

В таком виде кажется, что это описание какой-то отдельной сущности, но нет, это просто создана переменная foo с таким вот типом, и этой переменной мы уже можем пользоваться:

printf("%d", foo.x);

Чтобы было ещё нагляднее, представим передачу параметра с таким типом в функцию:

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

И компилятор это примет. Правда, реально передать параметр такого типа будет очень проблематично. Да и очевидно, что так записывать неудобно, поэтому сразу перейдём к следующему варианту:

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

Это наиболее распространённая запись, и её отличие в том, что foo пишется два раза. Зачастую новички не разбираются и просто пишут как подглядели в примерах, типа, ну раз там так написано, то и я напишу. Становится реально непонятно, что, где и зачем.

Здесь мы по-прежнему создали переменную foo с типом "структура". Но теперь структура имеет собственное имя, которое записано после struct:

struct foo

Имя структуры не имеет никакого отношения к переменной foo, и то, что они одинаковые, всего лишь чья-то прихоть.

Я предпочитаю следовать практикам ООП и называть сложные типы с большой буквы и по делу, например:

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

В таком виде становится понятно, что мы объявили переменную foo с типом struct Point. Обратите внимание: это не тип Point, а тип struct Point.

Именованные структуры позволяют создавать больше таких переменных, не повторяя само описание структуры:

struct Point bar;

И спокойно передавать их как параметры в функции:

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

Также теперь описание структуры может быть самостоятельным (обратите внимание, что переменная foo уже не создаётся):

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

И последний вариант.

В языке C мы можем вводить собственные типы, давая уже существующим типам альтернативные имена (алиасы). Начнём опять с примера для int:

typedef int Number;

Здесь мы с помощью typedef назначили типу int алиас Number и далее можем им пользоваться:

Number foo = 5;

Совершенно аналогично можем назначить алиас и struct-типу:

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

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

struct Point foo;

и так:

Point foo;

В первом случае переменная foo имеет тип struct Point, а во втором – тот же самый тип, просто с алиасом Point. Как и ранее, повторение Point два раза это прихоть программиста, а не требование.

Если вы уже освоились, то догадаетесь, в чём тут дело:

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

В этом варианте мы назначили алиас Point безымянной структуре. И значит, по-прежнему можем написать:

Point foo;

но не можем написать:

struct Point foo;

Потому что структуры с названием Point уже нет.

Рекомендую изучать эти варианты до полного понимания. Зубрить их не надо, надо понимать.

Структуры и указатели

Доступ к полям структуры делается через точку:

printf("%d, %d", foo.x, foo.y);

Но только в том случае, если foo не указатель.

Рассмотрим передачу указателя на структуру в функцию:

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

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

В то же время это немного упрощает жизнь и программисту. Когда мы видим ->, то понимаем это как "возьми поле структуры оттуда, по указателю". А когда видим точку, то понимаем это как "возьми поле структуры прямо отсюда".

Полиморфизм структур

Возвращаясь к началу,

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

Структура:

struct Point { int x; int y; };

имеет размер 8 байт и логически делится на 2 поля по 4 байта. Можно создать ещё одну структуру:

struct Char8 {
char c1; char c2; char c3; char c4; char c5; char c6; char c7; char c8;
};

И она тоже будет иметь размер 8 байт.

Хотя компилятор считает их двумя разными типами и не даст сделать так:

struct Point foo;
struct Char8 bar;
bar = foo;

Мы тем не менее можем привести один тип к другому с помощью указателя:

bar = *((struct Char8*) &foo);

Рассмотрим эту операцию поэтапно. Сначала мы получаем указатель на foo:

&foo

Этот указатель имеет тип struct Point*, но мы приводим его к struct Char8*:

(struct Char8*) &foo

Затем берём значение памяти по этому указателю:

*((struct Char8*) &foo)

И копируем его в bar:

bar = *((struct Char8*) &foo);

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

Копирование cтруктур

Как можно понять из примера выше, при присваивании одной структуры другой происходит просто копирование памяти такого размера, как у целевой структуры.

Компилятор поступает довольно хитро, так его волнует лишь размер, но не логическое устройство структуры.

Например, чтобы скопировать одну структуру Сhar8 в другую структуру Char8, компилятор не будет делать это по одному полю: foo.c1 = bar.c1; foo.c2 = bar.c2 и т.д. Вместо этого он возьмет все 8 байт как одно 64-битное число и перебросит его за один раз в место назначения.

Если структура длиннее, то компилятор может перебросить её за 2 раза или использовать какие-то другое способы.

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

memcpy(&bar, &foo, sizeof(foo));

Функция memcpy() копирует память из одного места в другое (в данном случае из указателя &foo в указатель &bar), и нужно только задать, сколько памяти скопировать: sizeof(foo). Псевдо-функция sizeof() возвращает размер данных, известный на этапе компиляции.

Для memcpy() вообще не играет роли, структуры каких типов мы используем, и структуры ли это вообще, она просто копирует память из одного указателя в другой. Она также оптимизирована для переброски больших объёмов памяти, но для маленьких структур может быть неэффективной именно ввиду этой оптимизации, для которой требуется предварительная подготовка.

Поэтому лучше просто писать foo = bar, когда это возможно, и компилятор сам решит, как это лучше скопировать.

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

В чистом C нельзя присваивать полям структуры начальные значения прямо во время её объявления:

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

Но это можно сделать, если программа написана на C, но вы используете для неё компилятор C++ :) То есть фактически вы пишете уже на C++.

Если же вы придерживаетесь чистого C, то инициализировать поля структуры придётся при создании переменной:

struct Point foo = { 5, 10 };

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

memset(&foo, 0, sizeof(foo));

Функция memset() подобна функции memcpy(), но она не копирует данные, а просто побайтово заполняет память указанным значением. Кроме 0, можно использовать и другие значения, но это редко бывает полезно.

Упрощённый вариант memset() для маленьких структур:

*((long*) &foo) = 0;

Как и раньше, мы привели указатель &foo к типу long* (64 бита), и присвоили содержимому этого указателя 64-битное число 0, таким образом обнулив поля foo.x и foo.y за один раз. Можно пойти и дальше и присвоить к примеру (10L << 32) + 5, что будет эквивалентно foo.x = 5 и foo.y = 10.

Истинные размеры структур

Интуитивно можно сказать, что размер структуры { int x; int y; } равен 8 байт, и это практически всегда будет так. Но иногда, в силу некоторых причин, структуры могут размещаться в памяти на адресах, кратных 16, к примеру.

Поэтому, хотя фактический размер структуры и равен 8 байт, он может быть увеличен до ближайшего кратного 16 байт, и коллекция таких структур может занимать больше памяти.

Есть способы управления этими параметрами, но о них поговорим отдельно.

Читайте дальше: