Определение структур и их свойства

Структуры

Определение новых названий типов

Язык 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).


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: