Язык c. Лекция 8
1. Массивы (продолжение)
1.4. Определение новых названий типов
Существует возможность давать имена-синонимы для существующих или определяемых программистом типов:
typedef unsigned short int word;
После этого word становится синонимом типа unsigned short int, так что можно, например, написать следующее объявление переменной:
word w;
и это будет то же самое, что
unsigned short int w;
Синтаксис конструкции typedef похож на объявление переменной, массива, структуры и т.п. с той лишь разницей, что вместо имени объекта стоит имя нового типа.
Рассмотрим пример с координатами точки в пространстве. В предыдущей лекции мы использовали массив из трех элементов для хранения координат x, y, z:
double p[3];
Можно определить новое имя типа:
typedef double Point[3];
и написать
Point p;
Объявление массива координат атомов из предыдущей программы (см. раздел 1.3)
static double at[MAXAT][3]; /* массив координат атомов */
теперь можно сделать более понятным:
static Point at[MAXAT]; /* массив координат атомов */
Смысл функции dist тоже становится более ясным, если записать ее заголовок в виде:
double dist (Point p1, Point p2)
Введение новых названий типов с помощью typedef позволяет упростить сложные объявления, сделать их более понятными.
2. Структуры
2.1. Определение структур и их свойства
Структура — второй (после массива) вид сложного типа данных, состоящего из нкскольких элементов. В отличие от массива элементы структуры имеют разные типы. Каждый элемент получает собственное имя, и к индивидуальным элементам структуры обращаются по именам, а не по индексам, как в случае массивов.
Рассмотрим пример. Чтобы определить сферу, достаточно задать ее радиус r и координаты центра. Эти данные логически связаны (они являются характеристиками одного объекта), поэтому в программе их разумно объединить:
struct sphere {
double r;
double c[3];
} s;
Это объявление говорит, что переменная s представляет собой структуру, состоящую из двух элементов — r и c, причем первый элемент является числом типа double, а второй массивом из трех элементов типа double (соответственно, радиус и три координаты центра сферы). Имя sphere после ключевого слова struct — это тег (или ярлык) структуры; им можно пользоваться в других объявлениях, чтобы не дублировать полное определение структуры, заданное в фигурных скобках. Например,
struct sphere a, b;
обявляет a и b как структуры того же вида, что и s. Обратите внимание, что слово struct необходимо повторить — без него компилятор не воспримет sphere как ярлык структуры. Ярлык (тег) — необязательный элемент описания структуры; его можно опустить, если на данную структуру не требуется ссылаться из других мест программы.
Обращаясь к отдельным элементам, их имена отделяют от имени структуры точкой. Например, следующие операторы присваивают значения элементам s:
s.r = 1.5;
s.c[0] = 0.25;
s.c[1] = 0.75;
s.c[2] = 1.15;
Теперь s содержит информацию о сфере с радиусом 1.5 и центром в точке [0.25, 0.75, 1.15].
Важное замечание. Размер структуры (в отличие от массива) не обязательно равен сумме размеров ее элементов. Причина в том, что величины некоторых типов желательно размещать в памяти по адресам, кратным 2, 4, 8 или даже 16 (в зависимости от архитектуры процессора и типа данных). Если это требование не соблюдено, то данные извлекаются из памяти (или записываются в память) значительно медленнее, и производительность программы снижается. Размещение данных в памяти по адресу, кратному заданной величине, называется выравниванием. Для того, чтобы обеспечить правильное выравнивание данных, компилятор может вставлять пустые промежутки между элементами структуры. Рассмотрим, например, такую структуру:
struct {
char ch;
int n;
double val;
} q;
Суммарный размер ее элементов — 13 байтов (1+4+8). Однако компилятор Watcom C сообщает (если использовать sizeof(q)), что размер q равен 16 байтам. Дело в том, что на 32-разрядных процессорах Intel x86 значения типа int рекомендуется выравнивать по границе, кратной 4 байтам, а значения типа double — по границе, кратной 8 байтам. Значения типа char выравнивать не нужно — они могут располагаться по любому адресу. Всю структуру компилятор выравнивает в соответствии с наиболее сильным из требований для ее элементов (в нашем случае q будет расположена по адресу, кратному 8 байтам). Между элементами ch и n остается пустой промежуток длиной 3 байта, чтобы адрес n оказался кратным 4. Между n и val промежутка нет, т.к. элементы ch и n вместе с промежутком займут как раз 8 байтов.
Если поменять порядок элементов структуры:
struct {
char ch;
double val;
int n;
} q;
то ее общий размер окажется равным уже не 16, а 24 байтам: между ch и val промежуток длиной 7 байтов, между val и n промежутка нет, а после n (в конце структуры) добавятся еще 4 байта, чтобы общий размер был кратным 8. Последнее необходимо для того, чтобы несколько таких структур можно было расположить в памяти друг за другом без промежутков — например, если понадобится создать массив структур (элементы массива по определению обязаны размещаться в памяти без промежутков).
Структуры одинакового вида можно копировать (присваивать) целиком, например:
a = s;
a.r *= 0.5;
После выполнения этих операторов a и s соответствуют двум концентрическим сферам, причем радиус сферы a вдвое меньше, чем у s.
Возможность присваивания значений структур как единого целого означает в частности, что структуры можно возвращать в качестве значений функций. Если структуры служат аргументами функций, то они передаются по значению, т.е. копируются во временные переменные-структуры. Чтобы избежать такого копирования (особенно когда речь идет о структурах большого размера) часто передают в качестве аргументов не сами структуры, а указатели на них. Рассмотрим, например, функцию inside(p, s), которая проверяет, находится ли точка p внутри сферы s:
/* Для удобства введем имя типа Sphere для структуры struct sphere */
typedef struct sphere Sphere;
double dist (Point, Point);
int inside (Point p, Sphere s)
{
return dist(p, s.c) <= s.r;
}
Мы воспользовались здесь определенной ранее функцией dist, которая вычисляет расстояние между двумя точками. Точка p находится внутри сферы, если ее расстояние от центра не превышает величины радиуса (точка на поверхности сферы включается в этот случай). Функция inside возвращает значение 0, если точка p находится вне сферы, и значение 1, если p находится внутри или на поверхности сферы (0 и 1 соответствуют логическим значениям ложь и истина).
При вызове функции inside в нее передается указатель на p (поскольку это массив), а вот структура s копируется целиком (4 числа типа double). Более экономный вариант получится, если передавать не структуру, а указатель на нее. Для этого функцию inside придется несколько изменить:
int inside (Point p, Sphere *sp)
{
return dist(p, (*sp).c) <= (*sp).r;
}
(Мы назвали новый аргумент sp, а не s, чтобы подчеркнуть, что это указатель: буква “p” от слова “pointer” — “указатель”.) Вызов функции теперь сопровождается меньшими затратами, так как передаются лишь два указателя (размер указателя обычно совпадает с размером int или long). Однако не слишком красиво выглядит внутри функции обращение к элементам через указатель. Если sp — указатель на структуру, то *sp — сама структура. Однако написать *sp.r для обращения к элементу r нельзя, потому что операция “.” (доступ к элементу структуры) имеет более высокий приоритет, чем “*” (доступ к объекту через указатель на него). Поэтому *sp приходится заключать в скобки.
Так как работать со структурами с помощью указателей на них приходится довольно часто, то в языке C ввели специальную операцию “->” для доступа к элементам структуры через указатель на нее. Эта операция позволяет записать выражение в функции inside более кратко:
int inside (Point p, Sphere *sp)
{
return dist(p, sp->c) <= sp->r;
}