게임 데이터들은 굉장히 많다. 유닛의 공격력, 방어력, 체력에서 시작하여 밸런스를 조정하는 숫자들... 그런 걸 프로그램에서 일일이 다루기보다는 엑셀같은 파일로 딱 보는 게 편할 것이다. 그래서 우리는 프로그램과 파일을 연결시키는 방법, 프로그램이 파일의 내용을 읽는 방법, 프로그램이 파일을 생성하고 파일에 기록하는 방법 등을 알아야 한다. 간단한 파일 입출력부터 알아보자.
프로그램이 파일에 입력하려면 다음과 같은 순서를 알아야 한다.
ofstream 객체를 생성하기 위해 <fstream> 라이브러리를 포함시킨다. 대부분의 컴파일러에서는 fstream 라이브러리 안에 iostream이 포함되어 있어서 iostream을 따로 쓰지 않아도 된다. 그 다음에 하나의 파일을 연결시켜준다.
ofstream fout; // fout은 그냥 객체 이름. outFile, cData, bye 따위도 가능.
fout.open("jar.txt"); // fout을 jar.txt와 연결. 만약 jar.txt가 없다? 생성함.
ofstream fout("jar.txt"); // 한 문장으로 쓸 수도 있다.
그 다음부터는 fout을 cout처럼 사용할 수 있다.
fout << "Dull Data";
그리고 실행하면 프로젝트 폴더 안에 파일 생성과 함께 메시지가 들어가있다.
파일 읽는 법도 파일에 입력하는 것과 매우 유사하다.
코드를 보자.
// 두 문장 방법.
ifstream fin; // 객체 생성.
fin.open("jar.txt"); // 파일과 연결.
// 한 문장 방법.
ifstream fin("jar.txt");
char ch;
fin >> ch; // "jar.txt"에서 한 문자를 읽는다.
char buf[80];
fin >> buf; // 파일에서 한 단어를 읽는다.
fin.getline(buf, 80); // 파일에서 한 행을 읽는다.
string str;
getline(fin, str); // 파일로부터 문자열 객체에 읽는다.
프로그램이 종료되면 파일과의 연결은 자동으로 닫힌다. 또는 close() 메서드를 사용하여 명시적으로 파일과의 연결을 닫을 수도 있다.
fout.close();
fin.close();
연결을 닫는다는 건 무슨 의미인가? 예를 들어 다른 파일을 연결시키고 싶을 때는 현재 연결하고 있는 파일과 닫고 다시 열어야 한다. 안 그러면 제대로 작동 안 한다.
파일을 제대로 열지 못할 때도 있다. 그럴 때는 3가지 방법이 있다. ifstream 객체 자체로, fail() 메서드, is_open()메서드를 통해서다.
fin.open("fail.txt");
if (!fin)
{
...
}
if (fin.fail())
{
...
}
if (!fin.is_open())
{
...
}
ifstream 객체는, istream 객체와 같이 bool형으로 변환할 수 있을 때는 변환하기 때문에 검사가 가능하다.
파일은 여러 가지 형태로 열 수 있다. 읽을 것인지, 기록할 것인지, 파일의 뒤에 덧붙일 것인지 등등을 결정하는 방법을 파일 모드라 한다. open() 메서드나 생성자의 제2 매개변수로 결정할 수 있다. 어, 지금까지 안 썼는데요? → 디폴트 값이다. 여러 모드를 보자.
상수 | 의미 |
---|---|
ios_base::in | 파일을 읽기 위해 연다. |
ios base::out | 파일을 쓰기 위해 연다. |
ios base::ate | 파일을 열 때 파일 끝을 찾는다. |
ios base::app | 파일 끝에 덧붙인다. |
ios base::trunc | 파일이 이미 존재하면 파일 내용을 비운다. |
ios base::binary | 2진 파일 |
ifstream의 open 메서드와 생성자는 제2 매개변수로 ios_base::in을 디폴트 매개변수로 사용한다. ofstream의 open 메서드는 ios_base::out | ios_base::trunc을 쓴다. (어쩐지 내용 추가가 안되더라...) |는 비트 or 연산자인데, 다른 포스팅에서 다룰 거라 간단히 얘기하자면, 상수 둘 다 적용한다는 거다.
상수 ios base::binary를 사용하면, 데이터를 2진 파일로 적을 수 있다. 이는 장단점이 있다.
장점
1. 2진 형식이라 숫자를 더 정확하게 저장한다.
2. 데이터 변환이 없어서 처리 속도가 빠르다.
3. 공간을 덜 차지한다.
4. 암호화가 된다.
단점
1. 읽기가 힘들다.
2. 다른 컴파일러로 옮기면 문제 생길 수도 있음.
2진 파일 작성은 이런 식으로 한다.
const int LIM = 20;
struct planet
{
char name[LIM];
double population;
double g;
};
planet pl;
char mars[LIM] = "Mars";
strcpy_s(pl.name, mars);
pl.population = 123456;
pl.g = 12.8;
ofstream fout("jar.txt", ios_base::out | ios_base::app | ios_base::binary);
fout.write((char*)&pl, sizeof(pl));
구조체에 값을 대입하고, write()메서드를 통해 적었다. 제1 매개변수에는 적을 변수, 제2 매개변수에는 그 크기를 적으면 된다. 중요한 것은 제1 매개변수는 char* 형으로 강제 형변환을 해주어야 한다. 왜? 나도 모름. 바이트를 텍스트로 변환하지 않고 적어서 그런가? 어쨌든 그러면 이런 식으로 저장된다.
Mars빼고는 알아보기 힘든 외계어다. 이게 바로 2진 데이터로 저장하기 때문이다. 그럼 이거 어떻게 불러오나? write랑 비슷하게 불러온다.
ifstream fin("jar.txt", ios_base::in | ios_base::binary);
fin.read((char*)&pl, sizeof(pl));
값이 제대로 들어온 것을 확인할 수 있다.
원한다면 파일을 수정할 수 있다. 그리고 원하는 위치에 가서 수정할 수도 있다. 그걸 임의 접근이라고 부른다. 먼저 파일 모드는, 읽기, 쓰기, 2진 데이터가 있으면 된다.
finout.open(file, ios::base::in | ios_base::out | ios_base::binary);
그 다음에 파일 위치에는 어떻게 접근하는가? fstream 클래스의 seekg()와 seekp()를 이용한다. seekg()는 입력 포인트를 주어진 위치로 옮긴다. seekp()는 출력 포인트를 주어진 위치로 옮긴다. char형일 경우 seekg()의 원형은 다음과 같다.
istream& seekg(streamoff, ios_base::seekdir);
istream& seekg(streampos);
첫 번째 원형의 제1 매개변수는 얼마나 떨어져있는지를 뜻하는 오프셋 값이다. 제2 매개변수는 그 기준인데, 3가지의 경우가 있다.
ios_base::seekdir | 의미 |
---|---|
ios_base::beg | 파일의 시작 위치. |
ios base::cur | 파일의 현재 위치. |
ios base::end | 파일의 끝 위치. |
예시는 다음과 같다.
// fin은 ifstream의 객체.
fin.seekg(30, ios_base::beg); // 시작 위치로부터 30바이트.
fin.seekg(-1, ios_base::cur); // 뒤로 1바이트 간다.
fin.seekg(0, ios::end); // 파일의 끝으로 간다.
두 번째 원형의 매개변수는, '시작 바이트0으로부터 얼만큼 떨어져있는지'를 나타내는 오프셋 값이다. 예를 들어,
fin.seekg(112);
이건 파일 포인터를 바이트 112에 위치시킨다. 그 위치는 파일의 113번째 위치일 것이다. 파일의 시작 파일 기준으로 현재 위치를 확인하고 싶다면, 입력 스트림은 tellg(), 출력 스트림은 tellp()를 쓰면 된다.