
File I/O
C++에서의 파일 입출력 클래스는 대표적으로 3가지가 존재한다
이러한 파일 입출력 클래스를 사용하려면 < fstream > 헤더를 포함시켜야 한다
일반적인 입출력 스트림인 cout, cin, cerr, clog는 프로그램 시작 시 바로 사용이 가능하지만 파일 스트림은 프로그래머가 명시적으로 설정해줘야 사용이 가능하다 (인자로 파일명을 넣어주어 file I/O 클래스 객체를 생성해야 함)
이렇게 설정하면 <<, >>로 파일에 데이터를 쓰거나 데이터를 읽어올 수 있으며 close()로 해당 파일을 닫을 수 있다 (혹은 file I/O 클래스 객체가 소멸되면 알아서 소멸자가 파일을 닫아준다)
파일 출력
#include <fstream>
#include <iostream>
int main()
{
std::ofstream outf{ "Kelvin.txt" };
if (!outf)
{
std::cerr << "파일을 열 수 없습니다." << std::endl;
return 1;
}
outf << "This is Name 1 \n";
outf << "This is Name 2 \n";
return 0;
}
이제 해당 프로젝트 dir에 가보면 Kelvin.txt 파일이 생성되고 outf << 로 작성한 텍스트가 남아있는걸 확인할 수 있다
여기서 std::ofstream 클래스 객체는 main() 함수 종료 시 소멸되기 때문에 자동으로 close()가 된다 (RAII)
또한 멤버함수인 put(char)으로 한 글자씩 추가도 가능하다
outf.put('K');
파일 입력
int main()
{
std::ifstream inf{ "Kelvin.txt" };
if (!inf)
{
return 1;
}
std::string strIn{};
while (inf >> strIn)
{
std::cout << strIn << '\n'; //공백을 기준으로 끊어지면서 계속 출력된다
//This, is, Name, 1, This, is, Name, 2
}
return 0;
}
ifsream은 파일의 끝인 EOF에 도달하게 되면 0을 return하기 때문에 조건으로 사용이 가능하다
단 위와 같은 경우는 공백때문에 전부 끊어져서 출력이 되는데 앞서 정리한 방식대로 getline()을 사용하면 한줄 전체를 공백을 무시하여 줄바꿈 문자를 만날때까지 끊어서 읽어올 수 있다
std::string strIn{};
while (std::getline(inf, strIn))
{
std::cout << strIn << '\n';
}
C++에는 buffering이라는게 존재한다, 따라서 file stream으로 출력되는 모든것이 그 즉시 디스크에 작성되지 않을 수 있다
이는 여러 출력 작업을 모아서 한번에 flush하면서 성능상 유리하게 가져가기 위함이다
강제적으로 buffer를 flush하려면 파일을 닫으면 된다, 파일을 닫게 되면 buffer에 있는 내용을 전부 디스크에 기록하고 파일을 닫는다
혹은 ostream::flush()나 출력 스트림에 std::flush를 보내서 수동으로 버퍼를 비울 수 있다
(std::endl도 강제로 buffer flush를 한다, 따라서 과도하게 사용하면 성능에 영향이 갈 수 있다, 따라서 '\n'을 사용한다)
std::cout << "Start long process";
std::cout << std::flush;
std::ofstream inf{ "Kelvin.txt" };
inf.flush();
사실 buffering 때문에 크게 문제가 될 경우는 잘 없지만 가장 유의해야 할 점은 buffer에 데이터가 남아있는 상태에서 프로그램이 즉시 종료될때이다
이 경우 file stream class의 소멸자가 실행되지 않아 파일이 RAII에 의해 자동으로 close()되지 않게되고 이 때문에 buffer flush도 발생하지 않게 된다 이렇게 되면 해당 buffer에 있던 데이터는 실제로 디스크에 작성되지 않게 되어 사라지게 된다
따라서 앞서 정리한대로 exit() 호출 전에 항상 모든 파일을 명시적으로 close() 해주는게 좋은 습관이다
File mode
file stream 클래스의 생성자에는 해당 파일을 어떤 방식으로 열 것인가에 대한 flag를 넘길 수 있다
이러한 flag들은 ios클래스에 존재한다 (std::ios::???)
이때 bit or연산자인 |를 사용하여 여러개의 flag를 동시에 지정할 수 있다
따라서 ifstream의 기본 모드는 std::ios::in이고 ofstream의 기본 모드는 std::ios::out이다, fstream의 기본 모드는 std::ios::in | std::ios::out이다
이렇기 때문에 fstream으로 파일을 열 때 열려는 파일이 존재하지 않으면 실패할 수 있다 (bitwise |이기 때문에 읽기도 되고 쓰기도 되는 상태로 파일을 열으라는 요청임)
따라서 fstream으로 새 파일을 생성해야 한다면 std::ios::out 모드만 사용해야 한다
아래는 플래그 전달 예시이다
std::ofstream outf{ "Kelvin.txt", std::ios::app };
if (!outf)
{
return 1;
}
outf << "Hello, Kelvin!" << '\n';
outf << "This is a test file." << '\n';
기존의 Kelvin.txt 내용에 이어서 두줄이 추가된다
이때 std::ios::trunc로 넘기게 되면 기존 내용은 전부 삭제되고 추가한 두줄만 남게된다
close()와 마찬가지로 open()도 명시적으로 할 수 있다, 이때 파일 스트림 생성자와 마찬가지로 파일명과 flag를 전달할 수 있다
int main()
{
std::ofstream outf{ "Kelvin.txt", std::ios::trunc };
if (!outf)
{
return 1;
}
outf << "Hello, Kelvin!" << '\n';
outf << "This is a test file." << '\n';
outf.close();
outf.open("Kelvin.txt", std::ios::app);
outf << "Appending to the file." << '\n';
return 0;
}
Random file I/O
File stream class에는 파일 내에서 읽기나 쓰기 위치를 추적하는 File Pointer를 가지고 있다
이러한 File Pointer를 이용하여 그 위치에 데이터를 읽거나 쓰는 방식이다, 기본적으로 파일을 읽기/쓰기 모드로 열게 되면 File Pointer는 파일의 시작 부분으로 설정된다
하지만 ios::app 모드로 열게되면 File Pointer는 EOF로 이동하게 된다
지금껏 파일을 읽거나 쓸 때 순차적 접근으로 했지만 특정 부분만 읽거나 쓰기도 가능하다
이는 seekg(), seekp()와 같은 함수로 File Pointer를 조작하면서 처리된다 (g는 get, p는 put임 따라서 seekg()는 읽기 위치 변경, seekp()는 쓰기 위치 변경으로 이해하면 쉽다)
위 함수들은 두개의 인자를 받는데 첫번째 인자는 바로 offset이다 (단위는 byte), File Pointer를 얼마나 이동시킬지를 결정한다 그리고 두번째 인자는 offset의 기준을 정하는 ios 플래그이다
기준을 정하는 ios flag들은 다음과 같다
offset값을 양수로 주면 EOF방향으로 음수로 주면 시작 방향으로 File Pointer가 이동된다
//Kelvin.txt
Hello, Kelvin!
This is a test file.
Appending to the file.
위 txt파일 기준으로 예시를 작성해보자
int main()
{
std::ifstream inf{ "Kelvin.txt" };
if (!inf)
{
return 1;
}
std::string str{};
inf.seekg(3);
std::getline(inf, str);
std::cout << str;
return 0;
}
inf.seekg(3)으로 파일의 시작부분으로 부터 offset 3이 들어갔기 때문에 lo, kelvin!이 나오게 된다
inf.seekg(-5, std::ios::end);
이렇게 하면 끝에서부터 앞으로 5를 이동하기 때문에 file.이 나오게 된다
File Pointer의 절대 위치를 얻으려면 tellg()와 tellp()를 사용하면 된다
inf.seekg(3, std::ios::beg);
std::cout << inf.tellg(); //3
이 함수들을 이용하여 해당 파일의 전체 크기도 알 수 있다 (byte), 맨 뒤로 File Pointer를 옮기고 tellg()로 하면 잘 나온다 (OS에 따라 다른 결과가 나올 수 있음 (문자 표현 방식이 다를 수 있기 때문에)
fstream을 이용한 읽기/쓰기
fstream 클래스는 하나의 파일을 동시에 읽기/쓰기 할 수 있다, 하지만 읽기와 쓰기 모드를 임의로 전환할 수 없다 따라서 한번 읽기나 쓰기 작업이 수행되었으면 seek()과 같은 함수로 File Pointer를 수정해야 모드를 전환할 수 있다 (만약 그대로 있고 싶다면 현재 위치로 seek)
iofile.seekg(iofile.tellg(), std::ios::beg); //현재 위치로 seek
std::fstream iofile{ "Kelvin.txt", std::ios::in | std::ios::out};
if (!iofile)
{
return 1;
}
char ch{};
while (iofile.get(ch)) //읽기
{
switch (ch)
{
case 'K':
iofile.seekg(-1, std::ios::cur); //읽기 -> 쓰기 전환을 위한 seek
iofile << '%'; //쓰기
iofile.seekg(iofile.tellg(), std::ios::beg); //쓰기 -> 읽기 전환을 위한 seek
break;
}
}
return 0;
이렇게 File I/O를 할 때 메모리 주소를 작성하는건 좋지 않다, 왜냐하면 프로세스가 실행될 때 마다 메모리 주소는 달라질 수 있기 때문이다