Struct в C: свои типы данных без классов

struct в C нужен, когда одних отдельных переменных уже мало. Цена, название и количество товара логически относятся к одному объекту. Их можно держать в трех переменных, но код быстро станет рассыпаться. Структура позволяет собрать связанные поля под одним именем

В этом уроке мы создадим тип Product, напечатаем его через функцию и посмотрим, зачем нужен оператор ->, когда работаем с указателем на структуру

Что получится в конце

Файл product_struct.c:

#include <stdio.h>

typedef struct {
    int id;
    char name[32];
    double price;
    int stock;
} Product;

void print_product(const Product *product)
{
    printf("Product #%d\n", product->id);
    printf("Name: %s\n", product->name);
    printf("Price: %.2f\n", product->price);
    printf("Stock: %d\n", product->stock);
}

int main(void)
{
    Product keyboard = {1, "Keyboard", 79.90, 12};
    print_product(&keyboard);

    return 0;
}

Сборка:

gcc -Wall -Wextra -std=c17 product_struct.c -o product_struct
./product_struct

Что делает struct

Без структуры данные выглядели бы так:

int product_id = 1;
char product_name[32] = "Keyboard";
double product_price = 79.90;
int product_stock = 12;

Это работает для одного товара, но плохо масштабируется. Для второго товара придется заводить новые имена. Для массива товаров станет совсем неудобно

Структура собирает поля:

typedef struct {
    int id;
    char name[32];
    double price;
    int stock;
} Product;

Теперь Product можно использовать как свой тип данных:

Product keyboard = {1, "Keyboard", 79.90, 12};

Это не класс из C++. В структуре C нет методов, конструкторов, наследования и приватных полей. Это просто составной тип данных

Доступ к полям через точку

Если у вас есть сама переменная структуры, используйте точку:

printf("%s\n", keyboard.name);
keyboard.stock = 10;

Точка означает: взять поле name или stock внутри переменной keyboard

Можно заполнять структуру после объявления:

Product mouse = {0};

mouse.id = 2;
mouse.price = 39.50;
mouse.stock = 20;

Со строковым полем осторожнее: массив char name[32] нельзя просто присвоить через mouse.name = "Mouse";. Для копирования строк используют функции из <string.h>, но в первом примере проще инициализировать строку сразу

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

Можно передать структуру по значению:

void print_product(Product product)
{
    printf("%s\n", product.name);
}

Но тогда функция получает копию всей структуры. Для маленького примера это не страшно, но в реальном коде структуры бывают крупнее. Поэтому часто передают указатель:

void print_product(const Product *product)

const говорит: функция не должна менять товар. Указатель позволяет не копировать всю структуру

Оператор стрелка ->

Если у вас указатель на структуру, доступ к полю пишут через ->:

product->price

Это короткая форма для:

(*product).price

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

Правило:

Что естьДоступ к полю

Если запомнить эту таблицу, половина путаницы со структурами уйдет

Массив структур

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

Product products[2] = {
    {1, "Keyboard", 79.90, 12},
    {2, "Mouse", 39.50, 20}
};

for (int i = 0; i < 2; i++) {
    print_product(&products[i]);
}

Теперь можно хранить список однотипных записей. Это уже похоже на маленькую таблицу в памяти: у каждой строки есть id, name, price, stock

Когда позже появятся файлы, такую структуру можно будет заполнять из файла или сохранять обратно. Поэтому struct — хороший мост от простых переменных к реальным задачам

Именованные инициализаторы

Структуру можно заполнять не только по порядку, но и по именам полей:

Product keyboard = {
    .id = 1,
    .name = "Keyboard",
    .price = 79.90,
    .stock = 12
};

Такой вариант длиннее, зато устойчивее к изменениям. Если вы поменяете порядок полей в struct, инициализация по именам останется понятной. Для обучающих материалов это особенно хорошо: читатель сразу видит, какое значение попадает в какое поле

Инициализация по порядку:

Product keyboard = {1, "Keyboard", 79.90, 12};

короче, но требует помнить порядок полей. Если случайно поменять price и stock, компилятор не всегда спасет смысл программы. Поэтому в длинных структурах именованные поля читаются лучше

Мини-практика: обновить остаток товара

Добавим функцию, которая уменьшает остаток товара после продажи. Она должна менять структуру, поэтому получает указатель

int sell(Product *product, int amount)
{
    if (amount <= 0) {
        return 0;
    }

    if (product->stock < amount) {
        return 0;
    }

    product->stock -= amount;
    return 1;
}

Вызов:

if (sell(&keyboard, 2)) {
    printf("Sold\n");
} else {
    printf("Cannot sell\n");
}

Здесь указатель нужен не ради красоты. Если передать Product product по значению, функция изменит копию, а исходный keyboard останется прежним. Указатель дает функции доступ к исходной структуре

const как честный договор

Сравните две функции:

void print_product(const Product *product);
int sell(Product *product, int amount);

Первая получает const Product , потому что только читает поля. Вторая получает Product , потому что меняет stock. Это делает код самодокументируемым: по сигнатуре видно, какая функция может менять данные

Если внутри print_product случайно написать product->stock = 0;, компилятор остановит такую попытку. Поэтому const — не украшение, а полезный ограничитель

Структуры и файлы

После урока про файлы структуру можно использовать как формат записи. Например, товар можно сохранить строкой:

1;Keyboard;79.90;12

А потом прочитать обратно и заполнить Product. На первом этапе не нужно строить полноценную базу данных на C. Достаточно увидеть связь: struct описывает форму данных в памяти, файл хранит эти данные между запусками программы

Когда структура становится слишком большой

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

typedef struct {
    double price;
    int stock;
} ProductInventory;

typedef struct {
    int id;
    char name[32];
    ProductInventory inventory;
} Product;

Теперь цена и остаток лежат в отдельной логической группе. Доступ становится чуть длиннее:

product.inventory.stock

зато смысл данных читается лучше. В C нет классов, но есть возможность аккуратно проектировать формы данных. Хорошая структура отвечает на вопрос: какие поля действительно принадлежат одному понятию

Почему порядок полей иногда важен

Компилятор может добавлять выравнивание между полями структуры. Поэтому размер структуры не всегда равен простой сумме размеров ее полей. Для первых уроков это не нужно оптимизировать, но полезно не строить предположений о точной раскладке байтов без проверки sizeof(Product)

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

Частые ошибки и порядок проверки

Путают точку и стрелку Если переменная сама структура, используйте .. Если переменная указатель на структуру, используйте ->

Думают, что struct — это класс В C структура хранит поля, но не дает объектную модель C++. Функции пишутся отдельно и получают структуру аргументом

Пытаются присвоить строку в char-массив После объявления массива строку нужно копировать функцией, а не присваивать через =

Передают большую структуру по значению Для чтения часто удобнее const Product *product. Это не копирует структуру и явно показывает, что функция не меняет данные

Что может быть еще интересно по этой теме

Нужно ли всегда писать typedef struct? Нет. Можно писать struct Product { ... }; и затем struct Product item;. typedef просто делает имя короче для учебного кода

Можно ли хранить указатели внутри структуры? Да, но тогда появляется вопрос владения памятью: кто выделяет, кто освобождает и когда строка перестает быть доступной

Почему const стоит перед Product? const Product *product означает, что через этот указатель нельзя менять поля структуры. Это хорошая защита для функций печати и чтения

Что дальше после struct? Работа с файлами. Структура дает формат данных, а файл позволяет сохранить эти данные между запусками программы

Что почитать дальше по C

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x