เหตุใด std :: getline () จึงข้ามอินพุตหลังจากการแยกรูปแบบ


105

ฉันมีโค้ดต่อไปนี้ที่แจ้งให้ผู้ใช้ทราบชื่อและสถานะ:

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

สิ่งที่ฉันพบคือชื่อถูกแยกออกมาสำเร็จแล้ว แต่ไม่ใช่สถานะ นี่คืออินพุตและเอาต์พุตผลลัพธ์:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

เหตุใดชื่อของรัฐจึงถูกละไว้ในเอาต์พุต ฉันได้ป้อนข้อมูลที่ถูกต้องแล้ว แต่โค้ดก็ไม่สนใจมัน ทำไมสิ่งนี้ถึงเกิดขึ้น?


ฉันเชื่อว่าstd::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)ควรทำงานตามที่คาดไว้ (นอกเหนือจากคำตอบด้านล่าง).
jww

คำตอบ:


122

ทำไมสิ่งนี้ถึงเกิดขึ้น?

สิ่งนี้มีส่วนเกี่ยวข้องเพียงเล็กน้อยกับอินพุตที่คุณให้ไว้ แต่เป็นการstd::getline()จัดแสดงพฤติกรรมเริ่มต้น เมื่อคุณป้อนข้อมูลสำหรับชื่อ ( std::cin >> name) คุณไม่เพียง แต่ส่งอักขระต่อไปนี้เท่านั้น แต่ยังมีการเพิ่มบรรทัดใหม่โดยนัยในสตรีมด้วย:

"John\n"

บรรทัดใหม่จะต่อท้ายข้อมูลที่คุณป้อนเสมอเมื่อคุณเลือกEnterหรือReturnเมื่อส่งจากเทอร์มินัล นอกจากนี้ยังใช้ในไฟล์เพื่อเลื่อนไปยังบรรทัดถัดไป การขึ้นบรรทัดใหม่จะถูกทิ้งไว้ในบัฟเฟอร์หลังจากการแยกข้อมูลไปnameจนถึงการดำเนินการ I / O ถัดไปซึ่งอาจถูกทิ้งหรือใช้ไป เมื่อกระแสการควบคุมมาถึงstd::getline()บรรทัดใหม่จะถูกละทิ้ง แต่อินพุตจะหยุดทันที สาเหตุที่เกิดขึ้นเนื่องจากฟังก์ชันเริ่มต้นของฟังก์ชันนี้กำหนดว่าควร (พยายามอ่านบรรทัดและหยุดเมื่อพบขึ้นบรรทัดใหม่)

เนื่องจากบรรทัดใหม่นำหน้านี้ขัดขวางฟังก์ชันการทำงานที่คาดไว้ของโปรแกรมของคุณจึงต้องข้ามไปโดยไม่สนใจอย่างใด ทางเลือกหนึ่งคือการโทรstd::cin.ignore()หลังจากการสกัดครั้งแรก มันจะทิ้งอักขระที่มีอยู่ถัดไปเพื่อไม่ให้ขึ้นบรรทัดใหม่อีกต่อไป

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

คำอธิบายเชิงลึก:

นี่คือการโอเวอร์โหลดstd::getline()ที่คุณเรียกว่า:

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

charTเกินพิกัดของฟังก์ชั่นนี้ก็จะใช้เวลาของตัวคั่นประเภท อักขระตัวคั่นคืออักขระที่แสดงขอบเขตระหว่างลำดับของอินพุต การโอเวอร์โหลดโดยเฉพาะนี้ตั้งค่าตัวคั่นเป็นอักขระขึ้นบรรทัดใหม่input.widen('\n')ตามค่าเริ่มต้นเนื่องจากไม่มีการระบุ

ต่อไปนี้เป็นเงื่อนไขบางประการที่จะstd::getline()ยุติการป้อนข้อมูล:

  • หากสตรีมดึงจำนวนอักขระสูงสุด a std::basic_string<charT>สามารถเก็บได้
  • หากพบอักขระ end-of-file (EOF)
  • หากพบตัวคั่น

เงื่อนไขที่สามคือสิ่งที่เรากำลังเผชิญอยู่ ข้อมูลที่คุณป้อนเข้าstateจะแสดงด้วยเหตุนี้:

"John\nNew Hampshire"
     ^
     |
 next_pointer

ที่next_pointerเป็นตัวละครต่อไปที่จะแยกวิเคราะห์ เนื่องจากอักขระที่จัดเก็บในตำแหน่งถัดไปในลำดับการป้อนข้อมูลเป็นตัวคั่นจึงstd::getline()จะละทิ้งอักขระนั้นอย่างเงียบ ๆ เพิ่มnext_pointerไปยังอักขระที่มีอยู่ถัดไปและหยุดการป้อนข้อมูล ซึ่งหมายความว่าอักขระที่เหลือที่คุณระบุยังคงอยู่ในบัฟเฟอร์สำหรับการดำเนินการ I / O ถัดไป คุณจะสังเกตเห็นว่าหากคุณทำการอ่านอีกครั้งจากบรรทัดเข้าไปการstateแยกของคุณจะให้ผลลัพธ์ที่ถูกต้องเป็นคำเรียกสุดท้ายที่จะstd::getline()ละทิ้งตัวคั่น


คุณอาจสังเกตเห็นว่าโดยทั่วไปแล้วคุณจะไม่ประสบปัญหานี้เมื่อแยกข้อมูลด้วยตัวดำเนินการป้อนข้อมูลที่จัดรูปแบบ ( operator>>()) เนื่องจากอินพุตสตรีมใช้ช่องว่างเป็นตัวคั่นสำหรับอินพุตและมีstd::skipws1 ตัวปรับแต่งตามค่าเริ่มต้น สตรีมจะละทิ้งช่องว่างชั้นนำจากสตรีมเมื่อเริ่มดำเนินการป้อนข้อมูลที่จัดรูปแบบ 2

ซึ่งแตกต่างจากตัวดำเนินการป้อนข้อมูลที่จัดรูปแบบstd::getline()คือฟังก์ชันอินพุตที่ไม่ได้จัดรูปแบบ และฟังก์ชันการป้อนข้อมูลที่ไม่ได้จัดรูปแบบทั้งหมดมีรหัสต่อไปนี้เหมือนกัน:

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

ด้านบนเป็นวัตถุยามซึ่งสร้างอินสแตนซ์ในฟังก์ชัน I / O ที่จัดรูปแบบ / ไม่ได้จัดรูปแบบทั้งหมดในการใช้งาน C ++ มาตรฐาน อ็อบเจ็กต์ Sentry ใช้สำหรับเตรียมสตรีมสำหรับ I / O และพิจารณาว่าอยู่ในสถานะล้มเหลวหรือไม่ คุณจะพบว่าในฟอร์แมตtrueฟังก์ชั่นการป้อนข้อมูลอาร์กิวเมนต์ที่สองที่จะคอนสตรัคยามคือ อาร์กิวเมนต์นั้นหมายความว่าช่องว่างนำหน้าจะไม่ถูกละทิ้งจากจุดเริ่มต้นของลำดับการป้อนข้อมูล นี่คือคำพูดที่เกี่ยวข้องจากมาตรฐาน [§27.7.2.1.3 / 2]:

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

[... ] ถ้าnoskipwsเป็นศูนย์และis.flags() & ios_base::skipwsไม่ใช่ศูนย์ฟังก์ชันจะแยกและละทิ้งอักขระแต่ละตัวตราบเท่าที่อักขระอินพุตถัดไปที่มีcอยู่เป็นอักขระช่องว่าง [... ]

เนื่องจากเงื่อนไขข้างต้นเป็นเท็จวัตถุยามจะไม่ทิ้งช่องว่าง เหตุผลที่noskipwsกำหนดtrueโดยฟังก์ชันนี้เนื่องจากประเด็นstd::getline()คือการอ่านอักขระดิบที่ไม่ได้จัดรูปแบบลงในstd::basic_string<charT>วัตถุ


การแก้ไขปัญหา:

std::getline()ไม่มีทางที่จะหยุดพฤติกรรมนี้ไม่ได้ สิ่งที่คุณต้องทำคือทิ้งบรรทัดใหม่ด้วยตัวคุณเองก่อนที่จะstd::getline()รัน (แต่ให้ทำหลังจากการแยกรูปแบบ) สามารถทำได้โดยใช้ignore()เพื่อทิ้งอินพุตที่เหลือจนกว่าเราจะถึงบรรทัดใหม่:

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

คุณจะต้องรวมถึงการใช้<limits> เป็นฟังก์ชันที่ละทิ้งอักขระตามจำนวนที่ระบุจนกว่าจะพบตัวคั่นหรือถึงจุดสิ้นสุดของสตรีม ( และจะทิ้งตัวคั่นด้วยหากพบ) ฟังก์ชันส่งกลับจำนวนมากที่สุดของตัวละครที่กระแสสามารถยอมรับstd::numeric_limitsstd::basic_istream<...>::ignore()ignore()max()

อีกวิธีหนึ่งในการละทิ้งช่องว่างคือการใช้std::wsฟังก์ชันซึ่งเป็นเครื่องมือจัดการที่ออกแบบมาเพื่อแยกและทิ้งช่องว่างชั้นนำตั้งแต่จุดเริ่มต้นของอินพุตสตรีม:

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

อะไรคือความแตกต่าง?

ความแตกต่างคือignore(std::streamsize count = 1, int_type delim = Traits::eof())3ตัวละครจะละทิ้งอักขระโดยไม่เลือกปฏิบัติจนกว่าจะละทิ้งcountอักขระค้นหาตัวคั่น (ระบุโดยอาร์กิวเมนต์ที่สองdelim) หรือถึงจุดสิ้นสุดของสตรีม std::wsใช้สำหรับการละทิ้งอักขระเว้นวรรคจากจุดเริ่มต้นของสตรีมเท่านั้น

std::wsหากคุณกำลังผสมป้อนข้อมูลในรูปแบบที่มีการป้อนข้อมูลที่ยังไม่ฟอร์แมตและคุณจำเป็นต้องทิ้งช่องว่างที่เหลือการใช้งาน ignore()มิฉะนั้นถ้าคุณจำเป็นต้องล้างออกป้อนไม่ถูกต้องโดยไม่คำนึงถึงว่ามันคืออะไรการใช้งาน ในตัวอย่างของเราเราจำเป็นต้องล้างช่องว่างเนื่องจากสตรีมใช้อินพุตของคุณ"John"สำหรับnameตัวแปร สิ่งที่เหลืออยู่คือตัวละครขึ้นบรรทัดใหม่


1: std::skipwsเป็นตัวควบคุมที่บอกให้สตรีมอินพุตทิ้งช่องว่างชั้นนำเมื่อดำเนินการป้อนข้อมูลที่จัดรูปแบบ สิ่งนี้สามารถปิดได้ด้วยstd::noskipwsหุ่นยนต์

2: อินพุตสตรีมถือว่าอักขระบางตัวเป็นช่องว่างโดยค่าเริ่มต้นเช่นอักขระช่องว่างอักขระขึ้นบรรทัดใหม่ฟีดแบบฟอร์มการส่งคืนค่าขนส่งเป็นต้น

3: นี่คือลายเซ็นของstd::basic_istream<...>::ignore(). คุณสามารถเรียกใช้โดยไม่มีอาร์กิวเมนต์เพื่อละทิ้งอักขระเดี่ยวจากสตรีมอาร์กิวเมนต์หนึ่งเพื่อทิ้งอักขระจำนวนหนึ่งหรือสองอาร์กิวเมนต์เพื่อทิ้งcountอักขระหรือจนกว่าจะถึงdelimแล้วแต่ว่าข้อใดจะเกิดขึ้นก่อน โดยปกติคุณจะใช้std::numeric_limits<std::streamsize>::max()เป็นค่าของcountถ้าคุณไม่ทราบว่ามีอักขระจำนวนเท่าใดก่อนตัวคั่น แต่คุณก็ยังต้องการทิ้งอยู่ดี


1
ทำไมไม่เพียงif (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

@FredLarson จุดดี. แม้ว่าจะใช้ไม่ได้หากการแยกครั้งแรกเป็นจำนวนเต็มหรืออะไรก็ตามที่ไม่ใช่สตริง
0x499602D2

แน่นอนว่านั่นไม่ใช่กรณีนี้และไม่มีเหตุผลในการทำสิ่งเดียวกันสองวิธีที่แตกต่างกัน สำหรับจำนวนเต็มคุณจะได้เส้นเป็นสตริงแล้วใช้std::stoi()แต่มันก็ไม่ชัดเจนนักมีข้อดี แต่ฉันมักจะชอบใช้std::getline()สำหรับการป้อนข้อมูลเชิงเส้นแล้วจัดการกับการแยกวิเคราะห์บรรทัดในลักษณะใดก็ตามที่เหมาะสม ฉันคิดว่ามันมีโอกาสเกิดข้อผิดพลาดน้อยกว่า
Fred Larson

@FredLarson เห็นด้วย บางทีฉันจะเพิ่มมันเข้าไปถ้าฉันมีเวลา
0x499602D2

1
@Albin เหตุผลที่คุณอาจต้องการใช้std::getline()คือถ้าคุณต้องการจับอักขระทั้งหมดจนถึงตัวคั่นที่กำหนดและป้อนลงในสตริงโดยค่าเริ่มต้นคือขึ้นบรรทัดใหม่ หากผู้ที่Xจำนวนของสตริงเพียงคำเดียว / >>สัญญาณแล้วงานนี้สามารถทำได้อย่างง่ายดายด้วย มิฉะนั้นคุณจะใส่หมายเลขแรกที่เข้าไปในจำนวนเต็มกับ>>โทรในบรรทัดถัดไปและเรียกใช้ห่วงที่คุณใช้cin.ignore() getline()
0x499602D2

11

ทุกอย่างจะเรียบร้อยหากคุณเปลี่ยนรหัสเริ่มต้นด้วยวิธีต่อไปนี้:

if ((cin >> name).get() && std::getline(cin, state))

3
ขอบคุณ. สิ่งนี้จะได้ผลเช่นกันเนื่องจากget()ใช้อักขระถัดไป นอกจากนี้ยังมี(std::cin >> name).ignore()สิ่งที่ฉันแนะนำไว้ก่อนหน้านี้ในคำตอบของฉัน
0x499602D2

".. ทำงานเพราะ get () ... " ใช่เป๊ะ ขออภัยที่ให้คำตอบโดยไม่มีรายละเอียด
Boris

4
ทำไมไม่เพียงif (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

0

สิ่งนี้เกิดขึ้นเนื่องจากฟีดบรรทัดโดยนัยหรือที่เรียกว่าอักขระขึ้นบรรทัด\nต่อท้ายอินพุตของผู้ใช้ทั้งหมดจากเทอร์มินัลเนื่องจากกำลังบอกสตรีมให้เริ่มบรรทัดใหม่ คุณสามารถระบุสิ่งนี้ได้อย่างปลอดภัยโดยใช้std::getlineเมื่อตรวจสอบการป้อนข้อมูลของผู้ใช้หลายบรรทัด พฤติกรรมเริ่มต้นของstd::getlineจะอ่านทุกอย่างจนถึงและรวมถึงอักขระขึ้นบรรทัดใหม่\nจากออบเจ็กต์สตรีมอินพุตซึ่งอยู่std::cinในกรณีนี้

#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"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.