icon
8장 : 파일 입출력

텍스트 파일 읽기

이제 구체적으로 텍스트 파일에서 데이터를 읽어오는 방법에 대해 더 깊이 있게 알아보겠습니다.

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

이 장에서는 std::ifstream을 사용하여 텍스트 파일에서 문자열, 숫자, 그리고 공백이 포함된 줄 단위 데이터를 읽어오는 다양한 방법을 살펴보겠습니다.


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

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

ifstream은 "Input 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.fail()) { // .fail()은 .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.eof()) { // 파일의 끝에 도달했는지 확인
        std::cout << "\n파일 끝에 도달했습니다. 모든 데이터를 읽었습니다." << std::endl;
    } else if (inFile.fail()) { // 읽기 작업 중 오류가 발생했는지 확인
        std::cerr << "\n오류: 데이터 읽기 중 문제가 발생했습니다." << std::endl;
    }

    inFile.close();
    return 0;
}
  • while (inFile >> id >> name >> value): 이 조건문은 데이터 추출(>>) 작업이 성공하는 동안 true를 반환합니다. 데이터 추출에 실패하거나 파일의 끝에 도달하면 false를 반환하여 루프를 종료합니다.
  • .eof(): 파일 끝(End Of File)에 도달했는지 여부를 나타내는 플래그를 확인합니다.
  • .fail(): 읽기 또는 쓰기 작업 중 비복구 가능한 오류가 발생했는지 확인합니다. (예: 잘못된 데이터 타입 읽기 시도)

줄 단위로 데이터 읽기 (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()의 혼용 시 주의사항: >> 연산자는 공백을 건너뛰고 데이터를 읽지만, 개행 문자(\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(): 스트림의 최대 크기를 나타냅니다.
  • \n: 개행 문자를 만날 때까지 무시합니다.

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

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

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

  • good(): 스트림이 정상 상태인지 (오류 없음, EOF 아님) 확인. true 반환 시 모든 작업 가능.
  • eof(): 파일 끝(End Of File)에 도달했는지 확인.
  • fail(): 읽기/쓰기 작업 실패로 스트림이 실패 상태가 되었는지 확인 (예: 타입 불일치).
  • bad(): 읽기/쓰기 작업 중 심각한 오류가 발생했는지 확인 (예: 하드웨어 오류, 파일 손상).
  • 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";
        }
        inFile.clear(); // 오류 플래그 초기화
        return 1;
    }

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

    if (inFile.fail()) {
        std::cout << "숫자를 읽는 데 실패했습니다.\n";
        inFile.clear(); // 스트림 상태를 good()으로 재설정 (다시 읽기 시도 가능)
        std::string s;
        inFile >> s; // 다른 타입으로 다시 읽기 시도
        std::cout << "다시 읽은 문자열: " << s << std::endl;
    }

    inFile.close();
    return 0;
}

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

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