9.5 — std::cin i obsługa nieprawidłowych danych wejściowych

Większość programów wyposażonych w interfejs użytkownika musi obsługiwać dane wejściowe użytkownika. W programach, które pisałeś, używałeś std::cin, aby poprosić użytkownika o wprowadzenie tekstu. Ponieważ wprowadzanie tekstu jest tak swobodne (użytkownik może wpisać wszystko), bardzo łatwo może wprowadzić dane, których się nie spodziewa.

Pisząc programy, powinieneś zawsze brać pod uwagę, w jaki sposób użytkownicy (nieumyślnie lub w inny sposób) będą nadużywać Twoich programów. Dobrze napisany program będzie przewidywał, w jaki sposób użytkownicy będą go niewłaściwie używać i albo uprzejmie poradzi sobie z takimi przypadkami, albo w pierwszej kolejności zapobiegnie ich wystąpieniu (jeśli to możliwe). Mówi się, że program, który dobrze radzi sobie z błędami, to solidny.

W tej lekcji przyjrzymy się szczegółowo sposobom, w jaki użytkownik może wprowadzić nieprawidłowy tekst poprzez std::cin, i pokażemy kilka różnych sposobów radzenia sobie z takimi przypadkami.

Zanim omówimy, jak std::cin i operator>> może zawieść, podsumujmy, jak one działają. Omówiliśmy ten materiał na lekcji 1.5 — Wprowadzenie do iostream: cout, cin i endl.

Oto uproszczony obraz działania operator>> dla danych wejściowych:

  1. Po pierwsze, początkowe białe znaki (spacje, tabulatory i znaki nowej linii na początku bufora) są usuwane z bufora wejściowego. Spowoduje to odrzucenie wszelkich niewyodrębnionych znaków nowego wiersza pozostałych z poprzedniego wiersza wejścia.
  2. Jeśli bufor wejściowy jest teraz pusty, operator>> zaczeka, aż użytkownik wprowadzi więcej danych. Wiodące białe znaki są ponownie odrzucane.
  3. operator>> następnie wyodrębnia tyle kolejnych znaków, ile się da, aż napotka znak nowego wiersza (reprezentujący koniec wiersza wejściowego) lub znak, który jest nieprawidłowy dla zmiennej, do której wyodrębniana jest zmienna.

Wynik wyodrębnienia jest następujący:

  • Jeśli w kroku 3 powyżej wyodrębniono jakiekolwiek znaki, wyodrębnienie zakończyło się sukcesem. Wyodrębnione znaki są konwertowane na wartość, która jest następnie przypisywana do zmiennej.
  • Jeśli w kroku 3 powyżej nie udało się wyodrębnić żadnych znaków, wyodrębnianie nie powiodło się. Obiektowi, do którego jest wyodrębniany, przypisana jest wartość 0 (od C++ 11), a wszelkie przyszłe wyodrębnienia natychmiast zakończą się niepowodzeniem (dopóki std::cin nie zostanie wyczyszczone).

Walidacja danych wejściowych

Proces sprawdzania, czy dane wejściowe użytkownika są zgodne z oczekiwaniami programu, nazywa się weryfikacja danych wejściowych.

Istnieją trzy podstawowe sposoby sprawdzania poprawności danych wejściowych:

Inline (w miarę pisania użytkownika):

  1. Zapobiegaj w pierwszej kolejności wpisaniu przez użytkownika nieprawidłowych danych.

Po wpisie (po wpisaniu przez użytkownika):

  1. Pozwól użytkownik może wprowadzić do łańcucha cokolwiek chce, następnie sprawdzić, czy ciąg jest poprawny, a jeśli tak, przekonwertować ciąg do końcowego formatu zmiennej.
  2. Pozwól użytkownikowi wprowadzić, co chce, niech std::cin i operator>> spróbują to wyodrębnić i obsłużyć przypadki błędów.

Niektóre graficzne interfejsy użytkownika i zaawansowane interfejsy tekstowe umożliwiają sprawdzanie wprowadzonych danych przez użytkownika (znak po znaku). Ogólnie rzecz biorąc, programista zapewnia funkcję sprawdzania poprawności, która akceptuje dane wejściowe wprowadzone do tej pory przez użytkownika i zwraca wartość true, jeśli dane wejściowe są prawidłowe, lub false w przeciwnym razie. Funkcja ta wywoływana jest za każdym razem, gdy użytkownik naciśnie klawisz. Jeśli funkcja sprawdzania poprawności zwróci wartość true, klawisz, który właśnie nacisnął użytkownik, zostanie zaakceptowany. Jeśli funkcja sprawdzania poprawności zwróci wartość false, znak wprowadzony przez użytkownika zostanie odrzucony (i nie zostanie wyświetlony na ekranie). Korzystając z tej metody, możesz mieć pewność, że wszelkie dane wprowadzone przez użytkownika będą prawidłowe, ponieważ wszelkie nieprawidłowe naciśnięcia klawiszy zostaną natychmiast wykryte i odrzucone. Niestety std::cin nie obsługuje tego stylu sprawdzania poprawności.

Ponieważ ciągi nie mają żadnych ograniczeń co do liczby wprowadzanych znaków, ekstrakcja z pewnością zakończy się sukcesem (choć pamiętaj, że std::cin zatrzymuje wyodrębnianie na pierwszym, niewiodącym, białym znaku). Po wprowadzeniu ciągu program może go przeanalizować, aby sprawdzić, czy jest on prawidłowy. Jednakże analizowanie ciągów znaków i konwertowanie wprowadzonych ciągów na inne typy (np. liczby) może być trudne, dlatego robi się to tylko w rzadkich przypadkach.

Najczęściej pozwalamy std::cin i operatorowi ekstrakcji wykonać ciężką pracę. W ramach tej metody pozwalamy użytkownikowi wpisać, co chce, std::cin i operator>> próbują to wyodrębnić i radzą sobie z konsekwencjami, jeśli się nie powiedzie. Jest to najłatwiejsza metoda, o której szerzej porozmawiamy poniżej.

Przykładowy program

Rozważmy następujący program kalkulatora, który nie obsługuje błędów:

#include <iostream>
 
double getDouble()
{
    std::cout << "Enter a decimal number: ";
    double x{};
    std::cin >> x;
    return x;
}
 
char getOperator()
{
    std::cout << "Enter one of the following: +, -, *, or /: ";
    char op{};
    std::cin >> op;
    return op;
}
 
void printResult(double x, char operation, double y)
{
    std::cout << x << ' ' << operation << ' ' << y << " is ";

    switch (operation)
    {
    case '+':
        std::cout << x + y << '\n';
        return;
    case '-':
        std::cout << x - y << '\n';
        return;
    case '*':
        std::cout << x * y << '\n';
        return;
    case '/':
        std::cout << x / y << '\n';
        return;
    }
}
 
int main()
{
    double x{ getDouble() };
    char operation{ getOperator() };
    double y{ getDouble() };

    printResult(x, operation, y);
 
    return 0;
}

Ten prosty program prosi użytkownika o wprowadzenie dwóch liczb i operatora matematycznego.

Enter a decimal number: 5
Enter one of the following: +, -, *, or /: *
Enter a decimal number: 7
5 * 7 is 35

Teraz zastanów się, gdzie nieprawidłowe wprowadzenie danych przez użytkownika może spowodować uszkodzenie programu.

Najpierw prosimy użytkownika o wprowadzenie kilku liczb. Co się stanie, jeśli wprowadzą coś innego niż liczbę (np. „q”)? W takim przypadku wyodrębnienie nie powiedzie się.

Po drugie, prosimy użytkownika o wprowadzenie jednego z czterech możliwych symboli. A co jeśli wprowadzą inny znak niż jeden z symboli, których oczekujemy? Będziemy mogli wyodrębnić dane wejściowe, ale obecnie nie zajmujemy się tym, co dzieje się później.

Po trzecie, co się stanie, jeśli poprosimy użytkownika o wprowadzenie symbolu, a on wprowadzi ciąg znaków taki jak "*q hello". Chociaż możemy wyodrębnić '*' znak, którego potrzebujemy, w buforze pozostają dodatkowe dane wejściowe, które mogą powodować problemy w przyszłości.

Rodzaje nieprawidłowego wprowadzania tekstu

Ogólnie możemy podzielić błędy wejściowego tekstu na cztery typy:

  • Wyodrębnianie danych wejściowych powiodło się, ale dane wejściowe nie mają znaczenia dla programu (np. wprowadzenie „k” jako wartości matematycznej operator).
  • Wyodrębnianie danych wejściowych powiodło się, ale użytkownik wprowadził dodatkowe dane wejściowe (np. wpisując „*q hello” jako operator matematyczny).
  • Wyodrębnianie danych wejściowych nie powiodło się (np. próba wprowadzenia „q” do wejścia numerycznego).
  • Wyodrębnianie danych wejściowych powiodło się, ale użytkownik przekroczył wartość liczbową.

W ten sposób, aby zapewnić niezawodność naszych programów, za każdym razem, gdy poprosimy o użytkownika, najlepiej byłoby ustalić, czy każda z powyższych sytuacji może wystąpić, a jeśli tak, napisać kod obsługujący te przypadki.

Przyjrzyjmy się każdemu z tych przypadków i jak sobie z nimi poradzić za pomocą std::cin.

Przypadek błędu 1: Wyodrębnianie powiodło się, ale wprowadzanie danych jest bez znaczenia

To najprostszy przypadek. Rozważmy następującą realizację powyższego programu:

Enter a decimal number: 5
Enter one of the following: +, -, *, or /: k
Enter a decimal number: 7

W tym przypadku poprosiliśmy użytkownika o wprowadzenie jednego z czterech symboli, ale zamiast tego wpisał „k”. „k” jest prawidłowym znakiem, więc std::cin z radością wyodrębnia go do zmiennej op, a ta zostaje zwrócona do main. Jednak nasz program nie spodziewał się, że tak się stanie, więc nie radzi sobie właściwie z tym przypadkiem. W rezultacie otrzymujemy:

5 k 7 is

Rozwiązanie jest proste: wykonaj walidację danych wejściowych. Zwykle składa się to z 3 kroków:

  1. Sprawdź, czy dane wprowadzone przez użytkownika były zgodne z oczekiwaniami.
  2. Jeśli tak, zwróć wartość osobie dzwoniącej.
  3. Jeśli nie, powiedz użytkownikowi, że coś poszło nie tak i poproś go o ponowną próbę.

Oto zaktualizowana funkcja getOperator(), która sprawdza poprawność wprowadzonych danych.

char getOperator()
{
    while (true) // Loop until user enters a valid input
    {
        std::cout << "Enter one of the following: +, -, *, or /: ";
        char operation{};
        std::cin >> operation;

        // Check whether the user entered meaningful input
        switch (operation)
        {
        case '+':
        case '-':
        case '*':
        case '/':
            return operation; // return it to the caller
        default: // otherwise tell the user what went wrong
            std::cout << "Oops, that input is invalid.  Please try again.\n";
        }
    } // and try again
}

Jak widać, używamy pętli while do ciągłego zapętlania, dopóki użytkownik nie wprowadzi prawidłowych danych wejściowych. Jeśli tego nie zrobią, prosimy ich, aby spróbowali ponownie, dopóki nie przekażą nam prawidłowych danych wejściowych, zamkną program lub zniszczą komputer.

Przypadek błędu 2: Wyodrębnianie powiodło się, ale z zewnętrznymi danymi wejściowymi

Rozważ następujące wykonanie powyższego programu:

Enter a decimal number: 5*7

Jak myślisz, co stanie się dalej?

Enter a decimal number: 5*7
Enter one of the following: +, -, *, or /: Enter a decimal number: 5 * 7 is 35

Program wypisuje poprawną odpowiedź, ale całe formatowanie jest błędne w górę. Przyjrzyjmy się bliżej dlaczego.

Gdy użytkownik wprowadzi 5*7 jako dane wejściowe, te dane wejściowe trafiają do bufora. Następnie operator>> wyodrębnia 5 do zmiennej x, pozostawiając *7\n w buforze. Następnie program wypisuje „Wprowadź jedno z poniższych: +, -, * lub /:”. Jednak po wywołaniu operatora wyodrębniania widzi *7\n czekający w buforze na wyodrębnienie, więc używa go, zamiast prosić użytkownika o dalsze dane wejściowe. W rezultacie wyodrębnia znak „*”, pozostawiając 7\n w buforze.

Po poproszeniu użytkownika o wprowadzenie kolejnej liczby dziesiętnej, 7 z bufora zostaje wyodrębniony bez pytania użytkownika. Ponieważ użytkownik nigdy nie miał możliwości wprowadzenia dodatkowych danych i naciśnięcia klawisza Enter (powodując pojawienie się nowej linii), wszystkie monity wyjściowe są wyświetlane w tej samej linii.

Chociaż powyższy program działa, wykonanie jest nieuporządkowane. Byłoby lepiej, gdyby wszelkie wprowadzone znaki obce zostały po prostu zignorowane. Na szczęście znaki można łatwo zignorować:

std::cin.ignore(100, '\n');  // clear up to 100 characters out of the buffer, or until a '\n' character is removed

To wywołanie usunie do 100 znaków, ale jeśli użytkownik wprowadzi więcej niż 100 znaków, ponownie otrzymamy nieuporządkowany wynik. Aby zignorować wszystkie znaki aż do następnego „\n”, możemy przekazać std::numeric_limits<std::streamsize>::max() Do >std::cin.ignore(). std::numeric_limits<std::streamsize>::max() zwraca największą wartość, jaką można zapisać w zmiennej typu std::streamsize. Przekazanie tej wartości do std::cin.ignore() powoduje wyłączenie sprawdzania zliczania.

Aby zignorować wszystko aż do następnego znaku „\n” włącznie, wywołujemy

std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

Ponieważ ta linia jest dość długa jak na jej działanie, wygodnie jest zawinąć ją w funkcję, którą można wywołać zamiast std::cin.ignore().

#include <limits> // for std::numeric_limits

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

Ponieważ ostatnim wprowadzonym przez użytkownika znakiem jest zazwyczaj „\n”, możemy tell std::cin aby ignorować buforowane znaki, dopóki nie znajdzie znaku nowej linii (który również zostanie usunięty).

Zaktualizujmy naszą funkcję getDouble(), aby ignorowała wszelkie zewnętrzne dane wejściowe:

double getDouble()
{
    std::cout << "Enter a decimal number: ";
    double x{};
    std::cin >> x;

    ignoreLine();
    return x;
}

Teraz nasz program będzie działał zgodnie z oczekiwaniami, nawet jeśli wprowadzimy 5*7 dla pierwszego wejścia - 5 zostanie wyodrębnionych, a pozostałe znaki zostaną usunięte z bufora wejściowego. Ponieważ bufor wejściowy jest teraz pusty, użytkownik zostanie poprawnie poproszony o wprowadzenie danych przy następnej operacji wyodrębniania!

Wskazówka

W niektórych przypadkach może być lepiej potraktować obce dane wejściowe jako przypadek niepowodzenia (zamiast je ignorować). Następnie możemy poprosić użytkownika o ponowne wprowadzenie danych.

Oto odmiana getDouble() która prosi użytkownika o ponowne wprowadzenie danych, jeśli zostaną wprowadzone dodatkowe dane:

// returns true if std::cin has unextracted input on the current line, false otherwise
bool hasUnextractedInput()
{
    return !std::cin.eof() && std::cin.peek() != '\n';
}

double getDouble()
{
    while (true) // Loop until user enters a valid input
    {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        // NOTE: YOU SHOULD CHECK FOR A FAILED EXTRACTION HERE (see section below)

        // If there is extraneous input, treat as failure case
        if (hasUnextractedInput())
        {
            ignoreLine(); // remove extraneous input
            continue;
        }
    
        return x;
    }
}

Powyższy fragment wykorzystuje dwie funkcje, których wcześniej nie widzieliśmy:

  • Klasa std::cin.eof() funkcja zwraca true jeśli ostatnia operacja wejściowa (w tym przypadku ekstrakcja do x) osiągnął koniec strumienia wejściowego. Funkcja
  • Klasa std::cin.peek() pozwala nam zajrzeć do następnego znaku w strumieniu wejściowym bez jego wyodrębniania.

Oto jak działa ta funkcja. Po wyodrębnieniu danych wejściowych użytkownika do x, w std::cin.

mogą pozostać dodatkowe (niewyodrębnione) znaki, ale nie muszą. Najpierw wywołujemy std::cin.eof() , aby sprawdzić, czy ekstrakcja do x dotarła do końca strumienia wejściowego. Jeśli tak, to wiemy, że wszystkie znaki zostały wyodrębnione, co jest sukcesem.

W przeciwnym razie w środku muszą znajdować się dodatkowe znaki std::cin oczekujące na wyodrębnienie. W takim przypadku wywołujemy std::cin.peek() aby zajrzeć do następnego znaku oczekującego na wyodrębnienie, bez faktycznego jego wyodrębniania. Jeśli następnym znakiem jest '\n', oznacza to, że wszystkie znaki w tym wierszu wejściowym zostały już wyodrębnione do x. To także przypadek sukcesu.

Jeśli jednak następny znak jest inny niż '\n', wówczas użytkownik musiał wprowadzić dodatkowe dane wejściowe, które nie zostały wyodrębnione do x. To nasz przypadek porażki. Usuwamy wszystkie niepotrzebne dane wejściowe i continue wracamy na początek pętli, aby spróbować ponownie.

Jeśli masz problemy z rozszyfrowaniem sposobu obliczania wyrażenia logicznego w hasUnextractedInput() , nie jest to zaskakujące — wyrażenia logiczne z negacjami mogą być trudne do zrozumienia. W takich przypadkach pomocne może być zastosowanie prawa de Morgana. Równoważne stwierdzenie to return !(std::cin.eof() || std::cin.peek() == '\n');. Dzięki temu staje się jaśniejsze, że testujemy EOF lub nową linię. Jeśli którekolwiek z nich jest prawdziwe, wyodrębniliśmy wszystkie dane wejściowe. Następnie stosujemy operator! aby poinformować nas, czy nie wyodrębniliśmy wszystkich danych wejściowych, co oznacza, że ​​nadal istnieją niewyodrębnione dane wejściowe.

Przypadek błędu 3: Wyodrębnienie nie powiodło się

Wyodrębnianie kończy się niepowodzeniem, gdy do określonej zmiennej nie można wyodrębnić żadnych danych wejściowych.

Rozważmy teraz następujące wykonanie naszego zaktualizowanego programu kalkulatora:

Enter a decimal number: a

Nie powinieneś być zaskoczony, że program nie działa zgodnie z oczekiwaniami, ale to, w jaki sposób to się nie powiedzie, jest interesujące:

Enter a decimal number: a
Enter one of the following: +, -, *, or /: Oops, that input is invalid.  Please try again.
Enter one of the following: +, -, *, or /: Oops, that input is invalid.  Please try again.
Enter one of the following: +, -, *, or /: Oops, that input is invalid.  Please try again.

i ostatnia linia jest drukowana aż do zamknięcia programu.

Wygląda to bardzo podobnie do przypadku zbędnych danych wejściowych, ale jest trochę inaczej. Przyjrzyjmy się bliżej.

Gdy użytkownik wpisze „a”, znak ten zostaje umieszczony w buforze. Następnie operator>> próbuje wyodrębnić „a” do zmiennej x, która jest typu double. Ponieważ „a” nie może zostać przekonwertowane na liczbę podwójną, operator>> nie można przeprowadzić ekstrakcji. W tym momencie dzieją się dwie rzeczy: „a” pozostaje w buforze, a std::cin przechodzi w „tryb awarii”.

Po wejściu w „tryb awarii” przyszłe żądania wyodrębnienia danych wejściowych będą dyskretnie kończyć się niepowodzeniem. Zatem w naszym programie kalkulatora monity wyjściowe są nadal drukowane, ale wszelkie żądania dalszego wyodrębnienia są ignorowane. Oznacza to, że zamiast czekać, aż wejdziemy w operację, monit o wprowadzenie danych jest pomijany i utkniemy w nieskończonej pętli, ponieważ nie ma możliwości dotarcia do jednego z prawidłowych przypadków.

Aby std::cin znowu działać poprawnie, zazwyczaj musimy wykonać trzy rzeczy:

  • Wykryć, że wcześniejsze wyodrębnienie nie powiodło się.
  • Przywróć std::cin normalne działanie
  • Usuń dane wejściowe, które spowodowały awarię (aby następne żądanie wyodrębnienia nie zakończyło się niepowodzeniem w identyczny sposób).

Oto jak to wygląda:

if (std::cin.fail()) // If the previous extraction failed
{
    // Let's handle the failure
    std::cin.clear(); // Put us back in 'normal' operation mode
    ignoreLine();     // And remove the bad input
}

Ponieważ std::cin ma konwersję logiczną wskazującą, czy ostatnie dane wejściowe powiodły się, bardziej idiomatyczne jest zapisanie powyższego w następujący sposób:

if (!std::cin) // If the previous extraction failed
{
    // Let's handle the failure
    std::cin.clear(); // Put us back in 'normal' operation mode
    ignoreLine();     // And remove the bad input
}

Kluczowa informacja

Gdy wyodrębnianie nie powiedzie się, przyszłe żądania wyodrębnienia danych wejściowych (w tym wywołania) do ignore()) zakończy się niepowodzeniem do czasu wywołania funkcji clear() . Zatem po wykryciu nieudanej ekstrakcji, wywołanie clear() jest zwykle pierwszą rzeczą, którą powinieneś zrobić.

Zintegrujmy to z naszą funkcją getDouble():

double getDouble()
{
    while (true) // Loop until user enters a valid input
    {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        if (!std::cin) // If the previous extraction failed
        {
            // Let's handle the failure
            std::cin.clear(); // Put us back in 'normal' operation mode
            ignoreLine();     // And remove the bad input
            continue;
        }

        // Our extraction succeeded
        ignoreLine(); // Ignore any additional input on this line
        return x;     // Return the value we extracted
    }
}

W przypadku typów podstawowych nieudana ekstrakcja z powodu nieprawidłowych danych wejściowych spowoduje, że zmiennej zostanie przypisana wartość 0 (lub inna wartość 0 konwertowana w zmiennej wpisz).

Możesz wywołać clear() nawet jeśli ekstrakcja nie zakończyła się niepowodzeniem – to nic nie da. W przypadkach, w których zamierzamy wywołać ignoreLine() niezależnie od tego, czy odnieśliśmy sukces, czy nie, możemy zasadniczo połączyć dwa przypadki:

double getDouble()
{
    while (true) // Loop until user enters a valid input
    {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        bool success { std::cin }; // Remember whether we had a successful extraction
        std::cin.clear();          // Put us back in 'normal' operation mode (in case we failed)
        ignoreLine();              // Ignore any additional input on this line (regardless)

        if (success)               // If we actually extracted a value
            return x;              // Return it (otherwise, we go back to top of loop)
    }
}

Sprawdzanie EOF

Jest jeszcze jeden przypadek, którym musimy się zająć.

Koniec pliku (EOF) to specjalny stan błędu, który oznacza „Brak dostępnych danych”. Jest to zwykle generowane po operacja wejściowa kończy się niepowodzeniem z powodu braku dostępnych danych. Na przykład, jeśli czytałeś zawartość pliku na dysku, a następnie próbowałeś wczytać więcej danych po osiągnięciu końca pliku, zostanie wygenerowany EOF informujący, że nie ma więcej dostępnych danych. W przypadku wprowadzania pliku nie stanowi to problemu - możemy po prostu zamknąć plik i przejść dalej.

Teraz rozważ std::cin. Jeśli spróbujemy wyodrębnić dane wejściowe z std::cin , a ich nie ma, zgodnie z projektem nie wygeneruje on EOF - po prostu poczeka, aż użytkownik wprowadzi więcej danych wejściowych. Jednakże std::cin może wygenerować EOF w niektórych przypadkach — najczęściej gdy użytkownik wprowadzi specjalną kombinację klawiszy dla swojego systemu operacyjnego. Zarówno Unix (przez ctrl-D), jak i Windows (przez ctrl-Z + ENTER) obsługują wprowadzanie „znaku EOF” z klawiatury.

Kluczowa informacja

W C++ EOF jest stanem błędu, a nie znakiem. Różne systemy operacyjne mają specjalne kombinacje znaków, które są traktowane jako „wprowadzone przez użytkownika żądanie EOF”. Są one czasami nazywane „znakami EOF”.

Podczas wyodrębniania danych do std::cin i użytkownik wprowadzi znak EOF, zachowanie jest specyficzne dla systemu operacyjnego. Oto, co zwykle się dzieje:

  • Jeśli EOF nie jest pierwszym znakiem wejściowym: Wszystkie dane wejściowe poprzedzające EOF zostaną usunięte, a znak EOF zostanie zignorowany. W systemie Windows wszelkie znaki wprowadzone po EOF są ignorowane, z wyjątkiem nowej linii.
  • Jeśli EOF jest pierwszym wprowadzonym znakiem: Zostanie ustawiony błąd EOF. Strumień wejściowy może (ale nie musi) zostać odłączony.

Chociaż std::cin.clear() skasuje błąd EOF, jeśli strumień wejściowy zostanie odłączony, następne żądanie wejściowe wygeneruje kolejny błąd EOF. Jest to problematyczne, gdy nasze dane wejściowe znajdują się w while(true) pętli, ponieważ utkniemy w nieskończonej pętli błędów EOF.

Ponieważ znak wprowadzany z klawiatury EOF ma na celu zakończenie strumienia wejściowego, najlepiej jest wykryć EOF (przez std::cin.eof()) i następnie zakończyć program.

Ponieważ wyczyszczenie nieudanego strumienia wejściowego to coś, co prawdopodobnie będziemy często sprawdzać. Jest to dobry kandydat na funkcję wielokrotnego użytku:

#include <limits> // for std::numeric_limits

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

// returns true if extraction failed, false otherwise
bool clearFailedExtraction()
{
    // Check for failed extraction
    if (!std::cin) // If the previous extraction failed
    {
        if (std::cin.eof()) // If the user entered an EOF
        {
            std::exit(0); // Shut down the program now
        }

        // Let's handle the failure
        std::cin.clear(); // Put us back in 'normal' operation mode
        ignoreLine();     // And remove the bad input

        return true;
    }
    
    return false;
}

Przypadek błędu 4: Wyodrębnianie powiodło się, ale użytkownik przekroczył wartość liczbową

Rozważ następujący prosty przykład:

#include <cstdint>
#include <iostream>

int main()
{
    std::int16_t x{}; // x is 16 bits, holds from -32768 to 32767
    std::cout << "Enter a number between -32768 and 32767: ";
    std::cin >> x;

    std::int16_t y{}; // y is 16 bits, holds from -32768 to 32767
    std::cout << "Enter another number between -32768 and 32767: ";
    std::cin >> y;

    std::cout << "The sum is: " << x + y << '\n';
    return 0;
}

Co się stanie, jeśli użytkownik wprowadzi zbyt dużą liczbę (np. 40000)?

Enter a number between -32768 and 32767: 40000
Enter another number between -32768 and 32767: The sum is: 32767

W powyższym przypadku std::cin przechodzi natychmiast do „tryb awaryjny”, ale także przypisuje zmiennej najbliższą wartość z zakresu. Jeżeli wprowadzona wartość jest większa niż największa możliwa wartość dla typu, najbliższą wartością z zakresu jest największa możliwa wartość dla typu. W rezultacie x pozostaje przypisana wartość 32767. Dodatkowe wejścia są pomijane, pozostawiając y z zainicjowaną wartością 0. Z tego rodzaju błędem możemy sobie poradzić w taki sam sposób, jak z nieudaną ekstrakcją.

Składanie wszystkiego w całość

Oto nasz przykładowy kalkulator, zaktualizowany o kilka dodatkowych elementów sprawdzania błędów:

#include <cstdlib> // for std::exit
#include <iostream>
#include <limits> // for std::numeric_limits

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

// returns true if extraction failed, false otherwise
bool clearFailedExtraction()
{
    // Check for failed extraction
    if (!std::cin) // If the previous extraction failed
    {
        if (std::cin.eof()) // If the stream was closed
        {
            std::exit(0); // Shut down the program now
        }

        // Let's handle the failure
        std::cin.clear(); // Put us back in 'normal' operation mode
        ignoreLine();     // And remove the bad input

        return true;
    }
    
    return false;
}

double getDouble()
{
    while (true) // Loop until user enters a valid input
    {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        if (clearFailedExtraction())
        {
            std::cout << "Oops, that input is invalid.  Please try again.\n";
            continue;
        }

        ignoreLine(); // Remove any extraneous input
        return x;     // Return the value we extracted
    }
}

char getOperator()
{
    while (true) // Loop until user enters a valid input
    {
        std::cout << "Enter one of the following: +, -, *, or /: ";
        char operation{};
        std::cin >> operation;

        if (!clearFailedExtraction()) // we'll handle error messaging if extraction failed below
             ignoreLine(); // remove any extraneous input (only if extraction succeded)

        // Check whether the user entered meaningful input
        switch (operation)
        {
        case '+':
        case '-':
        case '*':
        case '/':
            return operation; // Return the entered char to the caller
        default: // Otherwise tell the user what went wrong
            std::cout << "Oops, that input is invalid.  Please try again.\n";
        }
    }
}
 
void printResult(double x, char operation, double y)
{
    std::cout << x << ' ' << operation << ' ' << y << " is ";

    switch (operation)
    {
    case '+':
        std::cout << x + y << '\n';
        return;
    case '-':
        std::cout << x - y << '\n';
        return;
    case '*':
        std::cout << x * y << '\n';
        return;
    case '/':
        if (y == 0.0)
            break;

        std::cout << x / y << '\n';
        return;
    }

    std::cout << "???";  // Being robust means handling unexpected parameters as well, even though getOperator() guarantees operation is valid in this particular program
}
 
int main()
{
    double x{ getDouble() };
    char operation{ getOperator() };
    double y{ getDouble() };

    // Handle division by 0
    while (operation == '/' && y == 0.0) 
    {
        std::cout << "The denominator cannot be zero.  Try again.\n";
        y = getDouble();
    }
 
    printResult(x, operation, y);
 
    return 0;
}

Wnioski

Pisząc programy, zastanów się, w jaki sposób użytkownicy będą go niewłaściwie używać, zwłaszcza przy wprowadzaniu tekstu. Dla każdego punktu wprowadzania tekstu rozważ:

  • Czy ekstrakcja może się nie udać?
  • Czy użytkownik może wprowadzić więcej danych wejściowych niż oczekiwano?
  • Czy użytkownik może wprowadzić bezsensowne dane wejściowe?
  • Czy użytkownik może przepełnić dane wejściowe?

Możesz użyć instrukcji if i logiki logicznej, aby sprawdzić, czy dane wejściowe są oczekiwane i znaczące.

Poniższe kod usunie wszelkie niepotrzebne dane wejściowe:

#include <limits> // for std::numeric_limits

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

Następujący kod przetestuje i naprawi nieudane wyodrębnianie lub przepełnienie (oraz usunie niepotrzebne dane wejściowe):

// returns true if extraction failed, false otherwise
bool clearFailedExtraction()
{
    // Check for failed extraction
    if (!std::cin) // If the previous extraction failed
    {
        if (std::cin.eof()) // If the stream was closed
        {
            std::exit(0); // Shut down the program now
        }

        // Let's handle the failure
        std::cin.clear(); // Put us back in 'normal' operation mode
        ignoreLine();     // And remove the bad input

        return true;
    }
    
    return false;
}

Możemy sprawdzić, czy istnieją niewyodrębnione dane wejściowe (inne niż znak nowej linii) w następujący sposób:

// returns true if std::cin has unextracted input on the current line, false otherwise
bool hasUnextractedInput()
{
    return !std::cin.eof() && std::cin.peek() != '\n';
}

Na koniec użyj pętli, aby poprosić użytkownika o ponowne wprowadzenie danych wejściowych, jeśli oryginalne dane wejściowe były nieprawidłowe.

Nota autora

Weryfikacja danych wejściowych jest ważne i przydatne, ale powoduje też, że przykłady stają się bardziej skomplikowane i trudniejsze do naśladowania. W związku z tym na przyszłych lekcjach zasadniczo nie będziemy przeprowadzać żadnego rodzaju weryfikacji danych wejściowych, chyba że będzie to istotne dla czegoś, czego staramy się uczyć.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
561 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze