파일 입출력(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 |