Перейти к содержанию

Лекция 17: Структуры в языке С

"Я человек весьма терпимый, могу выслушать и чужие мнения."
— Ярослав Гашек


Введение

Лекция посвящена моделированию объектов реального мира с помощью композитных типов в языке Си. Рассмотрены механизмы объявления и использования структур, работу с указателями, динамическое выделение памяти, передача структур в функции, а также особенности перечислимых типов (enum) и объединений (union). Приведённые примеры кода помогают закрепить материал и понять практические аспекты работы с данными конструкциями языка.


Объявление структур

Основные положения:

  • Структура – агрегатный тип, позволяющий объединить переменные разных типов под одним именем.
  • Синтаксис объявления:
    struct имя_структуры {
        тип1 поле1;
        тип2 поле2;
        // …
    };
    
  • Если несколько полей имеют одинаковый тип, их можно объединить через запятую:
    struct Point3D {
        int x, y, z;
    };
    

Пример объявления структуры:

struct point_t {
    int x;
    int y;
};

После объявления можно создавать переменные типа struct point_t и обращаться к полям через точку.


Инициализация и использование структур

Основные моменты:

  • Создание переменной структуры сразу после объявления:
    struct point_t {
        int x;
        int y;
    } A;
    
  • Инициализация полей структуры происходит при создании переменной:
    struct point_t A = {10, 20};
    
  • Возможна именованная инициализация (с современным стандартом C):
    typedef struct thing {
        int a;
        float b;
        const char *c;
    } thing_t;
    
    int main() {
        thing_t t = {
            .a = 10,
            .b = 1.0,
            .c = "example"
        };
        // …
    }
    

Код для расчёта расстояния:

#include <stdio.h>
#include <math.h>

struct point_t {
    int x;
    int y;
};

int main() {
    struct point_t A;
    float distance;

    A.x = 10;
    A.y = 20;
    distance = sqrt((float)(A.x * A.x + A.y * A.y));

    printf("Distance = %.3f\n", distance);
    return 0;
}

Пример демонстрирует создание структуры, инициализацию полей и вычисление расстояния от начала координат.


Указатели на структуры

Ключевые моменты:

  • Для доступа к полям структуры по указателю используется оператор ->:
    struct point_t A = {3, 4};
    struct point_t *ptrA = &A;
    ptrA->x = 5; // то же, что (*ptrA).x = 5;
    
  • Использование указателей позволяет эффективно работать с динамическими структурами и передавать их в функции.

Пример работы с указателем:

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point A = {3, 4};
    struct Point *ptrA = &A;

    // Изменяем поля через указатель
    ptrA->x = 5;
    ptrA->y = 6;

    printf("A: (%d, %d)\n", A.x, A.y);
    return 0;
}

Работа с указателями позволяет получать прямой доступ к данным и изменять их в функции без копирования всей структуры.


Использование typedef для упрощения объявления типов

Суть:

  • С помощью typedef можно создать псевдонимы для структур, чтобы не писать постоянно ключевое слово struct.
  • Пример:
    typedef struct point_t {
        int x;
        int y;
    } Point;
    
    int main() {
        Point A, B = {10, 13};
        // …
    }
    

Преимущества:

  • Упрощает запись кода.
  • Улучшает читаемость.

Пример демонстрирует, как использование typedef избавляет от необходимости писать struct при создании переменной типа Point.


Динамические структуры данных

Основные аспекты:

  • Выделение памяти под массив структур с помощью malloc.
  • При работе с динамическими структурами важно правильно освобождать память, так как поля структуры (особенно указатели) не освобождаются автоматически.

Пример выделения памяти и работы с массивом структур:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_SIZE 20

typedef struct User {
    char *login;
    char *password;
    int id;
} User;

void jsonUser(User *user) {
    printf("{id: %d, login: \"%s\", password: \"%s\"}\n",
           user->id, user->login, user->password);
}

void freeUsersArray(User **users, unsigned size) {
    for (unsigned i = 0; i < size; i++) {
        free((*users)[i].login);
        free((*users)[i].password);
    }
    free(*users);
}

int main() {
    User *users = NULL;
    unsigned size;
    char buffer[128];

    printf("Enter number of users: ");
    scanf("%u", &size);
    size = size <= MAX_SIZE ? size : MAX_SIZE;

    users = (User*)malloc(size * sizeof(User));
    for (unsigned i = 0; i < size; i++) {
        printf("User #%u name: ", i);
        scanf("%127s", buffer);
        users[i].id = i;
        users[i].login = (char*)malloc(strlen(buffer) + 1);
        strcpy(users[i].login, buffer);
        printf("Password: ");
        scanf("%127s", buffer);
        users[i].password = (char*)malloc(strlen(buffer) + 1);
        strcpy(users[i].password, buffer);
    }

    for (unsigned i = 0; i < size; i++) {
        jsonUser(&users[i]);
    }

    freeUsersArray(&users, size);
    return 0;
}

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


Передача структур в функции

Способы передачи:

  • Передача по значению: копия всей структуры передается в функцию. Изменения внутри функции не влияют на оригинал.
  • Передача по указателю: передается адрес структуры, что позволяет изменять содержимое непосредственно.

Пример передачи структуры по значению:

#include <stdio.h>
#include <conio.h>

struct struct_type {
    int a, b;
    char ch;
};

void fun(struct struct_type parm) {
    printf("\n%d\n%d\n%c\n", parm.a, parm.b, parm.ch);
}

int main(void) {
    struct struct_type arg;
    arg.a = 1000;
    arg.b = 9000;
    arg.ch = 'A';
    fun(arg);
    printf("\nPress any key: ");
    getch();
    return 0;
}

Демонстрируется базовая передача структуры в функцию.


Копирование структур и работа с памятью

Ключевые моменты:

  • Копирование «бит в бит»: можно использовать оператор присваивания или функцию memcpy.
  • При копировании массива структур важно учитывать размер и порядок элементов.

Пример копирования структуры:

#include <stdio.h>
#include <string.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point point = {10, 20};
    struct Point apex;

    // Поэлементное копирование
    apex.x = point.x;
    apex.y = point.y;

    // Или с использованием memcpy
    // memcpy(&apex, &point, sizeof(struct Point));

    printf("a = %d, b = %d\n", apex.x, apex.y);
    return 0;
}

Важно помнить, что приведение одной структуры к другой недопустимо, даже если их поля совпадают.


Битовые поля в структурах

Назначение:

  • Позволяют упаковать данные более компактно, выделяя фиксированное число бит для хранения значения.
  • Объявление битовых полей происходит с использованием двоеточия после имени поля.

Пример структуры с битовыми полями:

#include <stdio.h>
#define YEAR0 1980

struct date {
    unsigned short day   : 5; // 5 бит (значения 1..31)
    unsigned short month : 4; // 4 бит (значения 1..12)
    unsigned short year  : 7; // 7 бит (смещение от YEAR0)
};

int main() {
    struct date today;
    today.day = 9;
    today.month = 2;
    today.year = 2022 - YEAR0; // Пример: 42

    printf("\nToday: %u.%u.%u\n", today.day, today.month, today.year + YEAR0);
    printf("Size of 'today': %lu bytes\n", sizeof(today));
    return 0;
}

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


Перечислимые типы (enum)

Основные сведения:

  • Перечисления позволяют задать символические имена для набора целочисленных констант.
  • Повышают читаемость кода и предотвращают использование "магических чисел".

Пример объявления и использования enum:

#include <stdio.h>

typedef enum {
    MONDAY = 1,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
} WEEK;

int main() {
    int day;
    printf("Введите номер дня недели: ");
    scanf("%d", &day);

    switch(day) {
        case MONDAY:
            printf("Понедельник\n");
            break;
        case TUESDAY:
            printf("Вторник\n");
            break;
        case WEDNESDAY:
            printf("Среда\n");
            break;
        case THURSDAY:
            printf("Четверг\n");
            break;
        case FRIDAY:
            printf("Пятница\n");
            break;
        case SATURDAY:
            printf("Суббота\n");
            break;
        case SUNDAY:
            printf("Воскресенье\n");
            break;
        default:
            printf("Некорректный ввод\n");
            break;
    }
    return 0;
}

Перечисления помогают задать понятные константы для дней недели и использовать их в конструкциях управления.


Объединения (union)

Что такое union:

  • Объединение позволяет хранить разные типы данных в одном участке памяти, но только одно из полей активно в каждый момент времени.
  • Размер объединения определяется размером самого большого поля.

Пример объединения:

#include <stdio.h>
#include <stdlib.h>

union u_tag {
    int ival;
    float fval;
    char *sval;
};

int main() {
    union u_tag u;
    u.ival = 42;
    printf("Integer: %d\n", u.ival);

    u.fval = 3.14f;
    printf("Float: %.2f\n", u.fval);

    return 0;
}

Пример практического применения (обмен байтами):

#include <stdio.h>
#include <stdlib.h>

int main() {
    char temp;
    union {
        unsigned char p[2];
        unsigned int t;
    } type;

    printf("Enter a number: ");
    scanf("%d", &type.t);
    printf("Original: %d = %x hex.\n", type.t, type.t);

    // Обмен двух младших байтов
    temp = type.p[0];
    type.p[0] = type.p[1];
    type.p[1] = temp;

    printf("Swapped bytes: %d = %x hex.\n", type.t, type.t);
    return 0;
}

Объединения полезны для работы с данными разных типов, когда необходимо экономить память или интерпретировать байтовое представление числа.


Заключение

  • Структуры в Си позволяют моделировать сложные объекты, объединяя различные типы данных.
  • Указатели на структуры и динамическое выделение памяти открывают возможности для создания гибких динамических структур данных (списки, деревья, графы).
  • typedef упрощает работу с пользовательскими типами.
  • Передача структур в функции может выполняться как по значению, так и по указателю – выбор зависит от задачи.
  • Перечисления (enum) и объединения (union) расширяют возможности языка, делая код более понятным и экономичным по памяти.

Практика написания программ с использованием этих конструкций является единственным способом глубокого освоения языка Си. Рекомендуется регулярно создавать небольшие проекты и эксперименты, чтобы закрепить полученные знания.


Бегут в спортзалах по кругу