Введение
Sunny Snacks нужен чек, сохраняющий каждую покупку, даже если покупатель снова берёт тот же перекус. Сначала вы запустите готовый пример с одним товаром, а затем дополните программу на C, которая хранит покупки отдельно и печатает каждую строку, цену единицы, промежуточную сумму и полный итог.
Нужно знать переменные, условия, циклы, массивы и функции C, а также основы ввода в терминале и работы с путями. В этой работе объясняются необходимые структуры и детали текстового вывода. В браузерной среде уже есть компилятор и терминал Workspace. Суммы — синтетические целые значения в центах, а не платёжная или бухгалтерская система. Время чтения и практики оценено автором; на начинающих оно не измерялось.
Чтение чека с одним товаром
На этом шаге вы запустите небольшой готовый пример и измените покупку одной единицы товара на покупку двух.
Откройте вкладку Workspace. Терминал начинает работу в /home/labex/project/receipts. cd меняет каталог, pwd показывает текущий, а ls перечисляет файлы. Эти команды также помогают исправить ошибку в пути или имени.
cd /home/labex/project/receipts
pwd
ls
sample.c — готовый пример с одним товаром. till.c — незаконченная программа для нескольких товаров из следующего шага. Каталог, анализатор и драйвер ввода подготовлены заранее; не меняйте их. cat показывает текстовый файл, не редактируя его:
cat sample.c
Переменная quantity в примере изначально равна 1. unit_cents равна 125: одна денежная единица и 25 центов. Умножение вычисляет промежуточную сумму в центах. Сохраняйте эту единицу измерения во всех расчётах. Целочисленное деление на 100 даёт целую часть, а % 100 — остаток. В printf спецификатор %d выводит целое число, %s — строку C с завершающим символом, %02d дополняет дробную часть до двух цифр, а \n завершает строку. Так 250 центов отображаются как 2.50 без арифметики с плавающей точкой.
Скомпилируйте программу. gcc — компилятор C, -std=c11 выбирает C11, -Wall -Wextra включает предупреждения, а -o sample задаёт имя исполняемого файла. ./sample запускает этот файл из текущего каталога.
gcc -std=c11 -Wall -Wextra sample.c -o sample
./sample
Полный обычный результат:
SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 1 | 1.25 | 1.25
TOTAL | 1.25
ID означает идентификатор товара: 101 соответствует этой записи каталога. UNIT — цена одной единицы, а SUBTOTAL — количество в этой покупке, умноженное на цену единицы. Вертикальные черты разделяют читаемые столбцы. Последний TOTAL складывает промежуточные суммы всех покупок; здесь покупка только одна.
Измените количество на 2. Следующая команда sed заменяет точный старый текст новым; -i сохраняет изменение. Одинарные кавычки удерживают замену в одном аргументе оболочки.
sed -i 's/int quantity = 1;/int quantity = 2;/' sample.c
После редактирования перекомпилируйте: изменение исходного кода не обновляет старый исполняемый файл автоматически. Оператор оболочки > перенаправляет стандартный вывод в файл. Он открывает или обнуляет файл до запуска программы C, поэтому используйте указанное новое имя вывода.
gcc -std=c11 -Wall -Wextra sample.c -o sample
./sample > sample-receipt.txt
cat sample-receipt.txt
Теперь количество равно 2, промежуточная и итоговая суммы равны 2.50, а цена единицы остаётся 1.25. Проверьте вручную: 125 × 2 = 250 центов. Сохраните sample.c, исполняемый файл sample и sample-receipt.txt.
Если файл не найден, вернитесь изученной командой cd и проверьте его имя через ls. Если компилятор сообщил об ошибке, исправьте указанную строку и перекомпилируйте перед запуском: после неудачной компиляции старый исполняемый файл может остаться.
Сохранение и печать каждой покупки
На этом шаге вы сохраните покупки отдельными записями и создадите полный чек с тремя строками покупок.
Начните в Workspace, в /home/labex/project/receipts. Прочитайте каталог и незаконченную программу:
cd /home/labex/project/receipts
cat catalog.h
cat till.c
Заголовочный файл catalog.h задаёт название прилавка, максимум шесть покупок, максимальное количество 20 и четыре записи каталога. #include "catalog.h" подключает эти определения при компиляции. #define даёт имя константе или инициализатору; CATALOG_ROWS задаёт строки для инициализации массива товаров. Обратная косая черта (\) в конце строки определения каталога продолжает тот же #define на следующей физической строке. Не меняйте catalog.h, parse.h, driver.h и read-line.h в этой работе.
Структура объединяет связанные поля в одну запись. Product объединяет целочисленный id, массив из 32 символов для имени и целочисленную цену cents. typedef позволяет использовать Product как короткое имя этого типа записи. Строка C заканчивается символом с нулевым значением \0, поэтому массив имени должен вмещать завершающий символ помимо видимых символов. Выданные имена помещаются; разбор произвольных имён не входит в задачу.
Purchase — второй тип записи. Он хранит позицию товара в массиве и количество в этой покупке. В basket[line_count].quantity квадратные скобки выбирают покупку, а точка — её поле количества. Позиции начинаются с нуля. Глобальный массив static и line_count инициализируются нулями. sizeof products / sizeof products[0] вычисляет число полных записей товаров в массиве.
Выданный цикл find_product ищет по идентификатору товара и возвращает позицию в массиве либо -1, если товара нет. Два товара с именем Water имеют разные ID и цены: 103 стоит 75 центов, а 104 бесплатный. Одно имя не идентифицирует товар. В каталоге четыре товара с ID от 1 до 999 и ценами от 0 до 9999 центов. Эта же программа должна работать после перекомпиляции с другим выданным каталогом из четырёх строк в этих пределах.
Дополните approve_purchase проверками ID, количества и свободного места до записи в массив. Только затем сохраняйте позицию товара и количество в basket[line_count] и увеличивайте счётчик. Количество — от 1 до 20; нулевая цена допустима. Не объединяйте повторяющиеся ID: более поздняя покупка — отдельная строка. Шесть покупок с максимальными допустимыми ценой и количеством дают не более 6 × 20 × 9999 = 1,199,880 центов, что помещается в знаковое 32-битное целое.
puts выводит строку с завершающим символом и добавляет перевод строки. printf использует спецификаторы формата, например %s для строки с терминатором NUL и %d для целого числа. print_receipt перебирает все одобренные покупки. Product product = ... копирует эту запись каталога в локальную переменную. Вычисляйте целочисленную промежуточную сумму, добавляйте её к итогу и печатайте полные строки по порядку после названия и заголовка, перед единственным последним TOTAL. Используйте пять полей, разделённых вертикальными чертами; пробелы вокруг полей и пустые строки могут различаться, но чек с лишними записями или пропущенными полями не является полным.
Замените till.c следующим полным кодом. cat > till.c заменяет файл, а <<'EOF' начинает here-document. Разделитель в кавычках копирует текст C буквально до строки, содержащей только EOF. Скопируйте весь блок, включая последнюю строку.
cat > till.c <<'EOF'
#include <stdio.h>
#include "catalog.h"
#include "parse.h"
typedef struct {
int id;
char name[32];
int cents;
} Product;
typedef struct {
int product_index;
int quantity;
} Purchase;
static const Product products[] = CATALOG_ROWS;
static const int product_count = sizeof products / sizeof products[0];
static Purchase basket[MAX_LINES];
static int line_count;
static int find_product(int id) {
for (int i = 0; i < product_count; i++)
if (products[i].id == id) return i;
return -1;
}
static int approve_purchase(int id, int quantity) {
int index = find_product(id);
if (index < 0 || quantity < 1 || quantity > MAX_QUANTITY || line_count >= MAX_LINES)
return 0;
basket[line_count].product_index = index;
basket[line_count].quantity = quantity;
line_count++;
return 1;
}
static void print_receipt(void) {
int total = 0;
puts(SHOP_TITLE);
puts("ID | ITEM | QTY | UNIT | SUBTOTAL");
for (int i = 0; i < line_count; i++) {
Product product = products[basket[i].product_index];
int quantity = basket[i].quantity;
int subtotal = product.cents * quantity;
total += subtotal;
printf("%d | %s | %d | %d.%02d | %d.%02d\n",
product.id, product.name, quantity,
product.cents / 100, product.cents % 100,
subtotal / 100, subtotal % 100);
}
printf("TOTAL | %d.%02d\n", total / 100, total % 100);
}
#include "driver.h"
EOF
parse.h и driver.h предоставляют уже работающий цикл ввода. Драйвер запрашивает ID и количество и вызывает ваши функции одобрения и печати. Их существующие имена и сигнатуры — точки интеграции; сохраняйте их как показано. Внутренние имена типов записей и локальных переменных не являются целью оценки. Границы разбора ввода вы изучите в следующей работе.
Скомпилируйте полную программу:
gcc -std=c11 -Wall -Wextra till.c -o till
Драйвер отправляет приглашения и сообщения в стандартный поток ошибок, который остаётся виден в терминале. Только готовый чек поступает в стандартный вывод. Запустите программу с новым выходным файлом:
./till > receipt.txt
Программа ожидает ввод. Введите каждое значение таблицы в соответствующем приглашении, каждый раз нажимая Enter. Не вводите их как команды оболочки. done — точная команда завершения в нижнем регистре, работающая только в приглашении Product ID.
| Ввод Product ID | Ввод Quantity | Результат |
|---|---|---|
101 |
2 |
Added line 1 |
102 |
1 |
Added line 2 |
101 |
1 |
Added line 3, предыдущая покупка 101 сохранена |
done |
Количество не запрашивается | Завершить чек и вернуться в оболочку |
Вы всё ещё видите приглашения и сообщения Added line, потому что драйвер пишет их в стандартный поток ошибок, отдельный поток. Только полный чек идёт в стандартный вывод, который > отправляет в receipt.txt. Когда приглашение оболочки вернётся, прочитайте действительно сохранённый чек:
cat receipt.txt
Ожидаемое содержимое:
SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 2 | 1.25 | 2.50
102 | Fruit cup | 1 | 2.30 | 2.30
101 | Oat bar | 1 | 1.25 | 1.25
TOTAL | 6.05
Две покупки Oat bar остаются отдельными. Сложите их промежуточные суммы с Fruit cup: 250 + 230 + 125 = 605 центов. Внешне правильный итог без всех этих строк потерял бы детали транзакции покупателя.
Числа используют десятичные цифры, необязательный знак и необязательные ASCII-пробелы или табуляции в начале и конце; ведущие нули разрешены. ID должны существовать в каталоге, количества — лежать в 1..20, а каждая строка содержать не больше 63 байт до Enter. Отказ для ID или количества отбрасывает ожидающего кандидата и возвращает к Product ID без изменения одобренных покупок. Выданный считыватель потребляет слишком длинную строку целиком, а не делит её на последующие ответы. Можно одобрить не более шести покупок. Пустая корзина и затем done дают допустимый пустой чек с итогом 0.00.
Чтобы отменить, а не завершить, нажмите Ctrl+D в пустом приглашении ввода. Этот сигнал конца файла либо EOF во время незаконченной строки отменяет операцию со статусом 2 и без полного чека. Незавершённая транзакция не экспортируется и не сохраняется. Перенаправление уже открыло выходной файл, поэтому для проверки намеренной отмены используйте другое имя. Ввод done как количества — неверный текст количества, а не завершение.
Если чек содержит только последнюю покупку, проверьте запись в basket[line_count] и увеличение счётчика после каждого одобрения. Сохраните, перекомпилируйте и выполните новую транзакцию с теми же нужными покупками. Каждый запуск начинает с пустой корзины и не загружает текст старого чека. Сохраните till.c, исполняемый файл till, выданные файлы и полный receipt.txt. Проверка курса также запускает неизменённые копии кода с динамическим обнаружением выхода за границы в одноразовом каталоге. Она может обнаружить ошибки, выполненные на этих входных данных; это не доказательство полной безопасности памяти и не исправление ваших файлов.
Резюме
Вы превратили пример с одним товаром в полный многострочный чек. Вы использовали структуры, массив отдельных покупок, стабильные идентификаторы каталога и целочисленные расчёты в центах, затем сохранили и прочитали реальный результат. Следующая работа посвящена проверке ввода до добавления покупки в этот массив.


