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 name
cho đế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_pointer
là 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::skipws
1 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 noskipws
bằng 0 và is.flags() & ios_base::skipws
khá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 c
là 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 true
bở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::ws
hà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 count
ký 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::ws
chỉ đượ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 name
biến. Tất cả những gì còn lại là ký tự dòng mới.
1: std::skipws
là 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ỏ count
ký 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 count
nế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.
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).