Структуры
Определение новых названий типов
Язык C. Лекция 8
1. Массивы (продолжение)
Существует возможность давать имена-синонимы для существующих или определяемых программистом типов:
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 позволяет упростить сложные объявления, сделать их более понятными.
Структура — второй (после массива) вид сложного типа данных, состоящего из нкскольких элементов. В отличие от массива элементы структуры имеют разные типы. Каждый элемент получает собственное имя, и к индивидуальным элементам структуры обращаются по именам, а не по индексам, как в случае массивов.
Рассмотрим пример. Чтобы определить сферу, достаточно задать ее радиус 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;
}
2.2 Пример: Таблица химических элементов
Рассмотрим условия двух задач:
1. Написать программу, которая читает химическую формулу вещества и выдает молекулярную массу и элементный состав в процентах, например:
Na2SO4
Molecular mass: 142.037
Na: 32.37%
S: 22.57%
O: 45.06%
2. Написать программу, которая читает файл с данными о геометрической конфигурации молекулы (как в примере 1.3 из предыдущей лекции) и вычисляет координаты центра масс.
У этих двух задач (в общем-то совсем разных) есть общая особенность: нужно уметь находить атомную массу (а может быть и другие свойства), зная химический символ элемента. Очевидно, в программе нужно хранить таблицу свойств элементов и иметь функцию поиска информации в этой таблице. Посмотрим, как это можно было бы организовать.
typedef struct { /* Информация о химическом элементе: */
char sym[3]; /* символ химического элемента */
double mass; /* атомная масса в углеродных единицах */
|
|
} Element;
static Element table[] = {
{ "H", 1.0079 },
{ "He", 4.0026 },
{ "Li", 6.941 },
{ "Be", 9.01218 },
{ "B", 10.81 },
{ "C", 12.011 },
{ "N", 14.0067 },
..........
{ "Xe", 131.30 }
};
static int tbsz = sizeof(table) / sizeof(Element);
Для хранения свойств химического элемента используется структура, обозначаемая для удобства (с помощью typedef) как тип Element. Таблица свойств задана в виде массива table таких структур, причем ее содержимое определено с помощью инициализации (показана лишь часть таблицы). Обратите внимание на синтаксис: список инициализаторов элементов массива заключен в фигурные скобки, элементы разделяются запятыми. Поскольку каждый элемент массива является структурой, т.е. состоит из нескольких элементарных значений, то список этих значений в свою очередь заключается в фигурные скобки, а значения внутри скобок тоже разделяются запятыми.
Размер массива table явно не указан — в этом случае по правилам языка С число элементов массива будет равно количеству инициализаторов. Фактический размер таблицы хранится в переменной tbsz (название построено из слов “table size”), причем инициализирующее значение определяется как частное от деления размера всего массива на размер одного элемента (размер любого объекта в байтах дает операция sizeof). Такая организация облегчает в дальнейшем изменение таблицы: если захочется ее расширить, то нужно лишь добавить в список инициализаторов новые элементы и перекомпилировать программу. Фактический размер массива table и значение переменной tbsz автоматически согласованно изменятся.
Заметим, что таблица в таком виде больше подходит для первой задачи — хранится атомная масса природной смеси изотопов. Для второй задачи желательна более детальная информация — массы не атомов, а ядер, причем иногда могут понадобиться различные изотопы одного элемента. В этом случае структуру Element следует видоизменить, включив в нее дополнительные элементы данных, и соответствующим образом записать инициализаторы таблицы table.
Теперь рассмотрим функцию (назовем ее getinfo) поиска информации в таблице по заданному химическому символу sym. Она возвращает указатель на информацию об элементе (т.е. на элемент массива — структуру типа Element) либо NULL, если указанного символа в таблице нет.
#include <string.h>
/* Функция getinfo возвращает указатель на информацию об элементе
по его символу sym (или NULL, если символа нет в таблице) */
Element *getinfo (char *sym)
{
int i;
for (i = 0; i < tbsz; i++) {
if (strcmp(sym, table[i].sym) == 0) return table + i;
}
return NULL;
}
Здесь использована стандартная библиотечная функция сравнения двух строк strcmp(s1, s2), которая возвращает значение 0, если строки совпадают, и ненулевое значение, если строки различаются. Прототип этой функции содержится в стандартном заголовочном файле string.h. Результат поиска (указатель на найденный элемент) возвращается в виде выражения table+i, где i — индекс нужного элемента. Это выражение основано на связи между указателями и массивами и на правилах сложения указателей с целыми числами. То же самое выражение можно было бы записать и как &table[i] (т.е. указатель на i-й элемент массива table).