목차

C++ 스터디 #13: 파일처리

시간 2021년 8월 10일 화요일 20:00 ~ 22:00
장소 ZOOM
참가자 -

출처: 천인국, “어서와 C++는 처음이지!”, INFINITY BOOKS

1. 파일 입출력

1.1. 개요

입력과 출력은 프로그램에서 몹시 중요하다. 만약 프로그래머가 입력 장치나 출력 장치의 종류에 따라서 서로 다르게 프로그램을 작성해야 한다면 많이 불편할 것이다. 예를 들어서 모니터 화면에 출력하는 것과 프린터에 출력하는 방법이 다르면 서로 다른 프로그램을 작성해야한다.
C++에서는 이 문제를 해결하기 위해서 스트림(stream)이란 개념을 사용하고 있다. C++에서는 입력과 출력은 모두 스트림으로 이루어진다. 스트림이란 입력과 출력을 바이트(byte)의 흐름으로 생각하는 것이다. 프로그램 밖으로 스트림이 흘러가면 출력 스트림이고, 밖에서 프로그램 안으로 흘러들어 오면 입력 스트림이다. 즉, 스트림을 바이트들이 떠다니는 시냇물이라고 생각하면 된다.

스트림의 최대 장점은 장치 독립성이다. 스트림을 사용하면 입출력 장치에 상관없이 프로그램을 작성할 수 있다. 우리는 이미 스트림을 사용했었는데 그것은 바로 cin과 cout이다. cin이 키보드와 연결된 입력 스트림이었고, cout이 출력을 위해 콘솔과 연결된 출력 스트림인 것이다.

클래스 설명
ofstream 출력 파일 스트림 클래스이다. 출력 파일을 생성하고 파일에 데이터를 쓸 때 사용한다.
ifstream 입력 파일 스트림 클래스이다. 파일에서 데이터를 읽을 때 사용한다.
fstream 일반적인 파일 스트림을 나타낸다.

C++에서 파일 처리를 수행할 때는 <iostream>과 <fstream> 헤더 파일을 포함시켜야 한다.

1.2. 파일 쓰기

파일에 입출력하는 것도 스트림을 통하여 이루어진다. 파일에 데이터를 쓸 때, 사용되는 스크림은 클래스 ofstream의 객체이다. 먼저 객체를 나타내는 변수를 선언한 후에 이 변수를 파일과 연결하면 된다. 파일과 연결할 때 두가지 방법이 있다. open() 멤버 함수를 사용하거나 생성자를 사용하는 것이다.

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ofstream os{ "numbers.txt" };
	if (!os)
	{
		cerr << "파일 오픈에 실패하였습니다" << endl;
		exit(1);
	}
	for (int i = 0; i < 100; i++)
		os << i << " ";

	return 0;

	// 객체 os가 범위를 벗어나면 ofstream 소멸자가 파일을 닫는다. 
}
스트림이 파일과 연결되면 »나 « 연산자로 입출력이 가능하다. 파일을 열 때 생성자를 사용하면 객체가 범위를 벗어날 때 자동적으로 소멸되기 때문에 open()이나 close() 함수를 사용할 필요가 없다.
ofstream을 이용하여 파일과 연결할 때 만약 그 파일이 존재하지 않으면 파일을 생성하고 그 파일에 데이터가 입력된다. 만약 파일이 이미 존재하면 그 파일 위에 덮어지기 때문에 원래 있던 데이터는 사라지고, 우리가 새로 입력한 데이터만 남는다.

1.3. 파일 읽기

파일을 통하여 입력할 때 사용되는 입력 스트림은 ifstream의 객체이다.

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream is{ "numbers.txt" };
	if (!is)
	{
		cerr << "파일 오픈에 실패하였습니다" << endl;
		exit(1);
	}
	int number;
	while (is) {
		is >> number;
		cout << number << " ";
	}
	cout << endl;
	return 0;

 // 객체 is가 범위를 벗어나면 ifstream 소멸자가 파일을 닫는다. 
}
ifstream을 통해 파일을 읽을 때, ofstream과는 다르게 연결될 파일이 이미 생성되어 있어야만 한다.

1.4. 파일 모드

읽기 전용으로 파일을 열고 싶거나, 파일에 내용을 덮어씌우는 것이 아니라 끝에 내용을 추가하고 싶은 경우 등등 파일을 열 때 어떤 파일 모드로 열지 결정할 수 있다. 아래는 파일 모드 예시이고 이외에도 많은 모드들이 있다.

파일 모드 설명
ios::in 입력을 위하여 파일을 연다.
ios::out 출력을 위하여 파일을 연다.
ios::binary 이진 파일 입출력을 위하여 파일을 연다.
ios::app 파일 끝에 추가된다.

읽기 전용으로 파일을 열고 싶으면 다음과 같이 사용하면 된다.

ifstream is(“numbers.txt”, ios::in);

| 연산자를 사용해 여러 파일 모드를 이용할 수 있다.

ofstream os(“example.bin”, ios::out | ios::app | ios::binary);

출력 파일을 추가 모드(ios::app)로 열어보자.

#include <iostream>
#include <fstream>

int main()
{
	using namespace std;

	ofstream os("appmode.txt", ios::app);
	if (!os)
	{
		cerr << "파일 오픈에 실패하였습니다" << endl;
		exit(1);
	}

	os << "추가되는 줄 #1" << endl;
	os << "추가되는 줄 #2" << endl;

	return 0;
}

1.5. 멤버 함수 이용하기

입출력 연산자 «와 »를 이용하여 데이터를 입출력할 수 있지마, fstream의 멤버 함수를 사용해서 파일에 입출력할 수도 있다. get()으로 하나의 문자를 읽거나, put()으로 하나의 문자를 출력할 수 있다. 파일의 끝에 도달하면 true를 출력하는 함수 eof()도 있다.

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream is{ "numbers.txt" };
	if (!is)
	{
		cerr << "파일 오픈에 실패하였습니다" << endl;
		exit(1);
	}
	char c;
	is.get(c);
	while (!is.eof())
	{
		cout << c;
		is.get(c);
	}
	cout << endl;
	return 0;
}
키보드에서 입력받은 문자들을 파일에 저장하는 프로그램을 멤버 함수를 이용해 만들 수 있다. 콘솔에서 Ctrl+Z 또는 Ctrl+C를 누르면 파일이 끝난다.
#include <iostream>
#include <fstream> 
using namespace std;

int main()
{
	ofstream os("test.txt");;
	char c;
	while (cin.get(c))
	{
		os.put(c);
	}
	return 0;
}

1.6. 파일 입출력 예제

1.6.1. 파일 입출력 예제(1)

다음과 같이 온도가 저장된 파일이 있다.

파일 입력을 통해 이 파일을 읽어서 화면에 다음과 같이 표시하는 프로그램을 작성할 수 있다.

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream is{ "temp.txt" };
	if (!is)
	{
		cerr << "파일 오픈에 실패하였습니다" << endl;
		exit(1);
	}

	int hour;
	double temperature;

	while (is >> hour >> temperature) {
		cout << hour << "시: 온도 " << temperature << endl;
	}
	return 0;
}

1.6.2. 파일 입출력 예제(2)

다음과 같은 내용이 저장된 텍스트 파일을 읽은 후 출력 파일에 다음과 같이 기록하는 프로그램을 만들 수 있다.

#include <iostream>
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream is("scores.txt");
	ofstream os("result.txt");
	if (is.fail()) {
		cerr << "파일 오픈 실패" << endl;
		exit(1);
	}
	if (os.fail()) {
		cerr << "파일 오픈 실패" << endl;
		exit(1);
	}
	char c;
	int line_number = 1;
	is.get(c);
	os << line_number << ": ";
	while (!is.eof())
	{
		os << c;
		if (c == '\n')
		{
			line_number++;
			os << line_number << ": ";
		}
		is.get(c);
	}
	return 0;
}

2. 이진 파일

2.1. 개요

C++에서는 텍스트 파일과 이진 파일 이 두가지 파일 유형을 제공한다. 텍스트 파일에는 문자들이 들어있고 이 문자들은 아스키 코드를 이용하여 표현된다. 텍스츠 파일이 중요한 이유는 모니터, 키보드, 프린터 등이 모두 문자 데이터만을 처리하기 때문이다.
이진 파일은 사람이 읽을 수는 없으나 컴퓨터는 읽을 수 있는 파일이다. 즉 문자 데이터가 아니라 이진 데이터가 직접 저장되어 있는 파일이다. 이진 파일의 예시로는 실행 파일, 사운드 파일, 이미지 파일 등이 있다.
텍스트 파일에서는 모든 정보가 문자열로 변환되어서 파일에 기록된다. 정수값도 을 통하여 문자열로 변환된 후 파일에 쓰인다. «과 »으로 123456이 입출력되면 ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’으로 변환되어 입력되고 출력된다. 즉 하나당 4바이트의 공간을 차지한다.(총 24바이트)
반면에 이진 파일은 사123456이 컴퓨터에서 4바이트로 표현되고 이 4바이트가 파일에 써진다.(123456→ 00000000 00000001 11100010 01000000) 이렇듯 이진 파일을 사용하면 컴퓨터가 텍스트 파일을 읽기 위해 변환하는 과정이 필요 없고 이진 파일이 텍스트 파일보다 차지하는 공간이 더 적다.
이진 파일의 단점은 인간이 파일의 내용을 확인하기 힘들다는 점과, 컴퓨터 시스템에 따라 데이터를 처리하는 방식이 다를 수 있기 때문에 이식성이 떨어진다.

2.2. 이진 파일 입출력

이진 파일을 입출력 하려면 파일 모드를 ios::binary를 주어서 파일을 열어야 한다. write() 함수로 변수의 값을 이진 모드로 파일에 쓰고, 읽을 때는 read() 함수를 이용한다.

ofstream os(“test.dat”, ios::binary);
int x=5;
os.write((char*) &x, sizeof(int));

2.2.1 이진 파일 입력

다음은 int형 배열을 기록하는 예제이다.

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	int buffer[] = { 10, 20, 30, 40, 50 };
	ofstream os{ "test.dat", ios::binary };
	if (!os)
	{
		cout << "test.txt 파일을 열 수 없습니다." << endl;
		exit(1);
	}
	os.write((char *)&buffer, sizeof(buffer));
	return 0;
}

2.2.2 이진 파일 출력

다음은 위에서 만들어진 이진 파일을 다시 읽는 예제이다. 이진 파일을 읽을 때는 파일에 들어 있는 데이터들의 정확한 순서를 알고 있어야 한다.

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	int buffer[5];
	ifstream is{ "test.dat", ios::binary };
	if (!is)
	{
		cout << "test.txt 파일을 열 수 없습니다." << endl;
		exit(1);
	}
	is.read((char *)&buffer, sizeof(buffer));
	for(auto& e: buffer)
		cout << e << " ";
	return 0;
}

3. 임의 접근 파일

3.1. 개요

지금까지의 파일 입출력 방법은 모두 데이터를 처음부터 순차적으로 읽는 순차 접근 방법을 다루었다. 하지만 이 방법은 한번 읽은 데이터를 다시 읽으려면 현재의 파일을 닫고 다시 열어서 앞에서부터 읽는 작업을 해야 한다. 이런 귀찮을 일을 하지 않게 앞부분을 읽지 않고 중간이나 마지막으로 건너뛸 수 있는 임의 접근 방법 또한 존재한다.

모든 파일에는 파일 위치 표시자라는 것이 존재한다. 새 파일이 만들어 지게 되면 파일 위피 표시자의 값이 0이고 이것은 파일의 시작 부분을 나타낸다. 기존 파일의 경우 추가 모드(ios::app)로 열리면 표시자는 파일의 끝이 되고, 다른 모드인 경우 파일의 시작 부분을 가르킨다.
임의 접근 방법은 이 위치 표시자를 조작하여 우리가 원하는 곳부터 파일을 읽는 방법인 것이다. 위치 표시자를 조작하는 함수는 seekg()이다.

seekg(long offset, seekdir way);

여기서 두 번째 매개 변수인 way는 다음과 값을 가질 수 있다.

way 설명
ios::beg 처음부터의 offset
ios::cur 현재 위치부터의 offset
ios::end 파일 끝에서부터의 offset

파일의 처음 위치부터 100바이트 떨어진 곳으로 이동하려면 다음과 같이 하면 된다.

is.seekg(100, ios::beg);

파일의 끝으로 이동하려면 다음과 같이 하면 된다.

is.seekg(0, ios::end);

현재 파일 위치 표시자의 값을 얻으려면 tellg()를 사용하면된다.

is.tellg();

3.2. 예제

3.2.1 예제(1)

10개의 난수를 저장한 이진 파일을 만들고 파일의 크기를 알아낸 다음에 파일의 중간으로 파일 위티 표시자를 이동시켜서 그 위치에 있는 난수를 읽어오는 프로그램을 작성해 보자.

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;
const int SIZE = 10;

int main()
{
	srand(time(NULL));
	int data;
	// 이진 파일을 쓰기 모드로 연다.
	ofstream os{ "test.dat", ios::binary };
	if (!os)
	{
		cout << "test.dat 파일을 열 수 없습니다." << endl;
		exit(1);
	}
	for (int i = 0; i < SIZE; i++)
	{
		data = rand();
		cout << data << " ";
		os.write((char*)&data, sizeof(data));
	}
	os.close();

	// 이진 파일을 읽기 모드로 연다.
	ifstream is{ "test.dat", ios::binary };
	if (!is)
	{
		cout << "test.dat 파일을 열 수 없습니다." << endl;
		exit(1);
	}
	// 파일 크기를 알아낸다. 
	is.seekg(0, ios::end);
	long size = is.tellg();
	cout << endl << "파일 크기는 " << size << endl;

	// 파일의 중앙으로 위치 표시자를 이동시킨다. 
	is.seekg(size / 2, ios::beg);
	is.read((char*)&data, sizeof(int));
	cout << "중앙위치의 값은 " << data << endl;
	return 0;
}

3.2.2 예제(2)

fstream 클래스는 동시에 파일을 읽고 쓸 수 있다. 하지만 임의로 읽기와 쓰기 사이로 전환할 수 없다는 것을 주의해야한다. 읽기와 쓰기를 전환하는 유일한 방법은 파일 위치 표시자를 수정하는 작업을 수행해야만 한다. 만약 위치 표시자를 수정하지만 이동하고 싶진 않을 때는 다음과 같이 현재 위치로 재설정해주면 된다.

iofile.seekg(iofile.tellg(), ios::beg);

fstream을 이용해 파일 안의 소문자 모음을 모두 ‘*’ 기호로 바꾸는 프로그램을 작성해보자.


#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	fstream iofile("words.txt", ios::in | ios::out);
	if (!iofile) {
		cout << "words.txt 파일을 열 수 없습니다." << endl;
		exit(1);
	}

	char ch; 
	while (iofile.get(ch)) {
		switch (ch)	{
		case 'a':
		case 'e':
		case 'i':
		case 'o':
		case 'u':
			// 한 글자 앞으로 간다.
			iofile.seekg(-1, ios::cur);

			// 쓰기 모드로 바꾸어서 모음 위치에 ‘*’을 쓴다. 
			iofile << '*';

			// 다시 읽기 모드로 바꾼다.
			iofile.seekg(iofile.tellg(), ios::beg);
			break;
		}
	}
	return 0;
}

4. 연습 문제

4.1. 텍스트 파일 안의 모든 정수의 합 계산

다음과 같은 text 파일이 있다. 이 text 파일 안의 모든 정수의 합을 계산하는 프로그램을 작성하세요.

⦁결과

45


⦁예시 답안

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream is("sample1.txt");
	if (!is)
	{
		cerr << "파일 오픈에 실패하였습니다." << endl;
		exit(1);
	}

	int num;
	int sum = 0;

	while (is)
	{
		is >> num;
		sum += num;
		num = 0;
	}
	cout << sum << endl;

	return 0;
}

4.2. 텍스트 파일 거꾸로 출력하기

텍스트 파일에서 seekg()와 tellg()를 이용하여 순서를 반대로 하여 문자를 읽고 화면에 출력하는 프로그램을 작성하기.
⦁프로그램 동작 예시

⦁예시 답안

#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream is("sample2.txt");
	if (!is)
	{
		cerr << "파일 오픈에 실패하였습니다." << endl;
		exit(1);
	}

	is.seekg(0, ios::end);
	int size = is.tellg();
	char c;

	for (int i = size-1; i >= 0; i--)
	{
		is.seekg(i, ios::beg);
		is.get(c);
		cout << c;

	}
	
	return 0;
}

4.3. 특정 단어 검색하기

파일에서 특정한 단어를 찾아서 파일 이름과 단어가 위치한 라인 번호를 출력하는 프로그램 작성하기.

Tip. getline(stream 객체 변수, 저장할 문자열 변수)으로 한 줄을 읽어서 string 클래스가 가지고 있는 find() 함수를 사용한다. s.find(검색할 단어)는 검색할 단어가 문장에 없으면 –1을 반환한다.\\

⦁사용되는 text 파일

⦁프로그램 동작 예시

⦁예시 답안

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
	ifstream is("sample3.txt");
	if (!is)
	{
		cerr << "파일 오픈에 실패하였습니다." << endl;
		exit(1);
	}

	string word, str;
	cout << "검색할 단어: ";
	cin >> word;
	int line_num = 1;

	while (!is.eof())
	{
		getline(is, str);
		if (str.find(word) != -1)
		{
			cout << line_num << ": " << str << endl;
		}
		++line_num;
	}

	return 0;
}