Eingaben ablehnen, die kein Kauf sein können

CBeginner
Jetzt üben

Einführung

Ein Kassenbon soll genehmigte Käufe enthalten, nicht jedes Präfix, das eine Funktion zur Zahlenumwandlung am Anfang einer Zeile erkennt. Sunny Snacks braucht jetzt eine vollständige Prüfung und eine Kapazitätskontrolle, die frühere Käufe nicht überschreibt.

Diese unabhängige Einheit bereitet einen eigenen Katalog und ein eigenes Kassenbonprogramm vor. Du benötigst die Arrays, Funktionen, Strukturen und Berechnungen in ganzzahligen Centwerten aus Jede Zeile des Kassenbons bewahren. Du lernst nur die für einen begrenzten Dezimalparser nötigen Details zu Zeichenketten und Adressen und weist dann ungültige Eingaben zurück, ohne den Warenkorb zu verändern. Das Terminal Workspace und der Compiler sind vorbereitet; dies ist ein Textbonprogramm mit synthetischen Daten, kein Zahlungssystem. Die Dauer ist eine Schätzung des Autors.

Vollständigen Dezimaltext umwandeln

In diesem Schritt unterscheidest du eine gültige vollständige Zahl von einem numerischen Präfix und implementierst die Erfolgs- und Fehlergrenze des Parsers.

Beginne im Tab Workspace. Die vorbereiteten Dateien sind unabhängig vom vorherigen Lab. cd führt ins Arbeitsverzeichnis, ls prüft die Namen und cat zeigt Text an:

cd /home/labex/project/receipts
ls
cat parse-demo.c
cat parse.h
cat read-line.h

parse-demo.c ist ein kleines Programm, das parse_number aufruft; es liest eine Menge, gibt die akzeptierte Ganzzahl oder eine Ablehnung aus und endet. till.c verwendet im nächsten Schritt dieselbe parse.h. Der aktuelle Parser liefert immer 0 und lehnt deshalb auch gültige Eingaben ab. Bewahre catalog.h, driver.h, read-line.h und parse-demo.c unverändert; bearbeite parse.h und später till.c.

Text ist in C ein Zeichenarray, das mit dem NUL-Abschlusszeichen mit dem Wert null endet, geschrieben als \0. Das unterscheidet sich von der sichtbaren Ziffer '0'. Zum Beispiel:

array index:    0     1     2
text:         '2'   '0'  '\0'

Das bereitgestellte read_line reserviert 64 Bytes: bis zu 63 Eingabebytes und das Abschlusszeichen. getchar() liest ein Zeichen; sein Ergebnis vom Typ int kann auch EOF darstellen. Die Schleife speichert bis zu 63 Zeichen, liest eine überlange Zeile aber bis zu ihrem Zeilenumbruch weiter, damit Reste nie zur nächsten Antwort werden. Sie liefert 1 für eine vollständige Zeile, -1 für eine vollständig verbrauchte überlange Zeile und 0 für EOF. EOF verwirft auch eine unvollständige Zeile. Die Bedingung read_line(text) != 1 im Beispiel lehnt die letzten beiden Fälle vor der Umwandlung ab.

Die öffentliche Schnittstelle des Parsers lautet:

int parse_number(const char text[], int minimum, int maximum, int *result)

Behalte diesen Namen und die Parameterreihenfolge bei: Das vorbereitete Beispiel und der Treiber verwenden sie. Liefere bei Erfolg 1 und bei Fehler 0. Bei Erfolg speicherst du die umgewandelte Ganzzahl in der Ergebnisvariable des Aufrufers. Bei Fehler bleibt diese Variable unverändert. minimum und maximum sind inklusive: 0 ist eine gültige Zahl, wenn das Minimum 0 ist, obwohl die Menge eines Kaufs das Minimum 1 hat.

Dieser Aufruf benötigt eine kleine Erklärung zu Adressen. Ein gewöhnliches Ganzzahlargument übergibt einen Wert. &quantity bedeutet die Adresse des quantity-Speicherplatzes des Aufrufers. Der Parameter int *result empfängt diese Adresse; *result = value schreibt einen Wert in den Speicherplatz des Aufrufers. Der Stern in einer Deklaration bezeichnet eine Variable, die eine Adresse speichert; in einem Ausdruck greift er auf den Speicherplatz an dieser Adresse zu. Wir verwenden diese konkreten Ausgabeparameter, ohne dynamische Speicherzuweisung oder eine allgemeine Zeigerübung.

strtol, deklariert in stdlib.h, wandelt Text in eine Ganzzahl vom Typ long um. Wir übergeben ausdrücklich Basis 10; eine führende Null bleibt daher dezimal. Das erste Argument zeigt auf das erste zu lesende Zeichen. Das zweite ist eine weitere Ausgabe: char *end speichert die Adresse des ersten Zeichens, das nicht zur Zahl gehörte. &end übergibt die Adresse dieser adressspeichernden Variable, damit strtol sie aktualisieren kann. Es wird keine zweite Zeichenkette kopiert.

"2bars" -> converted long value 2; end points at 'b'
"20"    -> converted long value 20; end points at '\0'
"+"     -> no conversion; end equals the starting address

Ein Präfix umzuwandeln reicht nicht. Unsere Syntax erlaubt optionale ASCII-Leerzeichen oder Tabulatoren am Anfang und Ende, ein optionales Vorzeichen und Dezimalziffern einschließlich führender Nullen. Innere Leerzeichen, Dezimalpunkte, Hexadezimalpräfixe, andere Leerraumzeichen und angehängte Buchstaben sind ungültig. Ein einzelnes Vorzeichen und leerer oder nur aus Leerraum bestehender Text sind ebenfalls ungültig. Der exakte Befehl done in Kleinbuchstaben gehört nur zur Product ID-Aufforderung des Programms; er ist kein Zahlentext.

Verwende einen Arrayindex begin, um nur die erlaubten anfänglichen Leerzeichen und Tabulatoren zu überspringen. &text[begin] ist die Adresse des ersten verbleibenden Zeichens. Prüfe vor dem Aufruf von strtol, dass es eine Ziffer oder ein Vorzeichen ist; so erweitert dessen großzügigere Leerraumbehandlung unsere Syntax nicht stillschweigend. Nach der Umwandlung bedeutet end == &text[begin], dass keine Ziffer verbraucht wurde. *end liest das Zeichen an der Stoppadresse; end++ bewegt diese Adresse um ein Zeichen weiter. Überspringe nur erlaubte abschließende Leerzeichen oder Tabulatoren und verlange danach das Abschlusszeichen.

Es gibt eine weitere Grenze: Der Text kann einen Wert darstellen, der selbst für long zu groß ist. errno aus errno.h ist die Fehleranzeige der Bibliothek. Setze sie vor dem Aufruf auf null; ERANGE danach zeigt einen Bereichsüberlauf an. Prüfe sowohl diesen Fehler als auch das geschäftliche Minimum und Maximum, bevor du long in int umwandelst. Ein zurückgegebener Zahlenwert null allein ist kein Fehlersignal.

Ersetze parse.h durch den vollständigen Parser. Die Headerfunktion ist static und gehört damit zu jedem Programm, das sie einbindet. cat > ersetzt die Datei und das Here-Dokument mit Begrenzungswort in Anführungszeichen kopiert den folgenden C-Code wörtlich bis zur letzten Zeile EOF.

cat > parse.h <<'EOF'
#include <errno.h>
#include <stdlib.h>

static int parse_number(const char text[], int minimum, int maximum, int *result) {
    int begin = 0;
    while (text[begin] == ' ' || text[begin] == '\t') begin++;
    char first = text[begin];
    if (first != '+' && first != '-' && (first < '0' || first > '9')) return 0;
    char *end;
    errno = 0;
    long value = strtol(&text[begin], &end, 10);
    if (end == &text[begin] || errno == ERANGE) return 0;
    while (*end == ' ' || *end == '\t') end++;
    if (*end != '\0' || value < minimum || value > maximum) return 0;
    *result = (int)value;
    return 1;
}
EOF

Die letzte Anweisung *result = (int)value schreibt die Ganzzahl des Aufrufers erst, nachdem alle Prüfungen bestanden wurden. Deshalb ändert eine ungültige Eingabe ein vorhandenes Ergebnis nicht. Die explizite Typumwandlung (int) führt die jetzt sichere Umwandlung aus.

Kompiliere das kleine Beispiel. -std=c11 wählt C11, -Wall -Wextra aktiviert Warnungen und -o benennt die ausführbare Datei:

gcc -std=c11 -Wall -Wextra parse-demo.c -o parse-demo
./parse-demo

Gib bei Quantity (1..20): den Wert +0002 ein und drücke Enter. Das Ergebnis lautet Accepted quantity: 2 und die Shellaufforderung kehrt zurück. Die Eingabe hat ein Vorzeichen und führende Nullen, stellt aber die gewöhnliche Ganzzahl 2 dar.

Führe das Beispiel erneut aus:

./parse-demo

Gib an der Mengenaufforderung 2bars ein. Das erwartete Ergebnis ist Rejected quantity.. Dieser Fehler ist beabsichtigt: Die Umwandlung des Präfixes 2 hat nicht die ganze Zeile validiert. Der Exitstatus eines Programms ist eine kleine Ganzzahl für seinen Aufrufer: Hier bedeutet 0 normalen Erfolg und 2, dass das Beispiel die Eingabe abgelehnt hat. Führe sofort echo $? aus, um diesen letzten Status anzuzeigen. Jeder Befehl dazwischen würde ihn ersetzen.

echo $?

Die erwartete Ausgabe ist 2. Diese Ablehnung bedeutet keinen Fehler des Terminals oder Compilers. Führe das Beispiel zum dritten Mal aus:

./parse-demo

Gib 9999999999999999999999999999999999999999 ein und drücke Enter. Auch das wird abgelehnt: Die Zeile hat weniger als 63 Bytes, ihre Dezimalzahl überschreitet aber den Umwandlungsbereich. Dagegen passt 21 in einen long, überschreitet aber die Höchstmenge 20. Das sind unterschiedliche Gründe für dieselbe sichere Ablehnung.

Meldet die Kompilierung eine fehlende Datei, finde mit cd und ls zurück. Speichere bei Syntaxfehlern den korrigierten Header und kompiliere neu. Das Programm parse-demo bindet deinen Header beim Kompilieren ein; ein alter Binärcode testet keine neue Bearbeitung.

Einen Kauf vor der Zustandsänderung genehmigen

In diesem Schritt kombinierst du den Parser mit dem Kaufarray, lehnst einen vollen Warenkorb vor dem Schreiben ab und speicherst nach mehreren abgelehnten Eingaben einen Kassenbon.

Beginne in Workspace unter /home/labex/project/receipts. Die vorbereitete till.c enthält bereits Datensatztypen, Katalogsuche und vollständige Ausgabe aus dem vorherigen Lab. In approve_purchase gibt es noch eine Lücke.

cd /home/labex/project/receipts
cat catalog.h
cat till.c
cat driver.h

Lies den Treiber ausgehend von seinen Eingabeaufforderungen. Er liest eine vollständige ID-Zeile, erkennt das exakte done, parst die dezimale ID und prüft, ob das Produkt existiert. Danach liest er eine Menge und ruft deinen Parser mit den Grenzen 1..20 auf. !parse_number(...) bedeutet, dass der Parser falsch zurückgegeben hat. Eine fehlgeschlagene Leseoperation oder Umwandlung kehrt zu Product ID zurück, ohne die Genehmigung aufzurufen. Eine ungültige Menge verwirft nur den ausstehenden Kandidaten, nicht die früheren Käufe.

Die Genehmigung schützt auch die Arraygrenze. Suche den Produktindex, lehne eine fehlende ID oder eine Menge außerhalb von 1..20 ab und lehne bei line_count >= MAX_LINES ab. || bedeutet, dass eine beliebige dieser Bedingungen zur Ablehnung genügt. Schreibe erst danach beide Felder, erhöhe line_count und liefere 1. Schon ein einziges Schreiben in basket[MAX_LINES] liegt außerhalb des Arrays mit sechs Einträgen; eine anschließende Ablehnung macht dieses ungültige Schreiben nicht rückgängig. Nutzbare Positionen sind 0 bis 5.

Alle Berechnungen bleiben in ganzzahligen Centwerten. Die vier bereitgestellten Zeilen verwenden IDs 1..999 und Preise 0..9999 Cent, einschließlich eines gültigen kostenlosen Artikels; sechs Mengen von höchstens 20 begrenzen die Gesamtsumme auf 1,199,880 Cent. Bewahre jeden genehmigten Kauf getrennt und in Reihenfolge, auch wenn sich die ID wiederholt. Dieselbe Logik muss nach dem Neukompilieren mit einem anderen bereitgestellten Katalog mit vier Zeilen innerhalb dieser Grenzen funktionieren. Strukturnamen und lokale Variablennamen dürfen variieren; die vorbereiteten Funktionsschnittstellen müssen weiterhin zum Treiber passen.

Ersetze till.c durch das vollständige Programm. Die Datensatz- und Ausgabebasis wird in dieser unabhängigen Einheit wiederholt, damit keine Dateien einer vorherigen VM nötig sind. parse.h bleibt der Parser, den du im vorherigen Schritt dieser VM vervollständigt hast.

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

Kompiliere den neuen Code:

gcc -std=c11 -Wall -Wextra till.c -o till

Der Treiber sendet Aufforderungen und Meldungen an die Standardfehlerausgabe. Nur ein vollständiger Kassenbon geht an die Standardausgabe. Leite ihn in eine neue Datei um und antworte weiterhin auf die sichtbaren Aufforderungen:

./till > receipt.txt

Gib die folgende Transaktion in Reihenfolge ein. Drücke nach jeder ID und Menge Enter. Eine unbekannte ID fragt keine Menge ab. Nach einer Ablehnung kehrst du zu Product ID zurück; versuche nicht, die vorherige Mengenaufforderung weiter zu beantworten.

Eingabe bei Product ID Eingabe bei Quantity Erwartete Beobachtung
101 2 Added line 1
102 2bars Menge abgelehnt; Zeile 1 bleibt erhalten
999 Keine Menge abgefragt Unbekannte ID; Zeile 1 bleibt erhalten
103 20 Added line 2, gültige obere Mengengrenze
104 1 Added line 3, gültiges kostenloses Produkt
102 1 Added line 4
101 1 Added line 5, getrennt von Zeile 1
104 2 Added line 6
103 1 Warenkorb voll: Ablehnung; alle sechs Zeilen bleiben erhalten
done Keine Menge abgefragt Kassenbon abschließen; zur Shell zurückkehren

Lies das gespeicherte Ergebnis erst, wenn die Shellaufforderung zurückkehrt:

cat receipt.txt

Erwarteter Inhalt:

SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 2 | 1.25 | 2.50
103 | Water | 20 | 0.75 | 15.00
104 | Water | 1 | 0.00 | 0.00
102 | Fruit cup | 1 | 2.30 | 2.30
101 | Oat bar | 1 | 1.25 | 1.25
104 | Water | 2 | 0.00 | 0.00
TOTAL | 21.05

Die abgelehnte Eingabe 2bars, die ID 999 und der siebte Kauf haben keine Zeilen erzeugt oder frühere Zeilen verändert. Die IDs 103 und 104 zeigen Water an, behalten aber unterschiedliche Preise. Ein Preis von null ist keine Menge von null. Die ganzzahlige Summe beträgt 250 + 1500 + 0 + 230 + 125 + 0 = 2105 Cent.

Der vollständige Bon enthält den Titel des Standes, die fünf angegebenen, durch Striche getrennten Spalten, alle Käufe in Reihenfolge und genau einen abschließenden TOTAL. Leerzeilen und Leerraum um Felder dürfen variieren, aber ein fehlender Spaltenkopf, zusätzliche unbekannte Datensätze oder Zeilen nach der Gesamtsumme ergeben eine unvollständige Ausgabe. Bewahre receipt.txt, till.c, parse.h, die ausführbare Datei till und alle bereitgestellten Dateien. Die Kursprüfung führt außerdem unveränderte Codekopien mit einer Laufzeitprüfung auf Grenzüberschreitungen in einem Wegwerfverzeichnis aus. Sie kann Fehler erkennen, die durch diese Eingaben ausgeführt werden; sie beweist keine vollständige Speichersicherheit und repariert deine Dateien nicht.

Um den Unterschied zwischen Abbruch und Abschluss zu beobachten, starte eine weitere Transaktion mit einem anderen Ausgabenamen:

./till > cancelled.txt

Gib die ID 101 ein, drücke Enter und dann Ctrl+D an der leeren Mengenaufforderung. EOF bricht mit Cancelled: no receipt. ab und veröffentlicht keinen vollständigen Bon. Es verwirft auch eine teilweise eingegebene, unvollständige Zeile. Lies sofort den nativen Exitstatus:

echo $?

Die erwartete Ausgabe ist 2. Die vorherige receipt.txt bleibt erhalten, weil dieser Befehl einen anderen Dateinamen verwendet. > hätte eine vorhandene gleichnamige Datei vor dem Programmstart geleert. Dagegen schließt done bei Product ID mit Status 0 ab, auch bei einem leeren Warenkorb mit Gesamtsumme 0.00; done ist nie ein Mengenbefehl.

Wird ein gültiger Kauf abgelehnt, prüfe die vollständige Textvalidierung und die inklusiven Parsergrenzen, danach die Genehmigungsbedingung. Verschwinden frühere Zeilen, prüfe, ob der Fehlerpfad zurückkehrt, bevor er Warenkorb oder Zähler verändert. Speichere, kompiliere neu und wiederhole die erforderliche Transaktion in einem neuen Lauf mit leerem Warenkorb, ohne den Bontext zu bearbeiten. Ein korrekt gespeicherter Bon und ein wiederverwendbares Programm müssen übereinstimmen.

Zusammenfassung

Du hast eine Zeichenzeile begrenzter Länge, einen Zeiger auf das Ende der Umwandlung, eine Überlaufprüfung und inklusive Geschäftsgrenzen verwendet, bevor du einen Kauf genehmigt hast. Dein Programm hat nach ungültigen Eingaben und der Ablehnung eines vollen Warenkorbs alle bisherigen Zeilen bewahrt und zugleich kostenlose Artikel und Grenzmengen akzeptiert. Abschließen und Abbrechen durch EOF haben jetzt klare, unterschiedliche Ergebnisse.