28.5 — Stany strumieni i weryfikacja danych wejściowych

Stany strumieni

Klasa ios_base zawiera kilka flag stanu, które służą do sygnalizowania różnych warunków, które mogą wystąpić podczas używania strumienie:

FlagZnaczenie
goodbitWszystko w porządku
badbitWystąpił jakiś błąd krytyczny (np. program próbował odczytać plik poza końcem pliku)
eofbitStrumień osiągnął koniec pliku
failbitA wystąpił błąd niekrytyczny (np. użytkownik wprowadził litery, gdy program oczekiwał liczby całkowitej)

Chociaż te flagi znajdują się w ios_base, ponieważ ios wywodzi się z ios_base, a ios wymaga mniej pisania niż ios_base, ogólnie są dostępne za pośrednictwem ios (np. jako std::ios::failbit).

ios zapewnia również szereg funkcji składowych umożliwiających wygodny dostęp do nich stwierdza:

Funkcja składowaZnaczenie
good()Zwraca wartość true, jeśli ustawiony jest dobry bit (strumień jest w porządku)
bad()Zwraca wartość true, jeśli ustawiony jest badbit (wystąpił błąd krytyczny)
eof()Zwraca wartość true, jeśli eofbit jest ustawiony (strumień znajduje się na końcu plik)
fail()Zwraca wartość true, jeśli ustawiony jest bit niepowodzenia (wystąpił błąd niekrytyczny)
clear()Usuwa wszystkie flagi i przywraca strumień do stanu dobrego bitu
clear(state)Usuwa wszystkie flagi i ustawia przekazaną flagę stanu in
rdstate()Zwraca aktualnie ustawione flagi
setstate(state)Ustawia flagę stanu przekazaną w

Najczęściej stosowanym bitem jest bit awarii, który jest ustawiany, gdy użytkownik wprowadzi nieprawidłowe dane wejściowe. Rozważmy na przykład następujący program:

std::cout << "Enter your age: ";
int age {};
std::cin >> age;

Zauważ, że ten program oczekuje, że użytkownik wprowadzi liczbę całkowitą. Jeśli jednak użytkownik wprowadzi dane nienumeryczne, takie jak „Alex”, cin nie będzie mógł wyodrębnić niczego do wieku i zostanie ustawiony bit niepowodzenia.

Jeśli wystąpi błąd i strumień zostanie ustawiony na inną wartość niż goodbit, dalsze operacje na tym strumieniu będą ignorowane. Warunek ten można usunąć wywołując funkcję clear().

Weryfikacja danych wejściowych

Weryfikacja danych wejściowych to proces sprawdzania, czy dane wejściowe użytkownika spełniają określony zestaw kryteriów. Sprawdzanie poprawności danych wejściowych można ogólnie podzielić na dwa typy: ciąg znaków i numeryczna.

W przypadku sprawdzania poprawności ciągu, akceptujemy wszystkie dane wprowadzone przez użytkownika jako ciąg, a następnie akceptujemy lub odrzucamy ten ciąg w zależności od tego, czy jest on odpowiednio sformatowany. Na przykład, jeśli poprosimy użytkownika o podanie numeru telefonu, możemy chcieć upewnić się, że wprowadzane przez niego dane składają się z dziesięciu cyfr. W większości języków (szczególnie języków skryptowych, takich jak Perl i PHP) odbywa się to za pomocą wyrażeń regularnych. Biblioteka standardowa C++ zawiera bibliotekę wyrażeń regularnych . Ponieważ wyrażenia regularne są powolne w porównaniu z ręczną walidacją ciągów znaków, należy ich używać tylko wtedy, gdy wydajność (w czasie kompilacji i wykonywania) nie ma znaczenia lub ręczna walidacja jest zbyt uciążliwa.

W przypadku walidacji numerycznej zazwyczaj staramy się upewnić, że liczba wprowadzana przez użytkownika mieści się w określonym zakresie (np. od 0 do 20). Jednak w przeciwieństwie do sprawdzania ciągów znaków użytkownik może wprowadzić elementy, które w ogóle nie są liczbami — i w takich przypadkach również musimy sobie poradzić.

Aby nam w tym pomóc, C++ udostępnia szereg przydatnych funkcji, których możemy użyć do określenia, czy określone znaki są cyframi, czy literami. W nagłówku cctype znajdują się następujące funkcje:

FunkcjaZnaczenie
std::isalnum(int)Zwraca wartość różną od zera, jeśli parametr jest literą lub cyfrą
std::isalpha(int)Zwraca wartość różną od zera, jeśli parametr jest litera
std::iscntrl(int)Zwraca wartość różną od zera, jeśli parametr jest znakiem kontrolnym
std::isdigit(int)Zwraca wartość różną od zera, jeśli parametr jest cyfrą
std::isgraph(int)Zwraca wartość różną od zera, jeśli parametr jest znakiem drukowalnym, który nie jest białym znakiem
std::isprint(int)Zwraca wartość różną od zera, jeśli parametr jest znakiem drukowalnym (w tym białe znaki)
std::ispunct(int)Zwraca wartość różną od zera, jeśli parametr nie jest ani alfanumeryczny, ani biały
std::isspace(int)Zwraca wartość różną od zera, jeśli parametr jest białym znakiem
std::isxdigit(int)Zwraca niezerowy, jeśli parametr jest cyfrą szesnastkową (0-9, a-f, A-F)

Weryfikacja ciągu

Zróbmy prosty przypadek sprawdzenia poprawności ciągu, prosząc użytkownika o podanie swojej nazwy. Naszymi kryteriami weryfikacji będzie to, że użytkownik wprowadza tylko znaki alfabetu lub spacje. Jeśli napotkane zostanie cokolwiek innego, dane wejściowe zostaną odrzucone.

Jeśli chodzi o dane wejściowe o zmiennej długości, najlepszym sposobem sprawdzania poprawności ciągów (oprócz użycia biblioteki wyrażeń regularnych) jest przejrzenie każdego znaku ciągu i upewnienie się, że spełnia on kryteria sprawdzania poprawności. Dokładnie to tutaj zrobimy, albo lepiej, to właśnie std::all_of robi dla nas.

#include <algorithm> // std::all_of
#include <cctype> // std::isalpha, std::isspace
#include <iostream>
#include <ranges>
#include <string>
#include <string_view>

bool isValidName(std::string_view name)
{
  return std::ranges::all_of(name, [](char ch) {
    return std::isalpha(ch) || std::isspace(ch);
  });

  // Before C++20, without ranges
  // return std::all_of(name.begin(), name.end(), [](char ch) {
  //    return std::isalpha(ch) || std::isspace(ch);
  // });
}

int main()
{
  std::string name{};

  do
  {
    std::cout << "Enter your name: ";
    std::getline(std::cin, name); // get the entire line, including spaces
  } while (!isValidName(name));

  std::cout << "Hello " << name << "!\n";
}

Zauważ, że ten kod nie jest doskonały: użytkownik może powiedzieć, że jego imię to „asf w jweo s di we ao” lub jakiś inny bełkot, lub co gorsza, po prostu kilka spacji. Moglibyśmy rozwiązać ten problem, doprecyzowując nasze kryteria walidacji, aby akceptowały tylko ciągi znaków zawierające co najmniej jeden znak i co najwyżej jedną spację.

Nota autora

Czytelnik „Waldo” udostępnia rozwiązanie C++20 (używając std::ranges), które eliminuje te niedociągnięcia. tutaj

Przyjrzyjmy się teraz innemu przykładowi, w którym poprosimy użytkownika o podanie numeru telefonu. W przeciwieństwie do nazwy użytkownika, która ma zmienną długość i gdzie kryteria sprawdzania poprawności są takie same dla każdego znaku, numer telefonu ma stałą długość, ale kryteria sprawdzania różnią się w zależności od pozycji znaku. W związku z tym zastosujemy inne podejście do sprawdzania poprawności wprowadzonego numeru telefonu. W tym przypadku napiszemy funkcję, która sprawdzi, czy dane wprowadzone przez użytkownika pasują do wcześniej ustalonego szablonu. Szablon będzie działał w następujący sposób:

A # dopasuje dowolną cyfrę wprowadzoną przez użytkownika.
A @ dopasuje dowolny znak alfabetu wpisany przez użytkownika.
A _ dopasuje dowolne białe znaki.
A ? będzie pasować do czegokolwiek.
W przeciwnym razie znaki wprowadzone przez użytkownika i szablon muszą dokładnie pasować.

Jeśli więc poprosimy funkcję o dopasowanie do szablonu „(###) ###-####”, oznacza to, że oczekujemy, że użytkownik wprowadzi znak „(”, trzy cyfry, znak „)”, spację, trzy cyfry, myślnik i jeszcze cztery cyfry. Jeśli którykolwiek z tych elementów nie pasuje, dane wejściowe zostaną odrzucone.

Oto kod:

#include <algorithm> // std::equal
#include <cctype> // std::isdigit, std::isspace, std::isalpha
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <string_view>

bool inputMatches(std::string_view input, std::string_view pattern)
{
    if (input.length() != pattern.length())
    {
        return false;
    }

    // This table defines all special symbols that can match a range of user input
    // Each symbol is mapped to a function that determines whether the input is valid for that symbol
    static const std::map<char, int (*)(int)> validators{
      { '#', &std::isdigit },
      { '_', &std::isspace },
      { '@', &std::isalpha },
      { '?', [](int) { return 1; } }
    };

    // Before C++20, use
    // return std::equal(input.begin(), input.end(), pattern.begin(), [](char ch, char mask) -> bool {
    // ...

    return std::ranges::equal(input, pattern, [](char ch, char mask) -> bool {
        auto found{ validators.find(mask) };
        
        if (found != validators.end())
        {
            // The pattern's current element was found in the validators. Call the
            // corresponding function.
            return (*found->second)(ch);
        }

        // The pattern's current element was not found in the validators. The
        // characters have to be an exact match.
        return ch == mask;
        }); // end of lambda
}

int main()
{
    std::string phoneNumber{};

    do
    {
        std::cout << "Enter a phone number (###) ###-####: ";
        std::getline(std::cin, phoneNumber);
    } while (!inputMatches(phoneNumber, "(###) ###-####"));

    std::cout << "You entered: " << phoneNumber << '\n';
}

Korzystając z tej funkcji, możemy zmusić użytkownika do dokładnego dopasowania naszego konkretnego formatu. Jednak ta funkcja nadal podlega kilku ograniczeniom: jeśli #, @, _ i ? są prawidłowymi znakami wprowadzonymi przez użytkownika, ta funkcja nie będzie działać, ponieważ tym symbolom nadano specjalne znaczenie. Ponadto, w przeciwieństwie do wyrażeń regularnych, nie ma tam symbolu szablonu, który oznacza, że ​​„można wprowadzić zmienną liczbę znaków”. Tym samym takiego szablonu nie można zastosować do zapewnienia, że ​​użytkownik wprowadzi dwa słowa oddzielone spacją, ponieważ nie radzi sobie z faktem, że słowa mają zmienną długość. W przypadku takich problemów ogólnie bardziej odpowiednie jest podejście inne niż szablonowe.

Weryfikacja numeryczna

W przypadku wprowadzania danych liczbowych oczywistym sposobem postępowania jest użycie operatora wyodrębniania w celu wyodrębnienia danych wejściowych do typu liczbowego. Sprawdzając bit błędu, możemy stwierdzić, czy użytkownik wprowadził liczbę, czy nie.

Wypróbujmy to podejście:

#include <iostream>
#include <limits>

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::cin >> age;

        if (std::cin.fail()) // no extraction took place
        {
            std::cin.clear(); // reset the state bits back to goodbit so we can use ignore()
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out the bad input from the stream
            continue; // try again
        }

        if (age <= 0) // make sure age is positive
            continue;

        break;
    }

    std::cout << "You entered: " << age << '\n';
}

Jeśli użytkownik wprowadzi liczbę całkowitą, ekstrakcja zakończy się pomyślnie. std::cin.fail() zwróci wartość false, pomijając warunek, i (zakładając, że użytkownik wprowadził liczbę dodatnią), wykonamy instrukcję break, opuszczając pętlę.

Jeśli zamiast tego użytkownik wprowadzi dane wejściowe rozpoczynające się od litery, wyodrębnianie zakończy się niepowodzeniem. std::cin.fail() zwróci wartość true i przejdziemy do trybu warunkowego. Na końcu bloku warunkowego wciśniemy instrukcjęcontinue, która przeskoczy z powrotem na początek pętli while, a użytkownik zostanie poproszony o ponowne wprowadzenie danych.

Jest jednak jeszcze jeden przypadek, którego nie testowaliśmy, a mianowicie, gdy użytkownik wprowadza ciąg znaków zaczynający się od cyfr, ale następnie zawierający litery (np. „34abcd56”). W tym przypadku liczby początkowe (34) zostaną wyodrębnione do wieku, pozostała część łańcucha („abcd56”) pozostanie w strumieniu wejściowym, a bit błędu NIE zostanie ustawiony. Powoduje to dwa potencjalne problemy:

  1. Jeśli chcesz, żeby to były prawidłowe dane wejściowe, w Twoim strumieniu znajdują się teraz śmieci.
  2. Jeśli nie chcesz, aby to były prawidłowe dane wejściowe, nie są one odrzucane (i masz śmieci w swoim strumieniu).

Rozwiążmy pierwszy problem. To proste:

#include <iostream>
#include <limits>

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::cin >> age;

        if (std::cin.fail()) // no extraction took place
        {
            std::cin.clear(); // reset the state bits back to goodbit so we can use ignore()
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out the bad input from the stream
            continue; // try again
        }

        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out any additional input from the stream

        if (age <= 0) // make sure age is positive
            continue;

      break;
    }

    std::cout << "You entered: " << age << '\n';
}

Jeśli nie chcesz, aby takie dane wejściowe były prawidłowe, będziemy musieli wykonać trochę dodatkowej pracy. Na szczęście poprzednie rozwiązanie prowadzi nas w połowie drogi. Możemy użyć funkcji gcount(), aby określić, ile znaków zostało zignorowanych. Jeśli nasze dane wejściowe były prawidłowe, gcount() powinna zwrócić 1 (znak nowej linii, który został odrzucony). Jeśli zwróci więcej niż 1, użytkownik wprowadził coś, co nie zostało poprawnie wyodrębnione i powinniśmy poprosić go o nowe dane. Oto przykład:

#include <iostream>
#include <limits>

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::cin >> age;

        if (std::cin.fail()) // no extraction took place
        {
            std::cin.clear(); // reset the state bits back to goodbit so we can use ignore()
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out the bad input from the stream
            continue; // try again
        }

        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out any additional input from the stream
        if (std::cin.gcount() > 1) // if we cleared out more than one additional character
        {
            continue; // we'll consider this input to be invalid
        }

        if (age <= 0) // make sure age is positive
        {
            continue;
        }

        break;
    }

    std::cout << "You entered: " << age << '\n';
}

Weryfikacja liczb jako ciąg znaków

Powyższy przykład wymagał sporo pracy, aby uzyskać prostą wartość! Innym sposobem przetwarzania danych liczbowych jest wczytanie ich jako ciągu znaków, a następnie próba przekonwertowania ich na typ numeryczny. Poniższy program korzysta z tej metodologii:

#include <charconv> // std::from_chars
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>

// std::optional<int> returns either an int or nothing
std::optional<int> extractAge(std::string_view age)
{
    int result{};
    const auto end{ age.data() + age.length() }; // get end iterator of underlying C-style string

    // Try to parse an int from age
    // If we got an error of some kind...
    if (std::from_chars(age.data(), end, result).ec != std::errc{})
    {
        return {}; // return nothing
    }

    if (result <= 0) // make sure age is positive
    {
        return {}; // return nothing
    }

    return result; // return an int value
}

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::string strAge{};

        // Try to get a line of input
        if (!std::getline(std::cin >> std::ws, strAge))
        {
            // If we failed, clean up and try again
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');    
            continue;
        }

        // Try to extract the age
        auto extracted{ extractAge(strAge) };

        // If we failed, try again
        if (!extracted)
            continue;

        age = *extracted; // get the value
        break;
    }

  std::cout << "You entered: " << age << '\n';
}

To, czy to podejście wymaga więcej, czy mniej pracy niż prosta ekstrakcja liczbowa, zależy od parametrów i ograniczeń walidacji.

Jak widać, sprawdzanie poprawności danych wejściowych w C++ wymaga dużo pracy. Na szczęście wiele takich zadań (np. sprawdzanie poprawności numerycznej w postaci ciągu znaków) można łatwo przekształcić w funkcje, które można ponownie wykorzystać w różnorodnych sytuacjach.

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:  
124 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze