Язык C довольно прост в том плане, что требует изучения всего пары основных концепций организации памяти. Это размер данных и указатель. А далее у вас есть полная свобода, чтобы манипулировать ими как угодно.
В то же время некоторые особенности синтаксиса могут вызвать недопонимание. Рассмотрим, например, структуры.
Структура это просто набор данных, такой же как массив. Единственное отличие в том, что массив содержит элементы одного размера с доступом к ним по индексу, а структура может содержать элементы разного размера, и у каждого элемента есть своё имя.
При создании структуры можно использовать три варианта синтаксиса, каждый из которых имеет своё предназначение. Чтобы не запутаться в них, вспомним, как создаётся переменная примитивного типа:
int foo;
Это мы объявили переменную foo с типом int. Собственно тип нужен, чтобы сообщить компилятору размер этой переменной (4 байта на современных машинах). Теперь начнём пошагово, чтобы было понятно, вводить тип-структуру:
struct foo;
Это мы как бы объявили переменную foo с типом "структура". Как бы – потому что такой тип тоже должен иметь размер, поэтому уточняем, из чего конкретно он состоит:
struct { int x; int y; } foo;
Тип struct { int x; int y; } это прямо вот целиком такой тип, который описывает структуру с двумя полями x и y и имеет размер 8 байт. Просто представьте его вместо int. Обычно пишут так:
В таком виде кажется, что это описание какой-то отдельной сущности, но нет, это просто создана переменная foo с таким вот типом, и этой переменной мы уже можем пользоваться:
printf("%d", foo.x);
Чтобы было ещё нагляднее, представим передачу параметра с таким типом в функцию:
И компилятор это примет. Правда, реально передать параметр такого типа будет очень проблематично. Да и очевидно, что так записывать неудобно, поэтому сразу перейдём к следующему варианту:
Это наиболее распространённая запись, и её отличие в том, что foo пишется два раза. Зачастую новички не разбираются и просто пишут как подглядели в примерах, типа, ну раз там так написано, то и я напишу. Становится реально непонятно, что, где и зачем.
Здесь мы по-прежнему создали переменную foo с типом "структура". Но теперь структура имеет собственное имя, которое записано после struct:
struct foo
Имя структуры не имеет никакого отношения к переменной foo, и то, что они одинаковые, всего лишь чья-то прихоть.
Я предпочитаю следовать практикам ООП и называть сложные типы с большой буквы и по делу, например:
В таком виде становится понятно, что мы объявили переменную foo с типом struct Point. Обратите внимание: это не тип Point, а тип struct Point.
Именованные структуры позволяют создавать больше таких переменных, не повторяя само описание структуры:
struct Point bar;
И спокойно передавать их как параметры в функции:
Также теперь описание структуры может быть самостоятельным (обратите внимание, что переменная foo уже не создаётся):
И последний вариант.
В языке C мы можем вводить собственные типы, давая уже существующим типам альтернативные имена (алиасы). Начнём опять с примера для int:
typedef int Number;
Здесь мы с помощью typedef назначили типу int алиас Number и далее можем им пользоваться:
Number foo = 5;
Совершенно аналогично можем назначить алиас и struct-типу:
Теперь мы можем создать переменную и так:
struct Point foo;
и так:
Point foo;
В первом случае переменная foo имеет тип struct Point, а во втором – тот же самый тип, просто с алиасом Point. Как и ранее, повторение Point два раза это прихоть программиста, а не требование.
Если вы уже освоились, то догадаетесь, в чём тут дело:
В этом варианте мы назначили алиас Point безымянной структуре. И значит, по-прежнему можем написать:
Point foo;
но не можем написать:
struct Point foo;
Потому что структуры с названием Point уже нет.
Рекомендую изучать эти варианты до полного понимания. Зубрить их не надо, надо понимать.
Структуры и указатели
Доступ к полям структуры делается через точку:
printf("%d, %d", foo.x, foo.y);
Но только в том случае, если foo не указатель.
Рассмотрим передачу указателя на структуру в функцию:
В случае, когда переменная это указатель на структуру, доступ к её полям делается через ->. Причины такого решения я не знаю, но вероятно что-то связанное с облегчением жизни для компилятора.
В то же время это немного упрощает жизнь и программисту. Когда мы видим ->, то понимаем это как "возьми поле структуры оттуда, по указателю". А когда видим точку, то понимаем это как "возьми поле структуры прямо отсюда".
Полиморфизм структур
Возвращаясь к началу,
Язык 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, но вы используете для неё компилятор 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 байт, и коллекция таких структур может занимать больше памяти.
Есть способы управления этими параметрами, но о них поговорим отдельно.
Читайте дальше: