Tại sao std :: getline () bỏ qua đầu vào sau khi trích xuất được định dạng?


105

Tôi có đoạn mã sau nhắc người dùng nhập tên và trạng thái của họ:

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

Những gì tôi tìm thấy là tên đã được trích xuất thành công, nhưng không phải trạng thái. Đây là đầu vào và đầu ra kết quả:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Tại sao tên của trạng thái bị bỏ qua khỏi đầu ra? Tôi đã cung cấp đầu vào thích hợp, nhưng mã bằng cách nào đó bỏ qua nó. Lý do tại sao điều này xảy ra?


Tôi tin rằng std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)cũng nên hoạt động như mong đợi. (Ngoài các câu trả lời bên dưới).
jww

Câu trả lời:


122

Lý do tại sao điều này xảy ra?

Điều này ít liên quan đến thông tin đầu vào do chính bạn cung cấp mà là do các std::getline()biểu hiện hành vi mặc định . Khi bạn cung cấp thông tin đầu vào cho tên ( std::cin >> name), bạn không chỉ gửi các ký tự sau mà còn thêm một dòng mới ngầm vào luồng:

"John\n"

Một dòng mới luôn được thêm vào đầu vào của bạn khi bạn chọn Enterhoặc Returnkhi gửi từ một thiết bị đầu cuối. Nó cũng được sử dụng trong các tệp để chuyển sang dòng tiếp theo. Dòng mới được để lại trong bộ đệm sau khi trích xuất namecho đến khi hoạt động I / O tiếp theo, nơi nó bị loại bỏ hoặc tiêu thụ. Khi đạt đến luồng kiểm soát std::getline(), dòng mới sẽ bị loại bỏ, nhưng đầu vào sẽ dừng ngay lập tức. Lý do điều này xảy ra là vì chức năng mặc định của hàm này ra lệnh rằng nó phải như vậy (nó cố gắng đọc một dòng và dừng lại khi tìm thấy một dòng mới).

Bởi vì dòng mới hàng đầu này hạn chế chức năng mong đợi của chương trình của bạn, nó sau đó phải được bỏ qua bằng cách nào đó đã bỏ qua của chúng tôi. Một tùy chọn là gọi std::cin.ignore()sau lần trích xuất đầu tiên. Nó sẽ loại bỏ ký tự có sẵn tiếp theo để dòng mới không còn cản trở.

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

Giải thích chuyên sâu:

Đây là quá tải std::getline()mà bạn đã gọi:

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

Quá tải khác của chức năng này có một dấu phân cách của loại charT. Ký tự phân tách là ký tự thể hiện ranh giới giữa các chuỗi dữ liệu đầu vào. Quá tải cụ thể này đặt dấu phân cách thành ký tự dòng mới input.widen('\n')theo mặc định vì một ký tự không được cung cấp.

Bây giờ, đây là một số điều kiện mà theo đó std::getline()kết thúc đầu vào:

  • Nếu luồng đã trích xuất số lượng ký tự tối đa std::basic_string<charT>có thể chứa
  • Nếu ký tự cuối tệp (EOF) đã được tìm thấy
  • Nếu dấu phân cách đã được tìm thấy

Điều kiện thứ ba là điều kiện chúng tôi đang giải quyết. Thông tin đầu vào của bạn stateđược thể hiện như sau:

"John\nNew Hampshire"
     ^
     |
 next_pointer

đâu next_pointerlà ký tự tiếp theo được phân tích cú pháp. Vì ký tự được lưu trữ ở vị trí tiếp theo trong chuỗi đầu vào là dấu phân cách, std::getline()sẽ lặng lẽ loại bỏ ký tự đó, tăng dần next_pointerđến ký tự có sẵn tiếp theo và dừng nhập. Điều này có nghĩa là phần còn lại của các ký tự mà bạn đã cung cấp vẫn còn trong bộ đệm cho thao tác I / O tiếp theo. Bạn sẽ nhận thấy rằng nếu bạn thực hiện một lần đọc khác từ dòng vào state, việc trích xuất của bạn sẽ mang lại kết quả chính xác là lần gọi cuối cùng để std::getline()loại bỏ dấu phân cách.


Bạn có thể nhận thấy rằng bạn thường không gặp phải sự cố này khi giải nén bằng toán tử đầu vào được định dạng ( operator>>()). Điều này là do các luồng đầu vào sử dụng khoảng trắng làm dấu phân cách cho đầu vào và có std::skipws1 trình thao tác được bật theo mặc định. Luồng sẽ loại bỏ khoảng trắng hàng đầu khỏi luồng khi bắt đầu thực hiện đầu vào được định dạng. 2

Không giống như các toán tử đầu vào được định dạng, std::getline()là một hàm đầu vào không được định dạng . Và tất cả các hàm đầu vào chưa được định dạng đều có mã sau đây phần nào giống nhau:

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

Ở trên là một đối tượng sentry được khởi tạo trong tất cả các hàm I / O được định dạng / chưa được định dạng trong triển khai C ++ tiêu chuẩn. Các đối tượng Sentry được sử dụng để chuẩn bị luồng cho I / O và xác định xem nó có ở trạng thái thất bại hay không. Bạn sẽ chỉ thấy rằng trong các hàm đầu vào chưa được định dạng , đối số thứ hai của hàm tạo sentry là true. Đối số đó có nghĩa là khoảng trắng ở đầu sẽ không bị loại bỏ từ đầu chuỗi nhập. Đây là phần trích dẫn liên quan từ Tiêu chuẩn [§27.7.2.1.3 / 2]:

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

[...] Nếu noskipwsbằng 0 và is.flags() & ios_base::skipwskhác không, hàm sẽ trích xuất và loại bỏ từng ký tự miễn là ký tự đầu vào có sẵn tiếp theo clà ký tự khoảng trắng. [...]

Vì điều kiện trên là sai, đối tượng sentry sẽ không loại bỏ khoảng trắng. Lý do noskipwsđược đặt thành truebởi hàm này là vì mục đích std::getline()là đọc các ký tự thô, chưa được định dạng thành một std::basic_string<charT>đối tượng.


Giải pháp:

Không có cách nào để ngăn chặn hành vi này của std::getline(). Những gì bạn sẽ phải làm là tự loại bỏ dòng mới trước khi std::getline()chạy (nhưng làm điều đó sau khi trích xuất đã định dạng). Điều này có thể được thực hiện bằng cách sử dụng ignore()để loại bỏ phần còn lại của đầu vào cho đến khi chúng ta đến một dòng mới mới:

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

Bạn sẽ cần phải bao gồm <limits>để sử dụng std::numeric_limits. std::basic_istream<...>::ignore()là một hàm loại bỏ một lượng ký tự được chỉ định cho đến khi nó tìm thấy dấu phân cách hoặc đến cuối luồng ( ignore()cũng loại bỏ dấu phân cách nếu nó tìm thấy nó). Các max()hàm trả về số lượng lớn các ký tự mà một dòng suối có thể chấp nhận.

Một cách khác để loại bỏ khoảng trắng là sử dụng std::wshàm là một trình thao tác được thiết kế để trích xuất và loại bỏ khoảng trắng đầu từ đầu luồng đầu vào:

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

Có gì khác biệt?

Sự khác biệt là ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 ký tự loại bỏ bừa bãi cho đến khi nó loại bỏ các countký tự, tìm thấy dấu phân cách (được chỉ định bởi đối số thứ hai delim) hoặc chạm đến cuối luồng. std::wschỉ được sử dụng để loại bỏ các ký tự khoảng trắng từ đầu luồng.

Nếu bạn đang trộn đầu vào được định dạng với đầu vào chưa được định dạng và bạn cần loại bỏ khoảng trắng còn lại, hãy sử dụng std::ws. Ngược lại, nếu bạn cần xóa đầu vào không hợp lệ bất kể nó là gì, hãy sử dụng ignore(). Trong ví dụ của chúng tôi, chúng tôi chỉ cần xóa khoảng trắng vì luồng tiêu thụ đầu vào của bạn "John"cho namebiến. Tất cả những gì còn lại là ký tự dòng mới.


1: std::skipwslà trình điều khiển cho biết luồng đầu vào loại bỏ khoảng trắng đầu khi thực hiện đầu vào được định dạng. Điều này có thể được tắt với trình std::noskipwsđiều khiển.

2: Các luồng đầu vào coi các ký tự nhất định là khoảng trắng theo mặc định, chẳng hạn như ký tự khoảng trắng, ký tự dòng mới, nguồn cấp biểu mẫu, ký tự xuống dòng, v.v.

3: Đây là chữ ký của std::basic_istream<...>::ignore(). Bạn có thể gọi nó với không đối số để loại bỏ một ký tự duy nhất khỏi luồng, một đối số để loại bỏ một lượng ký tự nhất định hoặc hai đối số để loại bỏ countký tự hoặc cho đến khi đạt đến delim, tùy điều kiện nào đến trước. Bạn thường sử dụng std::numeric_limits<std::streamsize>::max()làm giá trị của countnếu bạn không biết có bao nhiêu ký tự trước dấu phân cách, nhưng bạn vẫn muốn loại bỏ chúng.


1
Tại sao không đơn giản if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

@FredLarson Điểm tốt. Mặc dù nó sẽ không hoạt động nếu lần trích xuất đầu tiên là một số nguyên hoặc bất kỳ thứ gì không phải là một chuỗi.
0x499602D2

Tất nhiên, đó không phải là trường hợp ở đây và không có ích gì khi làm cùng một việc theo hai cách khác nhau. Đối với một số nguyên, bạn có thể lấy dòng thành một chuỗi và sau đó sử dụng std::stoi(), nhưng không rõ ràng là có lợi thế. Nhưng tôi có xu hướng thích chỉ sử dụng std::getline()cho đầu vào hướng dòng và sau đó xử lý phân tích cú pháp dòng theo bất kỳ cách nào có ý nghĩa. Tôi nghĩ rằng nó ít bị lỗi hơn.
Fred Larson

@FredLarson Đồng ý. Có lẽ tôi sẽ thêm nó vào nếu tôi có thời gian.
0x499602D2

1
@Albin Lý do bạn có thể muốn sử dụng std::getline()là nếu bạn muốn nắm bắt tất cả các ký tự đến một dấu phân cách nhất định và nhập nó vào một chuỗi, theo mặc định đó là dòng mới. Nếu Xsố chuỗi đó chỉ là các từ / mã thông báo đơn lẻ thì công việc này có thể dễ dàng hoàn thành >>. Nếu không, bạn sẽ nhập số đầu tiên vào một số nguyên với >>, gọi cin.ignore()ở dòng tiếp theo, rồi chạy một vòng lặp mà bạn sử dụng getline().
0x499602D2

11

Mọi thứ sẽ ổn nếu bạn thay đổi mã ban đầu của mình theo cách sau:

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

3
Cảm ơn bạn. Điều này cũng sẽ hoạt động vì get()tiêu thụ ký tự tiếp theo. Cũng có (std::cin >> name).ignore()cái mà tôi đã đề xuất trước đó trong câu trả lời của mình.
0x499602D2

"..work bởi vì get () ..." Đúng, chính xác. Xin lỗi vì đã đưa ra câu trả lời mà không có chi tiết.
Boris

4
Tại sao không đơn giản if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

0

Điều này xảy ra bởi vì một nguồn cấp dòng ngầm còn được gọi là ký tự dòng mới \nđược thêm vào tất cả dữ liệu nhập của người dùng từ một thiết bị đầu cuối vì nó yêu cầu luồng bắt đầu một dòng mới. Bạn có thể giải thích điều này một cách an toàn bằng cách sử dụng std::getlinekhi kiểm tra nhiều dòng nhập của người dùng. Hành vi mặc định của std::getlinesẽ đọc mọi thứ lên đến và bao gồm cả ký tự dòng mới \ntừ đối tượng luồng đầu vào, std::cintrong trường hợp này.

#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"
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.