Лекция 20. Структуры

Структуры – составной тип данных, позволяющий размещать внутри одного типа значения разных типов и обращаться к ним по заданным именам.

Синтаксис:

struct тег_структуры
{
    поле1;
    ...
    полеN;
};

Пример:

struct mob     // Определеяем структуру (составной тип данных) с тегом mob.
{              // Этот тег будет использоваться для создания переменных.
    char name[10];
    int max_hp;
    int hp;
    double x;
    double y;
};

int main(void)
{
    struct mob mob1; // При определении переменной со структурным типом
                     // нужно использовать тип struct имя_тега.
    strcpy(mob1.name, "dog");
    mob1.max_hp = mob1.hp = 15;
    mob1.x = 0.5; // Доступ к полю структруры при чтении и записи
                  // происходит с помощью точки.
    mob1.y = 10.0;
    printf("x = %lf, y = %lf\n", mob1.x, mob1.y);

    struct mob mob2;
    mob2 = mob1;  // Полное копирование всех полей mob1 в mob2.
    strcpy(mob2.name, "cat");

    // Поля структуры можно инициализировать при определении
    // переменной. Значения полей должны идти в списке в том
    // же порядке, что и в определении структурного типа.
    struct mob mob3 = {"bird", 3, 3, 10.0, 15.0};

    // Кроме одиночных структур можно создавать их массивы:
    struct mob mobs[10];
    int i = 0;
    for(i = 0; i < 10; ++i) {
        strcpy(mobs[i].name, "ant");
        mobs[i].max_hp = mobs[i].hp = 1;
        mobs[i].x = 0;
        mobs[i].y = 5 * i;
    }

    // ... дальнейшая работа со структурами ... //

    return 0;
}

Замечание о синтаксисе

Оператор struct может одновременно определять структурный тип и переменную этого типа. То есть, вместо Полной записи с раздельным определением нового типа и переменной этого типа

struct имя_тега
{
    поле1;
    ...
    полеN;
};

struct имя_тега имя_переменной;

возможна укороченная запись вида

struct имя_тега
{
    поле1;
    ...
    полеN;
} имя_переменной;

Замечание о массивах и указателях в полях структур

Рассмотрим такой пример:

struct mob {
    char name[10];
    char *type;
    int max_hp;
    int hp;
};

typedef struct mob Mob;
// Определяем синоним Mob для типа struct mob. Теперь можно просто
// пользоваться типом Mob и не писать каждый раз struct.

int main(void)
{
    Mob mob1;
    strcpy(mob1.name, "dog");
    mob1.max_hp = mob1.hp = 10;
    mob1.type = "pet"; // Присвоили указатель на строковую константу.
                       // Теперь мы можем присваивать mob1.pet другие
                       // указатели, но менять содержимое строковой
                       // константы нельзя.

    mob1.type = (char *) malloc(20); // Здесь мы выделяем 20 байт изменяемой
                                     // динамической памяти.
    strcpy(mob.type, "pet");
    printf("name = %s, type = %s\n", mob1.name, mob1.type);
    // Всё ровно как и ожидалось, имя моба "dog" и тип "pet".

    Mob mob2 = mob1;
    strcpy(mob2.name, "tiger");
    strcpy(mob2.type, "hungry pet");

    printf("name = %s, type = %s\n", mob2.name, mob2.type);
    // Мы переписали значения обоих полей второго моба, теперь у нас
    // моб с именем "tiger" и типом "hungry pet".

    // И тут нам зачем-то понадобился первый моб...
    printf("name = %s, type = %s\n", mob1.name, mob1.type);
    // Внезапно, имя моба по-прежнему "dog", но тип изменился на
    // "hungry pet". Пояснение к этому на первый взгляд странному
    // поведению на картинке ниже.

    return 0;
}

Как видно на рисунке, такое поведение вызвано тем, что указатели в полях type у обеих структур указывают на один и тот же участок памяти.

_images/011.png

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

Mob mob2 = mob1;
mob2.type = (char *) malloc(20);
strcpy(mob2.type, mob1.type);

Выравнивание полей структур

Препдоложим, что нам нужно вычислить размер структуры вида

struct S {
    char c1;
    short s;
    char c2;
    int x;
};

как мы знаем, размер всех типов данных, кроме char (размер всегда равен одному байту), может отличаться для различных типов процессоров и компиляторов. Пусть в этом примере sizeof(short) == 2 и sizeof(int) == 4, что верно для большинства настольных компьютеров. Казалось бы, в этом случае нам нужно просто сложить размеры всех полей и получить тривиальный ответ. Однако, выполнение кода

int main(void)
{
    printf("sizeof(S) = %d\n", sizeof(S));
    return 0;
}

выводит нам число 12. Как так? Дело в том, что адреса чисел внутри структур подвержены выравниванию точно так же, как и адреса переменных. Выравнивание – расположение значений различных данных таким образом, чтобы начальный адрес этого числа был кратен некоторой константе. Например, числа типа int в программах для процессоров Intel и AMD выравниваются на 4 байта (и их размер также равен 4 байтам). Такая договорённость о расположении чисел в памяти позволяет проектировать оперативную память таким образом, чтобы она извлекала 4 байта из адресов 4*n, 4*n+1, 4*n+2, 4*n+3 одновременно, таким образом увеличивая скорость работы памяти.

Рисунок с выравниванием полей в структуре S.

_images/021.png