텍스트 파일 읽기
std::ifstream으로 파일을 열고 토큰·줄 단위 데이터를 읽으며 EOF와 스트림 상태에 맞게 오류를 처리합니다.
텍스트 파일은 사람이 읽을 수 있는 글자로 데이터를 저장한 파일입니다. 설정 파일, 로그, 표 형식의 데이터처럼 흔히 쓰이며, 메모장으로 열어 내용을 바로 확인하고 고칠 수 있습니다.
이 글은 std::ifstream으로 텍스트 파일을 열고, 공백으로 구분된 값과 한 줄 단위의 데이터를 읽는 방법을 다룹니다. 두 방식을 섞을 때 남는 줄바꿈 문제와, 읽기가 멈춘 이유를 상태 플래그로 구분하는 방법도 함께 봅니다. 예제의 입력 파일은 실행하는 작업 폴더에 둡니다.
std::ifstream으로 텍스트 파일 열기
텍스트 파일은 입력 파일 스트림(input file stream)인 std::ifstream으로 읽습니다. 스트림과 파일 입출력 개요에서 본 것처럼, 객체를 만들면서 파일 이름을 넘기거나 객체를 만든 뒤 open을 부릅니다.
#include <fstream>
#include <iostream>
int main() {
std::ifstream first("data.txt");
if (!first.is_open()) {
std::cerr << "cannot open data.txt\n";
}
std::ifstream second;
second.open("another_data.txt");
if (!second.is_open()) {
std::cerr << "cannot open another_data.txt\n";
}
}std::ifstream은 읽기 모드로 열며, 파일이 없으면 새로 만들지 않고 열기에 실패합니다. 그래서 읽기 전에 반드시 is_open()으로 연결을 확인합니다. 두 스트림은 main이 끝나 블록을 벗어날 때 자동으로 닫힙니다.
공백으로 구분된 값 읽기
std::cin처럼 파일 스트림에도 추출 연산자 >>를 쓸 수 있습니다. >>는 앞의 공백, 탭, 줄바꿈을 건너뛰고 다음 값을 읽으므로, 값 사이의 공백 개수나 줄 구분과 상관없이 차례로 읽습니다.
100 John 3.14
200 Jane 2.718#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("numbers.txt");
if (!in.is_open()) {
std::cerr << "cannot open numbers.txt\n";
return 1;
}
int id;
std::string name;
double value;
while (in >> id >> name >> value) {
std::cout << "id " << id << ", name " << name
<< ", value " << value << '\n';
}
if (in.bad()) {
std::cerr << "I/O error\n";
return 1;
} else if (in.eof()) {
std::cout << "stopped at end of file\n";
} else if (in.fail()) {
std::cerr << "bad format\n";
return 1;
}
}id 100, name John, value 3.14
id 200, name Jane, value 2.718
stopped at end of filein >> id >> name >> value는 스트림 자신을 돌려주므로, 조건은 세 값을 모두 읽은 뒤 스트림이 실패 상태가 아닌지를 묻습니다. 두 번째 레코드 뒤에서 다음 id를 읽으려다 파일 끝에 닿으면 실패해 루프가 끝나고, 이때 eofbit가 설정되어 있으므로 정상적인 끝으로 판단합니다.
다만 eof()가 참이라고 마지막 레코드가 완전했다는 뜻은 아닙니다. 마지막 줄이 200 Jane처럼 값 하나가 빠진 채 끝나면, value를 읽다가 파일 끝에 닿아 eofbit와 failbit가 함께 설정됩니다. 그 레코드는 출력되지 않지만 위 코드는 정상 종료로 보고하므로, 불완전한 레코드를 오류로 다뤄야 한다면 레코드를 한 줄씩 읽어 필드 수를 따로 검사합니다.
한 줄씩 읽기
한 줄 전체가 의미 있는 단위라면 std::getline으로 줄바꿈 앞까지 읽습니다. 공백이 포함된 문장도 한 번에 읽을 수 있습니다.
Imagination is more important than knowledge.
The only way to do great work is to love what you do.
Life is what happens when you're busy making other plans.#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("quotes.txt");
if (!in.is_open()) {
std::cerr << "cannot open quotes.txt\n";
return 1;
}
std::string line;
int number = 1;
while (std::getline(in, line)) {
std::cout << number++ << ": " << line << '\n';
}
}1: Imagination is more important than knowledge.
2: The only way to do great work is to love what you do.
3: Life is what happens when you're busy making other plans.std::getline은 줄바꿈을 입력에서 꺼내 버리고, line에는 줄바꿈 앞까지만 넣습니다. 마지막 줄에 줄바꿈이 없어도 글자를 읽었다면 성공으로 처리되므로, 그 줄도 빠짐없이 출력됩니다. 셋째 인자로 구분 문자를 넘기면 std::getline(in, field, ',')처럼 쉼표 앞까지 읽을 수도 있습니다.
>>와 getline을 섞을 때
>>는 값을 읽은 뒤 그 뒤의 줄바꿈을 입력에 남겨 둡니다. 그래서 숫자를 >>로 읽은 직후 getline을 부르면, 남은 줄바꿈 앞의 빈 문자열을 읽고 바로 돌아옵니다.
100
Alice Wonderland
200
Bob The Builder아래 그림은 같은 입력에서 두 경로를 비교합니다. 100을 읽은 뒤 바로 getline을 부르면 빈 이름을 얻고, ignore로 현재 줄의 나머지를 버린 뒤 부르면 다음 줄의 Alice Wonderland를 공백까지 그대로 읽습니다.
토큰 입력 뒤 줄 입력이 보는 버퍼
같은 100 개행 Alice Wonderland 입력을 두 독립 경로에서 읽으면 바로 getline은 빈 이름, ignore 후 getline은 이름 전체가 된다. 다음 문자/추출 결과 상태를 나란히 추적한다.
C++ · formatted input → line input · sequence
읽기 단위가 바뀌면 다음 문자의 위치부터 확인한다
operator>>는 토큰을 변환하고 뒤따르는 구분 문자를 남길 수 있습니다. 곧바로 getline()을 호출할지, 현재 줄을 버리고 다음 줄부터 읽을지는 입력 형식이 결정합니다.
100\nAlice Wonderland\n
id = 100 다음 문자는 \n
getline(name)이 그 개행을 줄 끝으로 소비해 name == ""가 됩니다.
ignore(max, '\n') 뒤 다음 줄 읽기
현재 줄의 나머지와 개행을 버린 뒤 getline(name)이 Alice Wonderland를 읽습니다.
토큰을 변환한다
기본 skipws는 선행 공백을 건너뛰지만, 변환한 토큰 뒤의 구분 문자는 다음 입력에 남을 수 있습니다.
구분 문자까지 소비한다
한 줄의 내용을 문자열에 저장하고 구분 문자 자체는 버퍼에서 제거하되 결과 문자열에는 넣지 않습니다.
모든 선행 공백을 건너뛴다
빈 줄이나 들여쓰기를 보존해야 한다면 의미가 달라질 수 있으므로 입력 형식에 맞을 때만 사용합니다.
현재 줄의 나머지를 버린다
max()에 streamsize가 표현할 수 있는 최댓값을 쓰고 '\n'을 구분자로 주면 현재 줄 끝까지 폐기합니다.
핵심은 무조건 개행을 지우는 것이 아니라 다음 필드가 같은 줄인지 다음 줄인지 먼저 정하는 것입니다. 입력 단위가 바뀌는 지점에서는 버퍼의 다음 문자와 스트림 상태를 함께 확인합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 도표 제목 | 동일 입력에서 곧바로 줄 읽기와 현재 줄 폐기 후 읽기는 대안이다 |
| 관계·조건 설명 | 두 대안은 동일한 100 개행 Alice Wonderland 개행 입력 조건을 사용한다. 첫 경로는 getline→빈 문자열, 둘째는 ignore→getline→Alice Wonderland로 각각 1개/2개 간선이며 서로 연결하지 않는다. 첫 경로 뒤에 ignore를 연속 실행한다는 뜻이 아니다. 원래 가로 그림의 ALT 조건과 값 입력/반환은 본문 시퀀스를 참조한다. |
| 노드·연결선 라벨 | ID 100 · 다음 문자는 줄바꿈 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 숫자 추출 뒤 같은 입력 100 개행 Alice Wonderland에서 시작한다. |
|
|
| 바로 getline 첫 번째 독립 경로에서는 개행이 즉시 읽힌다. |
|
|
| ignore 뒤 getline 다른 사본에서는 개행까지 제거한 뒤 읽는다. |
|
|
토큰 입력 뒤 줄 입력이 보는 버퍼
같은 100 개행 Alice Wonderland 입력을 두 독립 경로에서 읽으면 바로 getline은 빈 이름, ignore 후 getline은 이름 전체가 된다. 다음 문자/추출 결과 상태를 나란히 추적한다.
C++ · formatted input → line input · sequence
읽기 단위가 바뀌면 다음 문자의 위치부터 확인한다
operator>>는 토큰을 변환하고 뒤따르는 구분 문자를 남길 수 있습니다. 곧바로 getline()을 호출할지, 현재 줄을 버리고 다음 줄부터 읽을지는 입력 형식이 결정합니다.
100\nAlice Wonderland\n
id = 100 다음 문자는 \n
getline(name)이 그 개행을 줄 끝으로 소비해 name == ""가 됩니다.
ignore(max, '\n') 뒤 다음 줄 읽기
현재 줄의 나머지와 개행을 버린 뒤 getline(name)이 Alice Wonderland를 읽습니다.
토큰을 변환한다
기본 skipws는 선행 공백을 건너뛰지만, 변환한 토큰 뒤의 구분 문자는 다음 입력에 남을 수 있습니다.
구분 문자까지 소비한다
한 줄의 내용을 문자열에 저장하고 구분 문자 자체는 버퍼에서 제거하되 결과 문자열에는 넣지 않습니다.
모든 선행 공백을 건너뛴다
빈 줄이나 들여쓰기를 보존해야 한다면 의미가 달라질 수 있으므로 입력 형식에 맞을 때만 사용합니다.
현재 줄의 나머지를 버린다
max()에 streamsize가 표현할 수 있는 최댓값을 쓰고 '\n'을 구분자로 주면 현재 줄 끝까지 폐기합니다.
핵심은 무조건 개행을 지우는 것이 아니라 다음 필드가 같은 줄인지 다음 줄인지 먼저 정하는 것입니다. 입력 단위가 바뀌는 지점에서는 버퍼의 다음 문자와 스트림 상태를 함께 확인합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 도표 제목 | 동일 입력에서 곧바로 줄 읽기와 현재 줄 폐기 후 읽기는 대안이다 |
| 관계·조건 설명 | 두 대안은 동일한 100 개행 Alice Wonderland 개행 입력 조건을 사용한다. 첫 경로는 getline→빈 문자열, 둘째는 ignore→getline→Alice Wonderland로 각각 1개/2개 간선이며 서로 연결하지 않는다. 첫 경로 뒤에 ignore를 연속 실행한다는 뜻이 아니다. 원래 가로 그림의 ALT 조건과 값 입력/반환은 본문 시퀀스를 참조한다. |
| 노드·연결선 라벨 | ID 100 · 다음 문자는 줄바꿈 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 숫자 추출 뒤 같은 입력 100 개행 Alice Wonderland에서 시작한다. |
|
|
| 바로 getline 첫 번째 독립 경로에서는 개행이 즉시 읽힌다. |
|
|
| ignore 뒤 getline 다른 사본에서는 개행까지 제거한 뒤 읽는다. |
|
|
#include <fstream>
#include <iostream>
#include <limits>
#include <string>
int main() {
std::ifstream in("user_data.txt");
if (!in.is_open()) {
std::cerr << "cannot open user_data.txt\n";
return 1;
}
int id;
std::string fullName;
while (in >> id) {
in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
if (!std::getline(in, fullName)) {
std::cerr << "missing name for id " << id << '\n';
return 1;
}
std::cout << "id " << id << ", name " << fullName << '\n';
}
}id 100, name Alice Wonderland
id 200, name Bob The Builderignore는 지정한 개수만큼, 또는 구분 문자를 만날 때까지 글자를 버립니다. std::numeric_limits<std::streamsize>::max()를 넘기면 개수 제한 없이 줄바꿈까지 버린다는 뜻입니다. 이름 줄을 읽지 못하면 레코드가 불완전하므로 오류로 끝냅니다.
ignore 대신 std::getline(in >> std::ws, fullName)을 쓸 수도 있습니다. std::ws는 앞의 공백과 줄바꿈을 모두 건너뛰므로, 이름 앞의 공백이 사라지고 빈 이름 줄도 건너뜁니다. 두 방법은 문법만 다른 것이 아니라 받아들이는 파일 형식이 다르므로, 파일의 규칙에 맞는 쪽을 고릅니다.
더 깊이: 줄을 읽은 뒤 숫자로 바꾸는 std::from_chars
한 줄을 std::getline으로 읽은 뒤 그 안의 숫자를 값으로 바꿀 때는 C++17의 <charconv>에 있는 std::from_chars를 쓸 수 있습니다. std::from_chars(first, last, value)는 [first, last)의 글자를 읽어 value에 담고, 처음으로 형식에 맞지 않는 글자를 가리키는 ptr과 오류를 나타내는 ec를 함께 돌려줍니다. 숫자가 하나도 없으면 ec가 std::errc::invalid_argument, 범위를 넘으면 std::errc::result_out_of_range이고, 두 경우 모두 value는 바뀌지 않습니다. 그래서 ec가 비어 있고 ptr이 last와 같은지까지 보면, 그 줄이 정확히 숫자 하나였는지를 확인할 수 있습니다.
스트림의 >>와 달리 std::from_chars는 로캘(locale)의 영향을 받지 않고, 메모리를 할당하지 않으며, 예외도 던지지 않습니다. 대신 앞의 공백을 건너뛰지 않고 맨 앞의 + 부호도 받아들이지 않으므로, " 42"나 "+42"는 숫자로 읽히지 않습니다. 파일의 형식이 이런 표기를 허용한다면 바꾸기 전에 직접 다듬어야 하며, 자세한 규칙은 cppreference의 std::from_chars에 있습니다.
상태 플래그와 읽기가 멈춘 이유
스트림은 지금까지의 작업 결과를 상태 플래그로 기억합니다.
| 함수 | 참이 되는 경우 |
|---|---|
good() | 어떤 오류 플래그도 설정되지 않음 |
eof() | 읽다가 파일 끝에 닿아 eofbit가 설정됨 |
fail() | 형식이 맞지 않는 등 작업이 실패해 failbit나 badbit가 설정됨 |
bad() | 장치나 버퍼에서 복구하기 어려운 오류가 나 badbit가 설정됨 |
clear()는 플래그를 지우고, rdstate()는 현재 플래그 전체를 돌려줍니다. 아래 그림처럼 읽기 루프는 읽기의 성공만으로 계속할지 정하고, 루프가 끝난 뒤에 bad, eof, fail의 순서로 멈춘 이유를 해석합니다. bad를 먼저 보는 것은 가장 심각한 오류이기 때문이고, eof일 때는 마지막 레코드가 완전했는지를 따로 확인합니다.
읽기 성공과 종료 이유를 분리하는 흐름
연결 성공 뒤 읽기 성공에서만 값 처리하고 실패하면 bad/eof/fail로 종료 이유를 판단한다. 형식/입력 위치 복구 후 재시도는 자동으로 추가하지 않는다.
C++ · stream state · flowchart
성공한 읽기만 처리하고, 멈춘 이유는 뒤에서 해석한다
while (in >> value)는 EOF를 미리 예측하지 않습니다. 추출 성공으로 반복을 결정하고, 루프가 끝난 뒤 상태 비트를 읽어 정상 종료와 형식 오류, 심각한 I/O 오류를 구분합니다.
- 연결을 먼저 확인
is_open()으로 파일 연관 여부를 확인하고 열기 실패는 읽기 루프 전에 처리합니다. - 읽기 결과로 반복
while (in >> value)가 성공한 값만 본문에서 처리하고 다음 추출을 다시 시도합니다. bad()를 우선 확인하부 장치나 버퍼의 심각한 I/O 오류면 정상 EOF로 해석하지 않고 중단하거나 보고합니다.
- EOF와 나머지 실패를 구분
정상적인 레코드 경계에서 끝났는지 확인합니다. 여러 필드 중간의 EOF라면 불완전 레코드일 수 있습니다.
- 복구는 상태와 입력을 함께
clear()는 상태 비트만 지웁니다. 문제 토큰 소비나 파일 위치 조정은 의도에 맞게 별도로 수행합니다.
상태 비트가 없다
good()은 현재 비트만 보고하며 다음 I/O 성공까지 보장하지 않습니다.
끝을 만나 eofbit가 섰다
읽기 시도가 끝에 닿은 결과이며 상황에 따라 failbit도 함께 설정될 수 있습니다.
failbit 또는 badbit가 있다
타입 변환 실패 같은 복구 가능한 오류와 badbit를 모두 포함하는 질의입니다.
복구하기 어려운 I/O 오류다
하부 장치나 스트림 버퍼 오류를 뜻하므로 단순히 clear()하고 재시도하지 않습니다.
eofbit와 failbit는 동시에 설정될 수 있습니다. 따라서 정상 EOF 판정에는 레코드가 완전했는지라는 입력 형식의 조건도 함께 들어갑니다. 다른 타입으로 같은 토큰을 다시 해석하는 복구라면 clear() 뒤 그 토큰을 그대로 추출할 수도 있습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 도표 제목 | 읽기 성공만 처리하고 종료 이유를 분리한다 |
| 관계·조건 설명 | open→연결 확인으로 이어진다. 아니요 분기는 열기 오류로 종료하고 예 분기는 열기 오류를 건너뛰어 읽기 성공을 검사한다. 성공은 값 처리 후 읽기 검사로 돌아간다. 실패는 값 처리를 건너뛰어 종료 상태로 간다. bad를 우선 보고 심각한 오류를 판단한다. bad가 없을 때 eof는 레코드 경계를 검사하고, eof가 없는 fail은 형식 오류 복구로 이어진다. 복구 후 재시도의 자동 간선은 없고 실제 전략을 본문에서 정한다. 9개 노드와 9개 간선의 각 성공/실패/루프 관계를 보존한다. |
| 본문·주석 | 끝을 만나 eofbit가 설정됐다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 읽기 시도 연결 성공 뒤 추출 연산을 수행한다. |
|
|
| 성공 처리 읽기가 성공한 값만 사용한다. |
|
|
| 실패 종료 판정 bad/eof/fail로 끝난 이유를 확인한다. |
|
|
읽기 성공과 종료 이유를 분리하는 흐름
연결 성공 뒤 읽기 성공에서만 값 처리하고 실패하면 bad/eof/fail로 종료 이유를 판단한다. 형식/입력 위치 복구 후 재시도는 자동으로 추가하지 않는다.
C++ · stream state · flowchart
성공한 읽기만 처리하고, 멈춘 이유는 뒤에서 해석한다
while (in >> value)는 EOF를 미리 예측하지 않습니다. 추출 성공으로 반복을 결정하고, 루프가 끝난 뒤 상태 비트를 읽어 정상 종료와 형식 오류, 심각한 I/O 오류를 구분합니다.
- 연결을 먼저 확인
is_open()으로 파일 연관 여부를 확인하고 열기 실패는 읽기 루프 전에 처리합니다. - 읽기 결과로 반복
while (in >> value)가 성공한 값만 본문에서 처리하고 다음 추출을 다시 시도합니다. bad()를 우선 확인하부 장치나 버퍼의 심각한 I/O 오류면 정상 EOF로 해석하지 않고 중단하거나 보고합니다.
- EOF와 나머지 실패를 구분
정상적인 레코드 경계에서 끝났는지 확인합니다. 여러 필드 중간의 EOF라면 불완전 레코드일 수 있습니다.
- 복구는 상태와 입력을 함께
clear()는 상태 비트만 지웁니다. 문제 토큰 소비나 파일 위치 조정은 의도에 맞게 별도로 수행합니다.
상태 비트가 없다
good()은 현재 비트만 보고하며 다음 I/O 성공까지 보장하지 않습니다.
끝을 만나 eofbit가 섰다
읽기 시도가 끝에 닿은 결과이며 상황에 따라 failbit도 함께 설정될 수 있습니다.
failbit 또는 badbit가 있다
타입 변환 실패 같은 복구 가능한 오류와 badbit를 모두 포함하는 질의입니다.
복구하기 어려운 I/O 오류다
하부 장치나 스트림 버퍼 오류를 뜻하므로 단순히 clear()하고 재시도하지 않습니다.
eofbit와 failbit는 동시에 설정될 수 있습니다. 따라서 정상 EOF 판정에는 레코드가 완전했는지라는 입력 형식의 조건도 함께 들어갑니다. 다른 타입으로 같은 토큰을 다시 해석하는 복구라면 clear() 뒤 그 토큰을 그대로 추출할 수도 있습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 도표 제목 | 읽기 성공만 처리하고 종료 이유를 분리한다 |
| 관계·조건 설명 | open→연결 확인으로 이어진다. 아니요 분기는 열기 오류로 종료하고 예 분기는 열기 오류를 건너뛰어 읽기 성공을 검사한다. 성공은 값 처리 후 읽기 검사로 돌아간다. 실패는 값 처리를 건너뛰어 종료 상태로 간다. bad를 우선 보고 심각한 오류를 판단한다. bad가 없을 때 eof는 레코드 경계를 검사하고, eof가 없는 fail은 형식 오류 복구로 이어진다. 복구 후 재시도의 자동 간선은 없고 실제 전략을 본문에서 정한다. 9개 노드와 9개 간선의 각 성공/실패/루프 관계를 보존한다. |
| 본문·주석 | 끝을 만나 eofbit가 설정됐다 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 읽기 시도 연결 성공 뒤 추출 연산을 수행한다. |
|
|
| 성공 처리 읽기가 성공한 값만 사용한다. |
|
|
| 실패 종료 판정 bad/eof/fail로 끝난 이유를 확인한다. |
|
|
형식 오류로 실패한 뒤 다시 읽으려면 clear()만으로는 부족합니다. clear()는 플래그만 지울 뿐, 읽지 못한 글자는 입력에 그대로 남아 있습니다.
abc 42#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("word.txt");
if (!in.is_open()) {
std::cerr << "cannot open word.txt\n";
return 1;
}
int number;
if (!(in >> number)) {
std::cout << "not a number\n";
in.clear();
std::string token;
in >> token;
std::cout << "skipped " << token << '\n';
}
if (in >> number) {
std::cout << "number " << number << '\n';
}
}not a number
skipped abc
number 42abc는 정수로 읽을 수 없어 failbit가 설정되고, clear()로 플래그를 지운 뒤 같은 abc를 문자열로 읽어 넘깁니다. 그다음에야 42를 정수로 읽을 수 있습니다. clear() 뒤에 abc를 넘기지 않고 다시 정수를 읽으면 같은 abc에서 또 실패하므로, 이런 코드를 루프 안에 두면 끝나지 않습니다.
정리
std::ifstream은 파일이 없으면 열기에 실패하므로, 읽기 전에is_open()으로 확인합니다.>>는 공백과 줄바꿈을 건너뛰고 값을 읽으며,while (in >> a >> b)는 모든 값을 읽은 경우에만 본문을 실행합니다.std::getline은 한 줄을 읽고 줄바꿈은 버리며, 셋째 인자로 다른 구분 문자를 줄 수 있습니다.>>다음에getline을 부르려면ignore나std::ws로 남은 줄바꿈을 처리하며, 둘은 받아들이는 형식이 다릅니다.- 루프가 끝난 뒤
bad,eof,fail로 멈춘 이유를 구분하고,eof만으로 마지막 레코드가 완전했다고 판단하지 않습니다. clear()는 플래그만 지우므로, 다시 읽기 전에 문제가 된 입력을 소비해야 합니다.
연습 문제
1. 값이 빠진 마지막 레코드
numbers.txt의 둘째 줄을 200 Jane으로 바꾸면 read_tokens.cpp는 무엇을 출력하나요? 마지막 줄이 정상 종료 메시지라면 둘째 레코드가 완전했다고 볼 수 있나요?
해설 보기
첫 레코드는 정상적으로 출력되지만, 둘째 레코드는 value를 읽다가 파일 끝에 닿아 실패하므로 본문이 실행되지 않습니다. 이때 eofbit와 failbit가 함께 설정되어 stopped at end of file이 출력되지만, 둘째 레코드는 값이 빠진 불완전한 레코드입니다. 이를 오류로 다루려면 한 줄씩 읽어 필드 수를 검사해야 합니다.
2. 공백과 빈 줄이 있는 이름
user_data.txt의 내용이 100, Alice Wonderland(앞에 공백 두 개), 200, 빈 줄의 네 줄이라면 mix_token_and_line.cpp가 읽는 두 이름은 무엇인가요? std::ws를 쓰는 방식으로 바꾸면 어떻게 되나요?
해설 보기
ignore는 숫자가 있는 줄의 나머지만 버리므로, 첫 이름은 앞 공백 두 개가 남은 Alice Wonderland이고 둘째 이름은 빈 줄을 읽은 빈 문자열입니다. std::ws를 쓰면 첫 이름의 앞 공백이 사라지고, 둘째 레코드에서는 빈 줄까지 건너뛴 뒤 파일 끝에 닿아 이름을 읽지 못하므로 missing name for id 200을 출력하고 끝납니다.
3. 지우기만 한 상태
recover_format_error.cpp에서 in >> token;을 지우면 출력은 어떻게 되나요?
해설 보기
clear()로 플래그는 지워지지만 abc가 입력에 그대로 남아 있습니다. 그래서 skipped를 출력한 뒤의 in >> number가 다시 abc에서 실패해 number 42는 출력되지 않습니다. 이때 token은 빈 문자열이므로 출력은 not a number와 skipped 입니다.
다음 글 텍스트 파일 쓰기에서는 std::ofstream으로 파일에 글자를 쓰고, 열기 모드와 출력 버퍼, 쓰기 오류를 확인하는 방법을 다룹니다.