Dlaczego std :: getline () pomija dane wejściowe po sformatowanym wyodrębnieniu?

105

Mam następujący fragment kodu, który prosi użytkownika o podanie nazwy i stanu:

#include <iostream>
#include <string>

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

    if (std::cin >> name && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
}

Uważam, że nazwa została pomyślnie wyodrębniona, ale nie stan. Oto dane wejściowe i wynikowe:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Dlaczego nazwa stanu została pominięta w danych wyjściowych? Podałem właściwe dane wejściowe, ale kod jakoś je ignoruje. Dlaczego to się dzieje?

0x499602D2
źródło
Uważam, że std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)powinno również działać zgodnie z oczekiwaniami. (Oprócz odpowiedzi poniżej).
jww

Odpowiedzi:

122

Dlaczego to się dzieje?

Ma to niewiele wspólnego z danymi wejściowymi, które sam podałeś, ale raczej z domyślnymi std::getline()pokazami zachowania . Kiedy podałeś swoje dane wejściowe dla name ( std::cin >> name), nie tylko przesłałeś następujące znaki, ale także niejawny znak nowej linii został dołączony do strumienia:

"John\n"

Nowa linia jest zawsze dodawana do twoich danych wejściowych, gdy wybierasz Enterlub Returnprzesyłasz z terminala. Jest również używany w plikach do przechodzenia do następnego wiersza. Nowa linia pozostaje w buforze po wyodrębnieniu namedo następnej operacji we / wy, w której jest odrzucana lub zużyta. Kiedy przepływ kontroli osiągnie std::getline(), nowa linia zostanie odrzucona, ale dane wejściowe natychmiast się zatrzymają. Powodem tego jest to, że domyślna funkcjonalność tej funkcji narzuca, że ​​powinna (próbuje odczytać linię i zatrzymuje się, gdy znajdzie nową linię).

Ponieważ ten wiodący znak nowej linii ogranicza oczekiwaną funkcjonalność twojego programu, wynika z tego, że musi on zostać w jakiś sposób pominięty i zignorowany. Jedną z opcji jest wezwanie std::cin.ignore()po pierwszej ekstrakcji. Odrzuci następny dostępny znak, dzięki czemu nowa linia nie będzie już przeszkadzać.

std::getline(std::cin.ignore(), state)

Szczegółowe wyjaśnienie:

To jest przeciążenie tego std::getline(), co nazwałeś:

template<class charT>
std::basic_istream<charT>& getline( std::basic_istream<charT>& input,
                                    std::basic_string<charT>& str )

Inne przeciążenie tej funkcji przyjmuje ogranicznik typu charT. Znak ogranicznika to znak, który reprezentuje granicę między sekwencjami danych wejściowych. To szczególne przeciążenie input.widen('\n')domyślnie ustawia ogranicznik na znak nowego wiersza, ponieważ nie został on podany.

Oto kilka warunków, które powodują std::getline()zakończenie wejścia:

  • Jeśli strumień wyodrębnił maksymalną liczbę znaków, które std::basic_string<charT>można przechowywać
  • Jeśli znaleziono znak końca pliku (EOF)
  • Jeśli separator został znaleziony

Trzeci warunek to ten, z którym mamy do czynienia. Twój wkład w statejest reprezentowany w następujący sposób:

"John\nNew Hampshire"
     ^
     |
 next_pointer

gdzie next_pointerjest następny znak do przeanalizowania. Ponieważ znak przechowywany na następnej pozycji w sekwencji wejściowej jest ogranicznikiem, std::getline()po cichu odrzuci ten znak, zwiększy next_pointerdo następnego dostępnego znaku i zatrzyma wprowadzanie. Oznacza to, że reszta znaków, które podałeś, nadal pozostaje w buforze do następnej operacji we / wy. Zauważysz, że jeśli wykonasz kolejny odczyt z wiersza do state, twoje wyodrębnienie da prawidłowy wynik jako ostatnie wywołanie std::getline()odrzucenia separatora.


Być może zauważyłeś, że zwykle nie napotykasz tego problemu podczas wyodrębniania za pomocą sformatowanego operatora wejściowego ( operator>>()). Dzieje się tak, ponieważ strumienie wejściowe używają białych znaków jako separatorów danych wejściowych i mają domyślnie włączony manipulator std::skipws1 . Strumienie będą odrzucać wiodące białe znaki ze strumienia, gdy zaczną wykonywać sformatowane dane wejściowe. 2

W przeciwieństwie do sformatowanych operatorów wejściowych, std::getline()jest to niesformatowana funkcja wejściowa. Wszystkie niesformatowane funkcje wejściowe mają nieco wspólny kod:

typename std::basic_istream<charT>::sentry ok(istream_object, true);

Powyższy obiekt to wartownik, którego instancja jest tworzona we wszystkich sformatowanych / niesformatowanych funkcjach we / wy w standardowej implementacji C ++. Obiekty Sentry są używane do przygotowania strumienia do wejścia / wyjścia i określenia, czy jest w stanie awarii. Przekonasz się tylko, że w niesformatowanych funkcjach wejściowych drugim argumentem konstruktora wartownika jest true. Ten argument oznacza, że początkowe białe znaki nie zostaną odrzucone z początku sekwencji wejściowej. Oto odpowiedni cytat ze standardu [§27.7.2.1.3 / 2]:

 explicit sentry(basic_istream<charT, traits>& is, bool noskipws = false);

[...] Jeśli noskipwswynosi zero i is.flags() & ios_base::skipwsjest niezerowe, funkcja wyodrębnia i odrzuca każdy znak, o ile następny dostępny znak wejściowy cjest znakiem odstępu. […]

Ponieważ powyższy warunek jest fałszywy, obiekt wartownika nie odrzuci białych znaków. Powód noskipwsjest ustawiony trueprzez tę funkcję, ponieważ celem std::getline()jest wczytanie surowych, niesformatowanych znaków do std::basic_string<charT>obiektu.


Rozwiązanie:

Nie ma sposobu, aby powstrzymać to zachowanie std::getline(). Musisz samodzielnie odrzucić nową linię przed std::getline()uruchomieniem (ale zrób to po sformatowanym rozpakowaniu). Można to zrobić za pomocą, ignore()aby odrzucić resztę danych wejściowych, dopóki nie osiągniemy nowego nowego wiersza:

if (std::cin >> name &&
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n') &&
    std::getline(std::cin, state))
{ ... }

Musisz dołączyć <limits>do użycia std::numeric_limits. std::basic_istream<...>::ignore()jest funkcją, która odrzuca określoną liczbę znaków, dopóki nie znajdzie separatora lub nie osiągnie końca strumienia ( ignore()również odrzuca ogranicznik, jeśli go znajdzie). max()Zwraca największą ilość znaków, że strumień może zaakceptować.

Innym sposobem na odrzucenie białych znaków jest użycie std::wsfunkcji, która jest manipulatorem zaprojektowanym do wyodrębniania i odrzucania wiodących białych znaków z początku strumienia wejściowego:

if (std::cin >> name && std::getline(std::cin >> std::ws, state))
{ ... }

Co za różnica?

Różnica polega na tym, że ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 bezkrytycznie odrzuca znaki, dopóki nie odrzuci countznaków, nie znajdzie separatora (określonego przez drugi argument delim) lub nie dotrze do końca strumienia. std::wssłuży tylko do usuwania białych znaków z początku strumienia.

Jeśli mieszasz formatowane wejście z niesformatowanym wejściem i musisz odrzucić pozostałe białe znaki, użyj std::ws. W przeciwnym razie, jeśli chcesz usunąć nieprawidłowe dane wejściowe, niezależnie od tego, co to jest, użyj ignore(). W naszym przykładzie musimy jedynie jasnego spacji ponieważ strumień spożywane swoją wejściowych "John"do namezmiennej. Został tylko znak nowej linii.


1: std::skipwsto manipulator, który mówi strumieniowi wejściowemu, aby odrzucał wiodące białe znaki podczas wykonywania sformatowanych danych wejściowych. Można to wyłączyć za pomocą std::noskipwsmanipulatora.

2: Strumienie wejściowe domyślnie uznają pewne znaki za białe spacje, takie jak spacja, znak nowego wiersza, wysuw strony, powrót karetki itp.

3: To jest podpis std::basic_istream<...>::ignore(). Możesz wywołać go bez argumentów, aby odrzucić pojedynczy znak ze strumienia, jeden argument, aby odrzucić określoną liczbę znaków lub dwa argumenty, aby odrzucić countznaki lub do momentu, gdy osiągnie delim, w zależności od tego, który z nich nastąpi wcześniej. Zwykle używasz std::numeric_limits<std::streamsize>::max()jako wartości, countjeśli nie wiesz, ile znaków znajduje się przed ogranicznikiem, ale i tak chcesz je odrzucić.

0x499602D2
źródło
1
Dlaczego nie po prostu if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
@FredLarson Słuszna uwaga. Chociaż nie zadziała, jeśli pierwsze wyodrębnienie będzie liczbą całkowitą lub czymś, co nie jest ciągiem.
0x499602D2
Oczywiście tak nie jest w tym przypadku i nie ma sensu robić tego samego na dwa różne sposoby. W przypadku liczby całkowitej możesz przekształcić wiersz w łańcuch, a następnie użyć go std::stoi(), ale wtedy nie jest tak jasne, że ma to przewagę. Ale wolę po prostu używać std::getline()do wprowadzania zorientowanego liniowo, a następnie zajmować się analizowaniem linii w jakikolwiek sensowny sposób. Myślę, że jest mniej podatny na błędy.
Fred Larson,
@FredLarson Zgoda. Może dodam to, jeśli będę miał czas.
0x499602D2
1
@Albin Powodem, dla którego możesz chcieć użyć, std::getline()jest to, że chcesz przechwycić wszystkie znaki do podanego separatora i wprowadzić je do ciągu, domyślnie jest to nowa linia. Jeśli ta Xliczba ciągów to tylko pojedyncze słowa / tokeny, to zadanie to można łatwo wykonać za pomocą >>. W przeciwnym razie wprowadzisz pierwszą liczbę do liczby całkowitej za pomocą >>, wywołasz cin.ignore()następny wiersz, a następnie uruchomisz pętlę, w której używasz getline().
0x499602D2
11

Wszystko będzie dobrze, jeśli zmienisz kod początkowy w następujący sposób:

if ((cin >> name).get() && std::getline(cin, state))
Boris
źródło
3
Dziękuję Ci. To również zadziała, ponieważ get()zużywa następną postać. Jest też to, (std::cin >> name).ignore()co zasugerowałem wcześniej w mojej odpowiedzi.
0x499602D2
"..prac, ponieważ get () ..." Tak, dokładnie. Przepraszamy za udzielenie odpowiedzi bez szczegółów.
Boris
4
Dlaczego nie po prostu if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
0

Dzieje się tak, ponieważ niejawny znak nowego wiersza, znany również jako znak nowej linii, \njest dołączany do wszystkich danych wejściowych użytkownika z terminala, ponieważ mówi strumieniowi, aby rozpoczął nowy wiersz. Możesz to bezpiecznie uwzględnić, używając std::getlinepodczas sprawdzania wielu wierszy danych wejściowych użytkownika. Domyślne zachowanie std::getlineodczyta wszystko do znaku nowego wiersza \nz obiektu strumienia wejściowego włącznie, co std::cinw tym przypadku ma miejsce.

#include <iostream>
#include <string>

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

    if (std::getline(std::cin, name) && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
    return 0;
}
Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in New Hampshire"
Justin Randall
źródło