오늘은 C++의 파일 입출력에 대해서 알아보고자 한다.
파일 입출력에서는 fstream이라는 입출력 파일 클래스를 사용한다. 파일 입출력에는 <<, >>, 조정자(manipulator) 등도 사용이 가능하다.
// 읽기 전용으로 파일을 오픈
ifstream fin;
fin.open("helloworld.txt");
// 쓰기 전용으로 파일을 오픈
ofstream fout;
fout.open("helloworld.txt");
// 읽기와 쓰기 전용으로 파일을 오픈
fstream fs;
fs.open("helloworld.txt");
여기서 나온 open() 이라는 메서드는 각 스트림마다 존재한다.
// void open(const char* filename, openmode mode)
fin.open("HelloWorld.txt", ios_base::in | ios_base::binary);
여기서 뒤에있는 in, binary 등을 모드 플래그(mode flag)라고 한다. 파일을 여는 모드를 바꿔줄 때 사용한다. binary의 경우 이진 모드로 열어주는 것을 의미한다. 모드 플래그에는 다음과 같은 값들이 있다. 조합을 섞어서 사용할 수는 있지만, 모든 조합이 다 되는 것은 아니다.
- in : 읽기 위한 파일
- out : 쓰기 위한 파일
- ate : 파일의 끝에 위치
- app : 모든 파일의 출력은 끝에 추가
- trunc : 존재하는 파일을 지움
- binary : 이진 모드
파일을 열고 읽고 쓰는 작업을 해 주었다면, 나중에 다시 써 주기 위해 닫아야 한다. 버퍼에서 차지하고 있는 것을 버리고 파일을 닫아주는 close() 메서드를 사용할 수 있다. close() 메서드 역시 각 스트림마다 존재한다.
ifstream fin;
// ...
fin.close();
또한 파일이 열려있는지를 확인하는 메서드로 is_open()이라는 메서드를 사용할 수도 있다.
fstream fs;
fs.open("HelloWorld.txt");
if (fs.is_open())
{
// ...
}
파일에서 문자를 한 글자씩 또는 한 줄씩 읽는 코드는 다음과 같이 작성해 볼 수 있다. 참고로 여기서 fail() 메서드는 입력값이 해당 타입과 맞지 않은 경우 true를 반환하는 메서드이고, eof() 메서드는 데이터의 마지막 값을 입력받았을 때 true를 반환하는 메서드이다.
// 파일에서 문자 하나씩 읽기
ifstream fin;
fin.open("HelloWorld.txt");
char character;
while (true)
{
fin.get(character);
if (fin.fail())
{
break;
}
cout << character;
}
fin.close();
// 파일에서 문자 한 줄씩 읽기
ifstream fin;
fin.open("HelloWorld.txt");
string line;
while (!fin.eof())
{
getline(fin, line);
cout << line << endl;
}
fin.close();
문자를 한 단어씩 읽어줄 수도 있다. 만약 파일에서 숫자들을 받아서 읽어온다면 단순하게 생각했을 때 다음과 같이 코드를 작성할 수 있을 것이다.
// 파일에서 문자 한 단어씩 읽기
ifstream fin;
fin.open("HelloWorld.txt");
int number;
while (!fin.eof())
{
fin >> number;
cout << number << endl;
}
fin.close();
하지만 이렇게 읽으면 문제가 발생할 수가 있다. 예를 들어 HelloWorld.txt 파일에 들어간 값이 "123 456 789" 이렇게 되어 있다면 아래 첫 번째 처럼 값이 들어있을 것이고 문제가 되지 않는다.
하지만 두 번째 처럼 "123 456 789 " 이 들어가 있게 된다면 이야기가 달라진다. 789까지 출력을 하고 fin.eof()가 false이기 때문에 한 번 더 while 문을 돌면서 789를 출력한다. 즉, 789를 두 번 출력하는 것이다.
그리고 숫자만 받아야 하는 경우 중간에 아래 첫 번째 예제처럼 문자가 끼게 되면 이 경우는 123을 무한하게 출력하며 (fin.eof()는 항상 false일 것이기 때문) 무한루프에 빠지게 된다.
첫 번째 예제의 경우 다음과 같이 number가 아닐 경우 기존에 받은 값을 지우고 다음 ' '까지 무시하는 식으로 코드를 진행할 수도 있다.
ifstream fin;
fin.open("HelloWorld.txt");
int number;
while (!fin.eof())
{
fin >> number;
if (fin.fail())
{
fin.clear();
fin.ignore(LLONG_MAX, ' ');
}
else
{
cout << number << endl;
}
}
fin.close();
하지만 이러한 코드는 위 그림 두 번째 예제와 같이 파일이 들어온다면 역시 에러를 발생한다. ' ' 공백은 구분할 수 있지만 '\t' 탭은 구분할 수 없기 때문이다. 이렇게 여러가지 엣지 케이스에 걸리지 않으면서 숫자를 잘 읽을 수 있는 코드는 아래와 같이 짜볼 수 있다. 숫자가 아닌 다른 값이 들어오면 trash로 읽어서 버려버릴 수가 있게 된다.
ifstream fin;
fin.open("HelloWorld.txt");
int number;
string trash;
while (true)
{
fin >> number;
if (!fin.fail())
{
cout << number << endl;
continue;
}
if (fin.eof())
{
break;
}
fin.clear();
fin >> trash;
}
fin.close();
파일에 값을 쓰는 것도 비슷하게 해볼 수 있다. 한 줄씩 값을 입력받고 파일에 저장하는 코드는 다음과 같다.
ofstream fout;
fout.open("HelloWorld.txt");
string line;
getline(cin, line);
if (!cin.fail())
{
fout << line << endl;
}
fout.close();
바이너리 파일을 읽는 경우는 조금 다르게 접근한다. 아래 예제에서 read() 메서드는 메모리에서 record의 주소값을 받아서 거기서부터 record의 크기만큼 메모리를 할당한다.
ifstream fin("test.dat", ios_base::in | ios_base::binary);
if (fin.is_open())
{
Record record;
fin.read((char*)&record, sizeof(Record));
}
fin.close();
바이너리 파일에 값을 쓰는 방식은 아래와 같다.
ofstream fout("test.dat", ios_base::out | ios_base::binary);
if (fin.is_open())
{
char buffer[20] = "Wonjong Oh";
fout.write(buffer, 20);
}
fout.close();
파일 안에서 값을 탐색해야 할 때도 있다. 파일의 중간부터 값을 덮어쓰고 싶은 경우 다음처럼 seekp() 메서드를 사용하면 된다. 탐색은 절대적 또는 상대적 위치로 갈 수 있는데 seekp()에서 하나의 파라미터를 가지면 절대적 위치, 두 개의 파라미터를 가지면 상대적 위치로 이동한다.
fstream fs("test.dat", ios_base::in | ios_base::out | ios_base::binary);
if (fs.is_open())
{
fs.seekp(20, ios_base::beg);
if (!fs.fail())
{
// 21번째부터 덮어쓰기
}
}
fs.close();
참고자료
- C++ 언매니지드 프로그래밍, 포큐 아카데미
'Prog. Langs & Tools > C++' 카테고리의 다른 글
[C++] Ch06. OOP 3 (0) | 2020.10.19 |
---|---|
[C++] Ch05. OOP 2 (0) | 2020.10.01 |
[C++] Ch04. OOP 1 (0) | 2020.09.22 |
[C++] Ch02. 참조, 문자열 (0) | 2020.08.11 |
[C++] Ch01. 입출력(I/O) (0) | 2020.08.03 |