Đọc trọn một dòng
cin >> text chỉ đọc một từ: nó dừng lại ở dấu cách đầu tiên. Để đọc mọi thứ người dùng gõ cho đến khi nhấn Enter, kể cả dấu cách, hãy dùng std::getline trong header <string>:
#include <iostream>
#include <string>
int main() {
std::string name;
std::cout << "Full name: ";
std::getline(std::cin, name);
std::cout << "Hello, " << name << "!\n";
}
Gõ Ada Lovelace sẽ in ra Hello, Ada Lovelace!. Nếu dùng std::cin >> name, chương trình sẽ in Hello, Ada! và để Lovelace nằm chờ trong đầu vào.
Signature của hàm như sau:
std::istream& std::getline(std::istream& input, std::string& text);
std::istream& std::getline(std::istream& input, std::string& text, char delim);
inputlà một stream đầu vào bất kỳ:std::cin, một tệp mở bằngstd::ifstream, hoặc mộtstd::istringstreamtạo từ chuỗi.textbị thay thế (không phải nối thêm) bằng các ký tự vừa đọc.delimlà ký tự kết thúc lần đọc, mặc định là'\n'. Ký tự phân tách bị loại khỏi stream và không được lưu vàotext.- Giá trị trả về là chính stream đó, nhờ vậy bạn có thể kiểm tra nó trong một điều kiện.
Vì getline hoạt động giống nhau trên mọi stream đầu vào, các ví dụ chạy được trong trang này đọc từ một std::istringstream chứa văn bản cố định. Nhờ đó đầu ra luôn đoán trước được mà bạn không cần gõ gì. Muốn đọc từ bàn phím, chỉ cần thay biến stream bằng std::cin.
Đầu ra:
[Ada Lovelace]
[Alan Turing]
Cặp ngoặc vuông cho thấy ký tự xuống dòng không thuộc chuỗi nào cả.
getline sau cin >> trả về chuỗi rỗng
Đây là lỗi hay gặp nhất với getline. Bạn đọc một số bằng >>, rồi đọc một dòng bằng getline, và lệnh getline có vẻ như bị bỏ qua:
Đầu ra:
age: 30
city: []
input >> age đọc các ký tự 30 và dừng ở ký tự đầu tiên không thể thuộc về một con số, tức ký tự xuống dòng. Ký tự xuống dòng đó vẫn nằm lại trong stream. Sau đó getline đọc đến ký tự xuống dòng kế tiếp, gặp ngay nó và trả về chuỗi rỗng. Với std::cin, hậu quả là chương trình không bao giờ dừng lại chờ câu trả lời thứ hai.
Có hai cách sửa tiêu chuẩn. Cả hai đều đặt giữa >> và getline.
Cách 1: bỏ phần còn lại của dòng bằng ignore.
Đầu ra:
age: 30
city: [Paris]
ignore(n, '\n') vứt bỏ các ký tự cho đến khi đã loại một ký tự xuống dòng hoặc đủ n ký tự, tùy điều kiện nào đến trước. Truyền giá trị std::streamsize lớn nhất có nghĩa là "đến ký tự xuống dòng, dù xa đến đâu". Cách này cũng loại luôn mọi thứ người dùng gõ sau con số trên cùng dòng, như các dấu cách thừa ở cuối. Với bàn phím, dòng lệnh là std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');, và cần #include <limits>.
Một lệnh std::cin.ignore() trơn chỉ bỏ đúng một ký tự. Nó chạy được khi ký tự xuống dòng đứng ngay sau con số, nhưng hỏng ngay khi có một dấu cách đứng trước.
Cách 2: bỏ qua khoảng trắng ở đầu bằng std::ws.
Đầu ra:
age: 30
city: [Paris]
std::ws là một manipulator tiêu thụ toàn bộ khoảng trắng (dấu cách, tab, xuống dòng) ở vị trí hiện tại. input >> std::ws trả về stream, nên có thể đưa thẳng vào getline. Nó cũng bỏ qua các dòng trống và dấu cách ở đầu, rất tiện cho tên người hay tên thành phố, nhưng lại sai khi dấu cách ở đầu hoặc dòng trống là đầu vào có nghĩa. Khi đó hãy dùng ignore.
Đọc đến hết đầu vào
getline trả về stream, và stream chuyển thành false khi một lần đọc thất bại. Khi không còn dòng nào, getline thất bại, nên một vòng lặp while bao quanh nó sẽ đọc mọi dòng và dừng ở cuối:
Đầu ra:
1: [apples 3]
2: [bread 1]
3: []
4: [milk 2]
read 4 lines
Hai chi tiết đáng chú ý trong đầu ra đó:
- Một dòng trống vẫn là một lần đọc thành công. Nó cho ra chuỗi rỗng và vòng lặp tiếp tục. Chỉ khi chạm đến cuối đầu vào thì vòng lặp mới dừng.
- Dòng cuối không cần ký tự xuống dòng ở cuối. Nếu văn bản kết thúc bằng
milk 2mà không có\n,getlinevẫn trả về dòng đó, và lần gọi tiếp theo mới thất bại.
Với std::cin, cùng vòng lặp đó đọc cho đến khi đầu vào kết thúc. Điều này xảy ra khi đầu vào được chuyển từ một tệp (./program < data.txt), hoặc khi người dùng nhấn Ctrl+D trên macOS và Linux, hay Ctrl+Z rồi Enter trên Windows.
Tránh kiểu viết while (!input.eof()) { std::getline(input, line); ... }. eof() chỉ thành true sau khi một lần đọc đã chạm cuối, nên thân vòng lặp chạy thêm một lần với lần đọc thất bại và một line cũ. Thay vào đó, hãy đặt lời gọi getline vào điều kiện.
Đọc tệp từng dòng
Tệp cũng dùng đúng vòng lặp đó. Mở tệp bằng std::ifstream trong <fstream>, kiểm tra tệp đã mở được chưa, rồi đọc:
Đầu ra:
1. Ada 92
2. Grace 88
3. Alan 95
if (!file) bắt được trường hợp thiếu tệp hoặc sai đường dẫn trước khi vào vòng lặp. Không có nó, vòng lặp chỉ đơn giản đọc được 0 dòng, trông như một tệp rỗng chứ không phải một lỗi. Tệp tự đóng khi file ra khỏi phạm vi ở cuối main.
Tệp lưu trên Windows kết thúc mỗi dòng bằng \r\n. Khi đọc tệp như vậy trên Linux hoặc macOS, getline loại \n nhưng giữ lại \r, nên dòng nào cũng kết thúc bằng một ký tự carriage return vô hình, làm hỏng các phép so sánh như line == "done". Hãy loại bỏ nó sau khi đọc:
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
Dùng ký tự phân tách tùy chọn
Đối số thứ ba thay đổi ký tự kết thúc mỗi lần đọc. Một cách dùng phổ biến là tách một dòng theo dấu phẩy. Đọc dòng trước, rồi tách nó qua một std::istringstream:
Đầu ra:
fields: 3
[Ada Lovelace]
[36]
[London]
Dấu phẩy bị loại khỏi stream và không được lưu, giống như ký tự xuống dòng ở dạng mặc định. Lưu ý rằng khi dùng ký tự phân tách tùy chọn, ký tự xuống dòng chỉ là một ký tự bình thường. Nếu bạn gọi thẳng std::getline(file, field, ',') trên một tệp nhiều dòng, trường cuối của dòng này và trường đầu của dòng sau sẽ bị dính vào nhau, như "London\nGrace Hopper". Đọc từng dòng một rồi tách riêng từng dòng sẽ tránh được chuyện này. Trang về string stream trình bày std::istringstream chi tiết hơn, kể cả cách chuyển các trường như "36" thành số.
std::getline và cin.getline
Còn có một hàm thành viên với tên gần giống:
char buffer[20];
std::cin.getline(buffer, 20); // member function, fixed-size char array
std::string text;
std::getline(std::cin, text); // free function, std::string
cin.getline ghi vào một mảng char và lưu tối đa size - 1 ký tự cộng với ký tự kết thúc '\0'. Một dòng dài hơn sẽ đưa stream vào trạng thái lỗi, và mọi lần đọc sau đó đều bị bỏ qua cho đến khi bạn gọi std::cin.clear(). std::getline với std::string thì không có giới hạn độ dài nào phải lo. Hãy dùng nó, trừ khi bạn làm việc với một API bắt buộc dùng mảng char.
Lỗi thường gặp
- Dùng lẫn
>>vàgetlinemà không bỏ ký tự xuống dòng. Thêmignore(...)hoặc dùngstd::wsở giữa. Trang về nhập xuất cho thấy cùng vấn đề này vớistd::cinvà cách khôi phục khi đọc số thất bại. - Gọi
getline(std::cin, number)với mộtint.getlinechỉ đọc vàostd::string. Hãy đọc cả dòng, rồi chuyển đổi bằngstd::stoihoặc mộtstd::istringstream. - Lặp dựa vào
eof(). Hãy kiểm tra chính lời gọigetline:while (std::getline(input, line)). - Quên
#include <string>.std::getlinecho chuỗi được khai báo trong<string>. Một số trình biên dịch vẫn cho qua nhờ<iostream>, và rồi code lại lỗi trên một trình biên dịch khác. - Không kiểm tra tệp đã mở được chưa. Một đường dẫn sai khiến
std::ifstreamthất bại trong im lặng và vòng lặp đọc chạy 0 lần.
Câu hỏi thường gặp
getline trong C++ dùng để làm gì?
std::getline(stream, text) đọc các ký tự từ stream vào std::string text cho đến khi gặp ký tự xuống dòng, rồi loại ký tự xuống dòng đó khỏi stream mà không lưu lại. Khác với cin >> text, hàm này giữ nguyên dấu cách, nên đọc được trọn một dòng như họ tên đầy đủ hay cả một câu.
Vì sao getline bị bỏ qua sau cin >> trong C++?
cin >> number dừng đọc ở ký tự xuống dòng bạn gõ sau con số và để ký tự đó lại trong bộ đệm đầu vào. Lệnh getline tiếp theo gặp ngay ký tự này và trả về chuỗi rỗng. Hãy bỏ nó đi trước bằng std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');, hoặc gọi std::getline(std::cin >> std::ws, text) để bỏ qua khoảng trắng ở đầu.
Dùng getline với ký tự phân tách khác ký tự xuống dòng như thế nào?
Truyền ký tự phân tách làm đối số thứ ba: std::getline(stream, field, ','). Hàm đọc đến dấu phẩy tiếp theo, lưu phần văn bản đứng trước và bỏ dấu phẩy đi. Khi đó ký tự xuống dòng chỉ là ký tự bình thường, nên cách này thường đi kèm một std::stringstream tạo từ một dòng duy nhất.
Đọc tệp từng dòng trong C++ như thế nào?
Mở tệp bằng std::ifstream file("data.txt"); rồi lặp với while (std::getline(file, line)) { ... }. getline trả về chính stream, và stream chuyển thành false khi hết dòng để đọc, nên vòng lặp dừng ở cuối tệp.
cin.getline và getline khác nhau thế nào?
std::getline(std::cin, text) là hàm tự do, đọc vào một std::string có thể tự mở rộng để chứa dòng dài bao nhiêu cũng được. std::cin.getline(buffer, size) là hàm thành viên, đọc vào một mảng char cố định và thất bại nếu dòng dài hơn size - 1 ký tự. Hãy dùng std::getline với std::string, trừ khi bạn có lý do để dùng mảng thô.