Mengapa std :: getline () melewatkan masukan setelah ekstraksi diformat?

105

Saya memiliki potongan kode berikut yang meminta nama dan status pengguna:

#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;
    }
}

Apa yang saya temukan adalah bahwa nama tersebut telah berhasil diekstraksi, tetapi bukan statusnya. Berikut adalah masukan dan keluaran yang dihasilkan:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Mengapa nama negara bagian dihilangkan dari keluaran? Saya telah memberikan masukan yang tepat, tetapi kode tersebut mengabaikannya. Mengapa ini terjadi?

0x499602D2
sumber
Saya yakin std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)juga harus bekerja seperti yang diharapkan. (Selain jawaban di bawah).
jww

Jawaban:

122

Mengapa ini terjadi?

Ini tidak ada hubungannya dengan masukan yang Anda berikan sendiri, melainkan dengan tampilan perilaku default std::getline(). Ketika Anda memberikan masukan Anda untuk name ( std::cin >> name), Anda tidak hanya mengirimkan karakter berikut, tetapi juga baris baru implisit telah ditambahkan ke aliran:

"John\n"

Sebuah baris baru selalu ditambahkan ke masukan Anda saat Anda memilih Enteratau Returnsaat mengirimkan dari terminal. Ini juga digunakan dalam file untuk berpindah ke baris berikutnya. Baris baru dibiarkan di buffer setelah ekstraksi namehingga operasi I / O berikutnya di mana ia dibuang atau digunakan. Saat aliran kontrol std::getline()tercapai, baris baru akan dibuang, tetapi masukan akan segera berhenti. Alasan hal ini terjadi adalah karena fungsionalitas default dari fungsi ini menentukan bahwa fungsi ini harus (mencoba membaca baris dan berhenti ketika menemukan baris baru).

Karena baris baru terkemuka ini menghambat fungsionalitas yang diharapkan dari program Anda, maka itu harus dilewati dengan cara apa pun. Salah satu opsi adalah memanggil std::cin.ignore()setelah ekstraksi pertama. Ini akan membuang karakter yang tersedia berikutnya sehingga baris baru tidak lagi menghalangi.

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

Penjelasan Mendalam:

Ini adalah kelebihan std::getline()yang Anda panggil:

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

Kelebihan lain dari fungsi ini membutuhkan pembatas tipe charT. Karakter pembatas adalah karakter yang merepresentasikan batas antara urutan input. Kelebihan khusus ini menyetel pembatas ke karakter baris baru input.widen('\n')secara default karena tidak ada yang diberikan.

Sekarang, ini adalah beberapa kondisi dimana std::getline()input akan diakhiri:

  • Jika aliran telah mengekstraksi jumlah maksimum karakter yang std::basic_string<charT>dapat ditampung
  • Jika karakter end-of-file (EOF) telah ditemukan
  • Jika pembatas sudah ditemukan

Kondisi ketiga adalah yang kita hadapi. Masukan Anda ke dalam statedirepresentasikan sebagai berikut:

"John\nNew Hampshire"
     ^
     |
 next_pointer

dimana next_pointerkarakter selanjutnya yang akan diurai. Karena karakter yang disimpan di posisi berikutnya dalam urutan masukan adalah pembatas, std::getline()akan secara diam-diam membuang karakter tersebut, menaikkan next_pointerke karakter berikutnya yang tersedia, dan menghentikan masukan. Ini berarti karakter lainnya yang telah Anda berikan masih tetap berada di buffer untuk operasi I / O berikutnya. Anda akan melihat bahwa jika Anda melakukan pembacaan lagi dari baris ke state, ekstraksi Anda akan menghasilkan hasil yang benar sebagai panggilan terakhir untuk std::getline()membuang pembatas.


Anda mungkin telah memperhatikan bahwa Anda biasanya tidak mengalami masalah ini saat mengekstrak dengan operator input yang diformat ( operator>>()). Ini karena input stream menggunakan spasi sebagai pembatas untuk input dan std::skipws1 manipulator telah disetel secara default. Aliran akan membuang spasi kosong utama dari aliran saat mulai melakukan masukan yang diformat. 2

Berbeda dengan operator input yang diformat, std::getline()ini adalah fungsi input yang tidak diformat . Dan semua fungsi masukan yang tidak diformat memiliki kode berikut yang agak sama:

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

Di atas adalah objek sentry yang dipakai di semua fungsi I / O yang diformat / tidak diformat dalam implementasi C ++ standar. Objek penjaga digunakan untuk menyiapkan aliran untuk I / O dan menentukan apakah dalam keadaan gagal atau tidak. Anda hanya akan menemukan bahwa dalam fungsi masukan yang tidak diformat , argumen kedua untuk konstruktor penjaga adalah true. Argumen tersebut berarti bahwa spasi putih utama tidak akan dibuang dari awal urutan masukan. Berikut adalah kutipan relevan dari Standar [§27.7.2.1.3 / 2]:

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

[...] Jika noskipwsnol dan is.flags() & ios_base::skipwsbukan nol, fungsi mengekstrak dan membuang setiap karakter selama karakter input berikutnya yang tersedia cadalah karakter spasi. [...]

Karena kondisi di atas salah, objek penjaga tidak akan membuang spasi. Alasan noskipwsditetapkan trueoleh fungsi ini adalah karena intinya std::getline()adalah untuk membaca karakter mentah yang belum diformat ke dalam std::basic_string<charT>objek.


Solusinya:

Tidak ada cara untuk menghentikan perilaku ini std::getline(). Apa yang harus Anda lakukan adalah membuang sendiri baris baru tersebut sebelum std::getline()dijalankan (tetapi lakukan setelah ekstraksi yang diformat). Ini dapat dilakukan dengan menggunakan ignore()untuk membuang sisa masukan sampai kita mencapai baris baru yang segar:

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

Anda harus menyertakan <limits>untuk menggunakan std::numeric_limits. std::basic_istream<...>::ignore()adalah fungsi yang membuang sejumlah karakter tertentu hingga menemukan pembatas atau mencapai akhir aliran ( ignore()juga membuang pembatas jika menemukannya). The max()mengembalikan fungsi jumlah terbesar dari karakter yang sungai dapat menerima.

Cara lain untuk membuang spasi putih adalah dengan menggunakan std::wsfungsi manipulator yang dirancang untuk mengekstrak dan membuang spasi putih utama dari awal aliran input:

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

Apa bedanya?

Perbedaannya adalah ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 tanpa pandang bulu membuang karakter sampai karakter tersebut dibuang count, menemukan pembatas (ditentukan oleh argumen kedua delim) atau mencapai akhir aliran. std::wshanya digunakan untuk membuang karakter spasi dari awal aliran.

Jika Anda mencampur masukan yang diformat dengan masukan yang tidak diformat dan Anda perlu membuang sisa spasi, gunakan std::ws. Jika tidak, jika Anda perlu menghapus masukan yang tidak valid apa pun itu, gunakan ignore(). Dalam contoh kita, kita hanya perlu mengosongkan spasi karena aliran menggunakan input Anda "John"untuk namevariabel. Yang tersisa hanyalah karakter baris baru.


1: std::skipwsadalah manipulator yang memberi tahu aliran input untuk membuang spasi kosong utama saat melakukan input yang diformat. Ini bisa dimatikan dengan std::noskipwsmanipulator.

2: Arus input menganggap karakter tertentu sebagai spasi secara default, seperti karakter spasi, karakter baris baru, umpan formulir, carriage return, dll.

3: Ini adalah tanda tangan dari std::basic_istream<...>::ignore(). Anda dapat memanggilnya dengan nol argumen untuk membuang satu karakter dari aliran, satu argumen untuk membuang sejumlah karakter, atau dua argumen untuk membuang countkarakter atau sampai mencapai delim, mana saja yang lebih dulu. Anda biasanya menggunakan std::numeric_limits<std::streamsize>::max()sebagai nilai countjika Anda tidak tahu berapa banyak karakter sebelum pembatas, tetapi Anda tetap ingin membuangnya.

0x499602D2
sumber
1
Mengapa tidak sederhana if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
@RedonArtinya bagus. Meskipun itu tidak akan berfungsi jika ekstraksi pertama adalah bilangan bulat atau apa pun yang bukan string.
0x499602D2
Tentu saja, bukan itu masalahnya di sini dan tidak ada gunanya melakukan hal yang sama dengan dua cara yang berbeda. Untuk integer Anda bisa mendapatkan garis menjadi string dan kemudian menggunakan std::stoi(), tapi tidak begitu jelas ada keuntungannya. Tapi saya cenderung lebih suka menggunakan std::getline()untuk input berorientasi garis dan kemudian berurusan dengan penguraian baris dengan cara apa pun yang masuk akal. Saya pikir itu tidak terlalu rentan terhadap kesalahan.
Fred Larson
@FredLarson Setuju. Mungkin saya akan menambahkannya jika saya punya waktu.
0x499602D2
1
@Albin Alasan Anda mungkin ingin menggunakan std::getline()adalah jika Anda ingin menangkap semua karakter hingga pembatas tertentu dan memasukkannya ke dalam string, secara default adalah baris baru. Jika Xjumlah string tersebut hanya satu kata / token maka pekerjaan ini dapat dengan mudah diselesaikan >>. Jika tidak, Anda akan memasukkan nomor pertama ke dalam bilangan bulat dengan >>, panggil cin.ignore()di baris berikutnya, dan kemudian jalankan loop di mana Anda menggunakan getline().
0x499602D2
11

Semuanya akan baik-baik saja jika Anda mengubah kode awal Anda dengan cara berikut:

if ((cin >> name).get() && std::getline(cin, state))
Boris
sumber
3
Terima kasih. Ini juga akan berfungsi karena get()mengkonsumsi karakter berikutnya. Ada juga (std::cin >> name).ignore()yang saya sarankan sebelumnya dalam jawaban saya.
0x499602D2
"..work karena get () ..." Ya, tepatnya. Maaf telah memberikan jawaban tanpa detail.
Boris
4
Mengapa tidak sederhana if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
0

Ini terjadi karena umpan baris implisit yang juga dikenal sebagai karakter baris baru \nditambahkan ke semua masukan pengguna dari terminal karena memberi tahu aliran untuk memulai baris baru. Anda dapat dengan aman menjelaskan hal ini dengan menggunakan std::getlinesaat memeriksa beberapa baris masukan pengguna. Perilaku default std::getlineakan membaca semuanya hingga dan termasuk karakter baris baru \ndari objek input stream yang std::cindalam kasus ini.

#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
sumber