본문으로 건너뛰기

안동민 개발노트

본문 시작

텍스트 파일 읽기

std::ifstream으로 파일을 열고 토큰·줄 단위 데이터를 읽으며 EOF와 스트림 상태에 맞게 오류를 처리합니다.

이번 절에서는 텍스트 파일에서 데이터를 읽어오는 방법을 더 구체적으로 다룹니다.

텍스트 파일은 사람이 읽을 수 있는 형태로 문자열 데이터가 저장된 파일이며, 가장 흔하게 접하는 파일 형식 중 하나입니다.

이 장에서는 std::ifstream을 사용하여 텍스트 파일에서 문자열, 숫자, 공백이 포함된 줄 단위 데이터를 읽어오는 방법을 정리합니다.


std::ifstream로 텍스트 파일 열기

텍스트 파일을 읽기 위해서는 std::ifstream 클래스의 객체를 사용합니다.

ifstreamInput File Stream의 약자입니다.

파일을 여는 방법은 지난 장에서 다루었듯이 open() 멤버 함수를 사용하거나, ifstream 객체를 생성할 때 파일 이름을 인자로 전달하여 바로 열 수 있습니다.

텍스트 파일 열기 예제
#include <fstream> // std::ifstream을 위해
#include <iostream> // std::cout, std::endl을 위해

int main() {
    // 방법 1: 생성과 동시에 파일 열기
    std::ifstream inFile1("data.txt");
    if (!inFile1.is_open()) {
        std::cerr << "data.txt를 열 수 없습니다!\n";
    } else {
        // 파일 처리...
        inFile1.close();
    }

    // 방법 2: 객체 생성 후 open() 호출
    std::ifstream inFile2;
    inFile2.open("another_data.txt");
    if (!inFile2.is_open()) { // 파일 연결 여부 확인
        std::cerr << "another_data.txt를 열 수 없습니다!\n";
    } else {
        // 파일 처리...
        inFile2.close();
    }

    return 0;
}

std::ifstream은 기본적으로 std::ios::in 모드로 파일을 엽니다.

이 모드는 파일이 존재하지 않으면 파일을 생성하지 않고 열기에 실패합니다.


공백으로 구분된 데이터 읽기 (>> 연산자)

콘솔에서 std::cin을 사용하여 공백(스페이스, 탭, 개행 문자 등)으로 구분된 데이터를 읽듯이, 파일 스트림에서도 >> (추출) 연산자를 사용하여 공백으로 구분된 데이터를 읽을 수 있습니다.

이 연산자는 기본적으로 공백 문자를 건너뛰고 다음 데이터를 읽습니다.

numbers.txt
100 John 3.14
200 Jane 2.718
공백으로 구분된 데이터 읽기 예제
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("numbers.txt");

    if (!inFile.is_open()) {
        std::cerr << "오류: numbers.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    int id;
    std::string name;
    double value;

    // 파일 끝(EOF)에 도달할 때까지 데이터 읽기
    while (inFile >> id >> name >> value) { // inFile에서 데이터를 읽어 id, name, value에 저장
        std::cout << "ID: " << id << ", 이름: " << name << ", 값: " << value << std::endl;
    }

    if (inFile.bad()) { // 하부 I/O에서 심각한 오류가 발생했는지 확인
        std::cerr << "\n오류: 파일 장치나 버퍼에서 I/O 문제가 발생했습니다." << std::endl;
    } else if (inFile.eof()) { // eofbit와 failbit가 함께 설정될 수도 있음
        std::cout << "\nEOF에서 읽기가 종료되었습니다." << std::endl;
    } else if (inFile.fail()) { // 형식 변환 등 읽기 실패 확인
        std::cerr << "\n오류: 데이터 읽기 중 문제가 발생했습니다." << std::endl;
    }

    inFile.close();
    return 0;
}
  • while (inFile >> id >> name >> value): 데이터 추출(>>)은 스트림을 반환하고, 조건식은 그 스트림에 failbitbadbit가 없는지를 평가합니다. 레코드 추출에 실패하면 루프를 종료합니다.
  • .eof(): 파일 끝(End Of File)에 도달했는지 여부를 나타내는 플래그를 확인합니다.
  • .fail(): failbit 또는 badbit가 설정되었는지 확인합니다. 타입 불일치 같은 형식 오류는 상태를 지우고 문제 입력을 처리하면 복구할 수 있지만, badbit는 더 심각한 I/O 오류를 뜻합니다.

여러 필드를 한 번에 추출하는 도중 EOF에 도달하면 eofbitfailbit가 함께 설정될 수 있습니다. 정상적인 파일 끝과 불완전한 마지막 레코드를 구분해야 한다면 필드별 성공 여부나 레코드 형식을 별도로 검증합니다.


줄 단위로 데이터 읽기 (std::getline())

때로는 데이터가 공백으로 구분되지 않고, 한 줄 전체가 하나의 의미 있는 덩어리인 경우가 있습니다.

예를 들어 문장이나 주소록의 한 항목 등.

이때는 std::getline() 함수를 사용하여 개행 문자(\n)를 기준으로 한 줄 전체를 읽어올 수 있습니다.

std::getline() 사용 형식
std::getline(입력스트림, 문자열변수);
std::getline(입력스트림, 문자열변수, 구분문자); // 선택적으로 구분 문자 지정 가능 (기본값은 '\n')
quotes.txt
"Imagination is more important than knowledge." - Albert Einstein
"The only way to do great work is to love what you do." - Steve Jobs
Life is what happens when you're busy making other plans. - John Lennon
줄 단위로 데이터 읽기 예제
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("quotes.txt");

    if (!inFile.is_open()) {
        std::cerr << "오류: quotes.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    std::string line;
    int lineNumber = 1;

    // 파일 끝까지 한 줄씩 읽기
    while (std::getline(inFile, line)) {
        std::cout << "줄 " << lineNumber++ << ": " << line << std::endl;
    }

    inFile.close();
    return 0;
}

std::getline()은 개행 문자를 읽고 버퍼에서 제거하지만, 반환되는 line 문자열에는 개행 문자가 포함되지 않습니다.

>> 연산자와 getline()의 혼용 시 주의사항: >> 연산자는 기본 설정에서 선행 공백을 건너뛰고 토큰을 변환하지만, 토큰 뒤의 구분 공백은 남길 수 있습니다. 이 예제에서는 ID 뒤의 개행 문자(\n)가 다음 입력의 시작점에 남습니다.

만약 >> 연산자 다음에 getline()을 호출하면, getline()은 남아있는 개행 문자를 빈 줄로 해석하여 즉시 반환해버리는 문제가 발생할 수 있습니다.

이를 해결하려면 >> 연산자 후에 std::ws (Whitespace manipulator)를 사용하거나, ignore() 함수를 사용하여 남은 개행 문자를 버퍼에서 제거해야 합니다.

user_data.txt
100
Alice Wonderland
200
Bob The Builder
>>와 getline() 혼용 시 문제 해결 예제
#include <iostream>
#include <fstream>
#include <string>
#include <limits> // std::numeric_limits

int main() {
    std::ifstream inFile("user_data.txt");

    if (!inFile.is_open()) {
        std::cerr << "오류: user_data.txt 파일을 열 수 없습니다!\n";
        return 1;
    }

    int id;
    std::string fullName;

    while (inFile >> id) { // ID(숫자)를 읽고, 개행 문자는 버퍼에 남음
        // 해결책 1: std::ws (Whitespace manipulator) 사용
        // inFile >> std::ws; // 남아있는 모든 공백(개행 포함)을 건너뛴다.
        // std::getline(inFile, fullName);

        // 해결책 2: ignore() 함수 사용
        // 현재 줄의 나머지(개행 문자 포함)를 무시
        inFile.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        std::getline(inFile, fullName);

        std::cout << "ID: " << id << ", 이름: " << fullName << std::endl;
    }

    inFile.close();
    return 0;
}
  • std::numeric_limits<std::streamsize>::max(): std::streamsize가 표현할 수 있는 최댓값입니다.
  • \n: 개행 문자를 만날 때까지 무시합니다.
서식 입력으로 ID 토큰을 읽은 뒤 남은 구분 문자를 처리하지 않고 getline을 호출하는 경로와 현재 줄을 버린 뒤 다음 전체 줄을 읽는 경로를 비교하는 C++ 입력 시퀀스

C++ · formatted input → line input · sequence

읽기 단위가 바뀌면 다음 문자의 위치부터 확인한다

operator>>는 토큰을 변환하고 뒤따르는 구분 문자를 남길 수 있습니다. 곧바로 getline()을 호출할지, 현재 줄을 버리고 다음 줄부터 읽을지는 입력 형식이 결정합니다.

토큰 입력에서 줄 입력으로 전환하는 시퀀스 호출 코드가 ifstream에서 숫자 100을 읽으면 뒤의 개행이 다음 문자로 남는다. 바로 getline을 호출하면 빈 문자열을 얻고, ignore로 현재 줄 끝까지 버린 뒤 getline을 호출하면 다음 줄의 Alice Wonderland를 얻는다. ALT [버퍼 처리를 생략] [다음 줄이 한 레코드] operator>>(id) id = 100 · next = '\n' getline(name) name = "" · '\n' consumed ignore(max, '\n') current line discarded getline(name) Alice Wonderland 호출 코드 std::ifstream formatted · line input
버퍼 100\nAlice Wonderland\n
바로 getline

id = 100 다음 문자는 \n

getline(name)이 그 개행을 줄 끝으로 소비해 name == ""가 됩니다.

현재 줄 폐기

ignore(max, '\n') 뒤 다음 줄 읽기

현재 줄의 나머지와 개행을 버린 뒤 getline(name)Alice Wonderland를 읽습니다.

operator>>

토큰을 변환한다

기본 skipws는 선행 공백을 건너뛰지만, 변환한 토큰 뒤의 구분 문자는 다음 입력에 남을 수 있습니다.

getline

구분 문자까지 소비한다

한 줄의 내용을 문자열에 저장하고 구분 문자 자체는 버퍼에서 제거하되 결과 문자열에는 넣지 않습니다.

std::ws

모든 선행 공백을 건너뛴다

빈 줄이나 들여쓰기를 보존해야 한다면 의미가 달라질 수 있으므로 입력 형식에 맞을 때만 사용합니다.

ignore

현재 줄의 나머지를 버린다

max()streamsize가 표현할 수 있는 최댓값을 쓰고 '\n'을 구분자로 주면 현재 줄 끝까지 폐기합니다.

핵심은 무조건 개행을 지우는 것이 아니라 다음 필드가 같은 줄인지 다음 줄인지 먼저 정하는 것입니다. 입력 단위가 바뀌는 지점에서는 버퍼의 다음 문자와 스트림 상태를 함께 확인합니다.


파일 상태 플래그 (File State Flags)

텍스트 파일 읽기는 열기 모드, 상태 플래그, 읽기·쓰기 단위, 닫기 책임으로 확인합니다.

파일 연결을 확인하고 읽기 자체의 성공으로 반복한 뒤 bad, eof, fail 상태를 순서대로 해석하며 필요한 경우 상태 비트와 입력 위치를 함께 복구하는 C++ 파일 읽기 흐름

C++ · stream state · flowchart

성공한 읽기만 처리하고, 멈춘 이유는 뒤에서 해석한다

while (in >> value)는 EOF를 미리 예측하지 않습니다. 추출 성공으로 반복을 결정하고, 루프가 끝난 뒤 상태 비트를 읽어 정상 종료와 형식 오류, 심각한 I/O 오류를 구분합니다.

파일 읽기 성공과 종료 상태 해석 흐름 파일을 연 뒤 연결 여부를 확인한다. 연결되면 읽기가 성공하는 동안 값을 처리하고 다시 읽으며, 실패하면 bad를 먼저 보고 eof와 나머지 fail을 구분한다. 복구할 때는 clear와 입력 소비 또는 위치 조정을 함께 사용한다. no yes yes next record no fail bad eof 파일 열기 std::ifstream in(path) is_open()? 파일 연결 여부 열기 오류 경로 · 권한 · failbit 읽기 성공? in >> value 값 처리 성공한 레코드만 사용 종료 상태? bad → eof → fail 형식 오류 · 복구 clear() + consume/seek 상태와 위치를 함께 조정 심각한 I/O 오류 badbit · 중단/보고 EOF 종료
  1. 연결을 먼저 확인

    is_open()으로 파일 연관 여부를 확인하고 열기 실패는 읽기 루프 전에 처리합니다.

  2. 읽기 결과로 반복

    while (in >> value)가 성공한 값만 본문에서 처리하고 다음 추출을 다시 시도합니다.

  3. bad()를 우선 확인

    하부 장치나 버퍼의 심각한 I/O 오류면 정상 EOF로 해석하지 않고 중단하거나 보고합니다.

  4. EOF와 나머지 실패를 구분

    정상적인 레코드 경계에서 끝났는지 확인합니다. 여러 필드 중간의 EOF라면 불완전 레코드일 수 있습니다.

  5. 복구는 상태와 입력을 함께

    clear()는 상태 비트만 지웁니다. 문제 토큰 소비나 파일 위치 조정은 의도에 맞게 별도로 수행합니다.

good

상태 비트가 없다

good()은 현재 비트만 보고하며 다음 I/O 성공까지 보장하지 않습니다.

eof

끝을 만나 eofbit가 섰다

읽기 시도가 끝에 닿은 결과이며 상황에 따라 failbit도 함께 설정될 수 있습니다.

fail

failbit 또는 badbit가 있다

타입 변환 실패 같은 복구 가능한 오류와 badbit를 모두 포함하는 질의입니다.

bad

복구하기 어려운 I/O 오류다

하부 장치나 스트림 버퍼 오류를 뜻하므로 단순히 clear()하고 재시도하지 않습니다.

eofbitfailbit는 동시에 설정될 수 있습니다. 따라서 정상 EOF 판정에는 레코드가 완전했는지라는 입력 형식의 조건도 함께 들어갑니다.

파일 스트림 객체는 현재 파일의 상태를 나타내는 여러 플래그를 가집니다.

이를 통해 읽기/쓰기 작업의 성공 여부나 파일의 끝 도달 여부 등을 확인할 수 있습니다.

  • good(): 오류 상태 비트가 하나도 설정되지 않았는지 확인합니다. 이후 작업의 성공까지 보장하지는 않습니다.
  • eof(): 파일 끝(End Of File)에 도달했는지 확인.
  • fail(): failbit 또는 badbit가 설정되었는지 확인합니다. 예를 들어 타입 불일치는 보통 failbit를 설정합니다.
  • bad(): 하부 장치나 스트림 버퍼에서 복구하기 어려운 I/O 오류가 발생했는지 확인합니다.
  • clear(): 상태 비트를 지웁니다. 문제 입력을 버리거나 파일 위치를 되돌리지는 않으므로 재시도 전략에 맞는 소비 또는 위치 조정이 따로 필요합니다.
  • rdstate(): 현재 스트림의 상태 플래그들을 비트마스크로 반환.
파일 상태 플래그 예제
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("non_existent_file.txt"); // 존재하지 않는 파일

    if (!inFile.is_open()) {
        std::cerr << "파일을 열 수 없습니다. ";
        if (inFile.fail()) { // 열기 실패 시 fail()이 true
            std::cerr << "fail() 상태입니다.\n";
        }
        return 1;
    }

    int num;
    inFile >> num; // 숫자 읽기 시도

    if (inFile.fail()) {
        std::cout << "숫자를 읽는 데 실패했습니다.\n";
        inFile.clear(); // 상태 비트만 지우며, 실패한 토큰은 입력에 남아 있음
        std::string s;
        if (inFile >> s) { // 같은 토큰을 문자열로 읽는 의도적인 복구
            std::cout << "다시 읽은 문자열: " << s << std::endl;
        }
    }

    inFile.close();
    return 0;
}

파일 입출력 작업 후에는 항상 스트림의 상태를 확인하여 오류를 적절히 처리하는 것이 중요합니다.

특히 while(inFile >> data)와 같은 루프에서는 fail()이나 eof()를 명시적으로 확인하여 예외적인 상황에 대응할 수 있습니다.

읽기 루프를 작성할 때는 반복 조건과 루프 종료 후의 상태 해석을 분리해 두면 마지막 데이터와 오류를 안정적으로 구분할 수 있습니다.

clear() 뒤 같은 타입을 다시 읽으려면 문제 토큰을 ignore() 등으로 소비하거나, seek 가능한 스트림이라면 의도한 위치로 옮겨야 합니다. 다른 타입으로 같은 토큰을 해석하려는 경우에는 상태 비트만 지운 뒤 그대로 다시 추출할 수도 있습니다.