본문 바로가기

C++/FOCU_C++

C++ chpt5.

파일 입출력(I/O)

ifstream

파일 입력

 

ofstream

파일 출력

 

fstream

파일 입력 및 출력

 

파일 스트림에 <<, >>, 조정자도 사용가능

 

C 와의 파일열기 비교

// C

FILE *fp;
	
// 읽기 전용으로 파일 열기
fp = fopen("helloWorld.txt", "r");

// 쓰기 전용으로 파일 열기 (파일이 없으면 해당 파일 생성)
fp = fopen("helloWorld.txt", "w+");

// 읽기와 쓰기 범용으로 파일 열기
fp = fopen("helloWorld.txt", "r+");

---------------------------------------------------

// C++

// 읽기 전용으로 파일 열기
ifstream fin;
fin.open("helloWorld.txt");

// 쓰기 전용으로 파일 열기 (파일이 없으면 해당 파일 생성)
ofstream fout;
fout.open("helloWorld.txt");

// 읽기와 쓰기 범용으로 파일 열기
fstream.fs
fs.open("helloWorld.txt");

 

 

위 예문에서 사용된 open 에 대해서 조금 알아보자

 

open()

각 스트림마다 open() 메서드가 존재

fin.open("helloWorld.txt", ios_base::in | ios_base::binary);

 

모드 플래그

네임스페이스 = ios_base

  • in
  • out
  • ate (at the end: 제일 마지막 파일 포인터로 이동)
  • app (append)
  • trunc
  • binary

모든 조합이 유효하지는 않음

 

C 의 모드 플래그와 비

C C++
"r" ios_base::in
"w" ios_base::out
ios_base::out | ios_base::trunc
"a" ios_base::out | ios_base::app
"r+" ios_base::in | ios_base::out
"w+" ios_base::in | ios_base::out | ios_base::trunc

 

 

C 와의 파일 닫기 비교

// C

FILE *fp;

// ...

fclose(fp);

-------------------------------------------

// C++

ifstream fin;

// ...

fin.close(); 
// fin 이 스코프를 벗어나면 자동으로 닫아주기 때문에 강제성은 없으나 가독성 측면이나 일찍 닫을 때를 위해 사용

 

역시 각 스트림마다 close() 가 존재

 

 

C 와의 스트림 상태 비교

// C

FILE *fp;

fp = fopen("helloWorld.txt", "r+");

if (fp != NULL)
{
	// ...
}

---------------------------------------------

// C++

fstream fs;

fs.open("helloWorld.txt");

if (fs.is_open()) // is_open() 파일이 열려있는지 확인
{
	// ...
}

파일에서 한 문자, 한 줄, 한 단어 읽기

C 와의 한 문자 읽기 비교

// C

FILE *fp;

fp = fopen("HelloWolrd.txt", "r");

char character;
do
{
    character = getc(fp);
    printf("%c", character);
} while(character != EOF);

fclose(fp);

----------------------------------------------------------------------

// C++

ifstream fin;
fin.open("HelloWorld.txt");

char character;
while (true)
{
    fin.get(character);
    if (fin.fail())
    {
        break;
    }
    cout << character;
}

fin.close();

 

get, getline(), >>

 

어떤 스트림(ex cin, istringstream)을 넣어도 동일하게 동작 (== 추상화)

예시

fin.get(character);

fin.getline(firstName, 20);
getline(fin, line);

fin >> word;

 

 

파일에서 한 줄 읽기

ifstream fin;
fin.open("HelloWolrd.txt");

string line;
while (!fin.eof())
{
    getline(fin, line);
    cout << line << endl;
}

fin.close();

 

 

파일에서 한 단어 읽기

 

문자 + 숫자 경우

ifstream fin;
fin.open("HelloWorld.txt");

// 문자 + 숫자가 존재할 경우

string name;
float balance;

while (!fin.eof())
{
    fin >> name >> balance;
    cout << name << ": $" << balance << endl;
}

fin.close();

--------------------------------------------------------

// 숫자만 존재할 경우

int number;

while (!fin.eof())
{
    fin >> number;
    cout << number << endl;
}

 

 

단어 읽어오기에서 오류가 생기는 케이스

 

1. 개행이 EOF 전에 존재

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 200 300\n";

int number;

while (!fin.eof())
{
    fin >> number;
    cout << number << endl;
}

------------------------
100
200
300 // 개행 전까지 무사히 읽어옴
300 // EOF 로 인해 파일에선 아무것도 읽지 못해서 number 값이 업데이트 되지 않아 이전 300 을 그대로 출력하고 종료

 

2. 숫자만 출력하려는 코드에 문자가 들어있는 파일을 읽을 때

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 C++ 300";

int number;

while (!fin.eof())
{
    fin >> number;
    cout << number << endl;
}


------------------------
100
100
100
100
... 
// fin 의 주소가 C 를 가리키고 있지만 C 는 문자이기 때문에 읽어오지를 못해서 주소는 제자리에서 계속 머물고, 
// number 또한 값이 업데이트 되지 않아 무한 루프로 100 을 출력

 

 

 

위 같은 문제들을 해결하기 위한 제시 방안을 살펴보자

 

1. 제대로 읽은 것만 출력 (failbit 를 이용)

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 200 300\n";

while (!fin.eof())
{
    fin >> number;
    
    if (!fin.fail())
    {
        cout << number << endl;
    }
}

 

이 방법은 1 번 문제에 대한 해결책으로는 손색 없으나 2 번 문제까진 해결해주지 못하는 단점이 존재한다\

 

 

2. 다음 구분 문자까지 건너뛰기

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 C++ 300";

int number;

while (!fin.eof())
{
    fin >> number;

    if (fin.fail())
    {
        fin.clear();			// 비트 플래그 원상복구
        fin.ignore(LLONG_MAX, ' ');	// 현재 구분자인 ' ' 를 만날때까지 마주하는 문자들은 한 번에 건너 뜀
    }
    else
    {
        cout << number << endl;
    }
}

 

이 방법이 문제 1, 2 를 해결해주는 방식임에는 적절한 방식이다.

하지만 문장에 구분자가 공백뿐이 아닌 다른 문자가 섞일 경우의 취약점이 존재함다

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 C++\t300";

int number;

while (!fin.eof())
{
    fin >> number;

    if (fin.fail())
    {
        fin.clear();			// 비트 플래그 원상복구
        fin.ignore(LLONG_MAX, ' ');	// 현재 구분자인 ' ' 를 만날때까지 마주하는 문자들은 한 번에 건너 뜀
    }
    else
    {
        cout << number << endl;
    }
}
// 구분자를 ' ' 으로 설정했기 때문에 \t 는 문자 취급을 받아서 그대로 넘어가버려 최종적으로 EOF 주소에 다다르게 된다

 

 

그래서 구분자에 구애 받지 안고 단어를 출력하는 방법에는 다음과 같은 예문을 사용할 수 있다

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 C++ 300";

int number;
string trash // 필요없는 값을 버리는 용도로 사용할 변수 선언

while (true)
{
    fin >> number;

    if (!fin.fail())
    {
        cout << number << endl;
        continue ;
    }
    if (fin.eof())	// fail 비트가 유효할 경우 EOF 에 의한 fail 이면 읽기를 종료
    {
        break;
    }
    fin.clear();	// 아니라면 비트 플래그를 원복
    fin >> trash;	// 현재 위치의 단어를 읽어서 trash 에 저장한 후, 파일에서 다음 단어를 읽을 수 잇도록 유도
}

fin.close();

 

위 코드처럼 해결 방식을 설정하고자 다음과 같이 clear() 의 위치가 달라지면 또 다른 문제가 발생할 수 있다

ifstream fin;
fin.open("HelloWorld.txt");

// HelloWord.txt = "100 C++ 300";

int number;
string trash // 필요없는 값을 버리는 용도로 사용할 변수 선언

while (!fin.eof())
{
    fin >> number;

    if (fin.fail())
    {
        fin >> trash;	// fail 비트시 더이상 읽지 않기 때문에 fin 의 주소 위치는 그대로이고, trash 엔 아무것도 저장이 되지 않음
        in.clear();	// 비트 플래그 원복
    }
    else
    {
        cout << number << endl;
    }
}

fin.close();

--------------------------------
100
... // 공백 무한루프

 

그러므로 clear() 위치 설정시 논리 적으로 잘 설정해야 된다.

 


파일 읽기의 best practice

EOF 처리는 까다롭다

  • 입출력 연산이 스트림 상태 비트를 변경한다는 사실을 기억할 것
  • EOF 비트를 잘못 처리하면 무한 반복을 초래
  • clear() 를 사용할 땐 여러번 생각하기

입력 처리 문제는 업계에서 매우 흔한 문제

  • C# 이나 Java 를 본떠 자신만의 스트림 리더(reader)를 만드는 일이 흔함
  • 처음부터 완벽하게 입력을 처리하는 코드를 작성하는 건 거의 불가능
    • 여러번의 테스트
    • 어떤 테스트를 적용할 것이가도 중요

훌륭한 테스트 케이스

1. 유효한 입력 뒤에  EOF

1 0 0 \n 2 0 0 EOF    

 

2. 유효한 입력과 뉴라인(\n) 뒤에 EOF

1 0 0 \n 2 0 0 \n EOF  

 

3. 유효하지 않은 입력 뒤에 EOF

1 0 0 \n C + + EOF    

 

4. 유효하지 않은 입력과 뉴라인(\n) 뒤에 EOF

1 0 0 \n C + + \n EOF  

 

5. 공백 | 탭 도 포함할 것인가?

1 0 0   C + + \t ...  

 

6. 키보드 입력과 입력 리다이렉션을 둘 다 확인할 것

// 키보드 입력
100 200 300 // 엔터 입력인한 '\n' 존재 확인

// 파일 입력
cmd > numbers.exe < numbeinput.txt // 파일 또는 문장 종료값이 '\n' 인지 EOF 인지 확인

 


파일에 쓰기

우선 C 와의 코드 비교를 해보자

// C

FILE *fp

fp = fopen("HelloWorld.txt". "w");

char line[512];

if (fgets(line, 512, stdin) != NULL)
{
    fprintf(fp, "%s\n", line);
}

fclose(fp);

-------------------------------------------------------------

// C++

ofstream fout;
fout.open("HelloWorld.txt");

string line;
getline(cin, line);

if (!cin.fail()) // EOF 입력으로 fail 비트 유도하여 종료 설정
{
    fout << line << endl;
}

fin.close();

 

 

put()

  • 문자를 써 넣음
  • fout,put(character);

<<

  • fout << line << endl;

[ 바이너리 파일읽기 C 코드와 비교 ]

// C

FILE *fp;

fp = fopen("studentRecord.dat", "rb"); // rb = read binary

if (fp != NULL)
{
    Record record; 
    // Record 라는 구조체를 record 라는 변수명으로 초기화, record 는 char[20] 변수 2개와 int 변수 1개를 가짐(총 44바이트)
    fread(&record, sizeof(record), 1, fp);
    // recode 에다가 총 recode 바이트 크기 만큼 fp 에서 1 개씩 읽어와라
}
fclose(fp);

--------------------------------------------------------------------------------

// C++

ifstream fin("studentRecords.dat", ios_base::in | ios_base::binary);

if (fin.is_open())
{
    Record record;
    // Record 라는 구조체를 record 라는 변수명으로 초기화, record 는 char[20] 변수 2개와 int 변수 1개를 가짐(총 44바이트)
    fin.read((char*)&record, sizeof(Record));
    // read 함수에 전달된 주소로부터 44바이트 만큼 읽어와라
}
fin.close();

 

코드는 크게 차이나지 않으나 C 는 fp 를 직접 열어주만, C++ 에선 fin 객체 내부에서 fp 이 설정되어 알아서 읽어온다.

 

read() 사용법

ifstream::read()

read(char *, streamsize)

// ex) 파일로부터 문자 20자를 읽어 firstName에 저장
fin.read(firstName, 20):

 


[ 바이너리 파일에 쓰기 ]

// C

FILE *fp;
fp = open("studentRecords.dat", "w");

if (fp != NULL)
{
    char buffer[20] = "Pope Kim";
    fwrite(buffer, 20, 1, fp);
}
fclose(fp);

-------------------------------------------------------------------------

// C++

ofstream fout("studentRecords.dat", ios_base::out | ios_base::binary)

if (fout.is_open())
{
    char buffre[20] = "Pope Kim";
    fin.write(buffer, 20);
}
fin.close();

 

write() 사용법

ofstream::write()

write(const char*, streamsize)

// ex) firstName에 저장 되어있는 문자 20자를 파일에 쓰기
fout.write(firstName, 20);

[ 파일 안에서의 탐색 ] (읽고싶은 지점을 찾는 행위)

 

// C

FILE *fp
fp = fopen("studentRecords.dat", "w");

if (fp != NULL)
{
    if (fseek(fp, 20, SEEK_SET) == 0)
    {
    	// SEEK_SET(처음) 으로부터 20 번 째까지 건너뜀. 즉, 21번 째 위치에서부터 덮어쓰기
    }
}
fclose(fp);

-------------------------------------------------------------------

C++

fstream fs("helloWorld.dat", ios_base::in | ios_base::out | ios_base::binary);

if (fs.is_open())
{
    fs.seekp(20, ios_base::beg); // seekp == seek + put, beg == begin
    if (!fs.fail())
    {
        // 21 번째 위치에서부터 덮어쓰기
    }
}
fs.close()

 

[ 탐색 유형 ]

  • 절대적 탐색
    • 특정한 위치로 이동
    • 보통 tellp() (= 읽기 포인터 위치 반환) / tellg() (=쓰기 포인터 위치 반환) 를 사용해서 기억해 놨던 위치로 돌아갈 때 사용
  • 상대적 탐색
    • 어떤 기준점을 통해 탐색
    • 앞에서 20 또는 뒤에서 -5 등등
    • C
      • CSEEK_SET == 시작지점으로부터
      • SEEK_CUR == 현재지점으로부터
      • SEEK_END == 끝지점으로부터 (offset 을 음수로 설정 fseek(fp, -20, SEEK_END))
    • C++
      • ios_base::beg ==  시작지점으로부터
      • ios_base::cur == 현재지점으로부터
      • ios_base::end == 끝지점으로부터

 

[ 파일 쓰기's 위치 읽기 및 변경 비교 ]

 

tellp(): 쓰기 포인터의 현재 위치 구함

ios::pos_type pos = fout.tellp();

 

seekp()

  • 절대적
fout.seekp(0); // 처음 위치로 이동
  • 상대적
fout.seekp(20, ios_base::cur); //현재 위치로부터 20 바이트 뒤로 이동

 

 

[ 파일 읽기's 위치 읽기 및 변경 비교 ]

 

tellg(): 읽기 포인터의 현재 위치를 구함

ios::pos_type pos = fin.tellg();

 

seekg()

  • 절대적
fin.seekg(0) // 처음 위치로 이동
  • 상대적
fin.seekg(-10, ios_base::end); // 파일의 끝에서부터 10 바이트 앞으로 이동

 


파일 입출력 예제

 

// PrintRecords.h

#pragma once

#include <iostream>
#include <string>

struct Record
{
    std::string FirstName;
    std::string LastName; 
    std::string StudentID;
    std::string Score;
};
// 이 구조체는 std::string을 포함해 메모리 크기가 고정되어 있지 않기 때문에, 몇 바이트(고정 크기)씩 잘라서 저장하거나 읽는 방식은 사용할 수 없으며,
// 구분자 기반 텍스트 저장 및 파싱 방식으로 다뤄야 한다.

namespace samples
{
    Record ReadRecord(std::istream& stream, bool bPrompt);
    // istream 을 통해 입력을 받음. istream 은 file이 될 수도 있고, cin 도 될 수 있음 (둘 다 input stream 이기 때문에)
    // bPormpt 프롬프트를 띄울지 말지 결정하는 플래그
    // istream 이 file이면 필요없으니 false 처리하지만, cin 은 키보드로부터 입력받으니 필요시 true 처리 해줌으로써 프롬프트에 문구를 제공함

    void WriteFileRecord(std::fstream&outputStream, const Record& record);
    // file 에만 쓰기위해 fstream 사용

    void DisplayRecords(std::fstream& filestream);
    // fstream 을 통해 처음부터의 내용을 화면에 보여줄 용도

    void ManageRecordsExample();
    // 사용자에게 메뉴얼을 제공할 용도
};
// PrintRecords.cpp

#include <fstream>
#include <iomanip>
#include <limits.h> 
#include "PrintRecords.h"

using namespace std;

namespace samples
{
    Record ReadRecord(istream& stream, bool bPrompt) // 프롬프트에 입력할 내용을 보여주고 사용자가 입력하면 구조체에다가 저장
    {
        Record record;

        if (bPrompt)
        {
            cout << "First Name: ";
        }
        stream >> record.FirstName;
        if (bPrompt)
        {
            cout << "Last Name: ";
        }
        stream >> record.LastName;
        if (bPrompt)
        {
            cout << "Student ID: ";
        }
        stream >> record.StudentID;
        if (bPrompt)
        {
            cout << "Score: ";
        }
        stream >> record.Score;

        return record;
        
        // error 핸들링 미설정으로 입력 중 중단을 하려해도 일단은 계속 읽어들이겠끔 구현
    }

    void DisplayRecords(fstream& fileStream) // fstream 내용을 화면에 제공
    {
        fileStream.seekg(0); // fstream 의 처음부터 읽기

        string line;
        
        while (true)
        {
            getline(fileStream, line); // line 을 통해 한 줄 씩 읽어오기

            if (fileStream.eof()) // EOF 감지시 비트 플래그 원복 후 루프 종료
            {
                fileStream.clear();
                break ;
            }
            cout << line << endl;
        }
    }
    
    void WriteFileRecord(fstream& outputStream, const Record& record) // 읽어온 record 를 파일에 쓰기
    {
        outputStream.seekp(0, ios_base::end); // 여러 번 입력될 것을 염두해서 파일 내용의 끝에서부터 이어서 작성

        outputStream << record.FirstName << " "
            << record.LastName << " "
            << record.StudentID << " "
            << record.Score << endl; // 개행으로 인해 다음 입력은 줄바꿈 상태에서 저장
        outputStream.flush();
    }
    
    void ManageRecordsExample()
    {
        fstream fileStream;
        fileStream.open("studentRecords.dat", ios_base::in | ios_base::out | ios_base::trunc);

        bool bExit = false;
        while (!bExit)
        {
            char command = ' ';

            cout << "a: add" << endl
                << "d: display" << endl
                << "x: exit" << endl;
            cin >> command;					// cin 에 입력된 내용중 command 크기 만큼 command 로 이동
            cin.ignore(LLONG_MAX, '\n');	// cin 에 남은 내용은 삭제
			//즉 입력된 내용의 첫 문자만 유효하겠끔 설정
            
            switch (command)
            {
            case 'a':
            {
                Record record = ReadRecord(cin, true);

                WriteFileRecord(fileStream, record);

                break;
            }
            case 'd':
            {
                DisplayRecords(fileStream);
                break;
            }
            case 'x':
            {
                bExit = true;
                break;
            }
            default:
            {
                cout << "invalid input" << endl;
                break ;
             }
             }
        }
        fileStream.close();
    }
}

'C++ > FOCU_C++' 카테고리의 다른 글

C++ chpt6.  (1) 2025.05.16
C++ chpt4.  (0) 2025.05.03
C++ chtp3.  (0) 2025.05.03
C++ chpt2.  (0) 2025.04.29
C++ chpt1.  (0) 2025.04.28