8장 : 파일 입출력
바이너리 파일 입출력
앞 절까지는 텍스트 파일 중심의 입출력을 다뤘습니다.
이번에는 바이너리(Binary) 파일 입출력을 살펴보겠습니다. 바이너리 파일은 사람이 직접 읽기 어렵지만, 텍스트보다 빠르고 저장 크기도 작아 성능이 중요한 프로그램에서 자주 사용됩니다.
텍스트 모드와 바이너리 모드의 차이
텍스트 모드는 줄바꿈 변환 같은 플랫폼 의존 처리가 개입될 수 있습니다.
바이너리 모드는 데이터를 바이트 그대로 읽고 씁니다.
- 텍스트 모드: 사람이 읽기 쉬움, 가공/파싱 필요
- 바이너리 모드: 사람이 읽기 어려움, 빠르고 원본 바이트 보존
파일을 바이너리 모드로 열 때는 std::ios::binary 플래그를 지정합니다.
바이너리 파일 열기
#include <fstream>
#include <iostream>
int main() {
std::ofstream out("data.bin", std::ios::binary);
if (!out) {
std::cerr << "data.bin 열기 실패\n";
return 1;
}
std::ifstream in("data.bin", std::ios::binary);
if (!in) {
std::cerr << "data.bin 읽기 열기 실패\n";
return 1;
}
}읽기/쓰기 모두 필요하면 std::fstream에 std::ios::in | std::ios::out | std::ios::binary를 조합해서 사용하면 됩니다.
write() / read()로 기본 타입 저장하기
바이너리 입출력의 핵심은 write()와 read()입니다.
write(const char* buffer, std::streamsize size)read(char* buffer, std::streamsize size)
#include <fstream>
#include <iostream>
int main() {
int number = 2025;
double score = 98.75;
{
std::ofstream out("basic.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(&number), sizeof(number));
out.write(reinterpret_cast<const char*>(&score), sizeof(score));
}
int loadedNumber = 0;
double loadedScore = 0.0;
{
std::ifstream in("basic.bin", std::ios::binary);
in.read(reinterpret_cast<char*>(&loadedNumber), sizeof(loadedNumber));
in.read(reinterpret_cast<char*>(&loadedScore), sizeof(loadedScore));
}
std::cout << loadedNumber << ", " << loadedScore << "\n";
}구조체 단위로 저장하기 (POD/Trivially Copyable 중심)
단순 구조체는 한 번에 쓰고 읽을 수 있습니다.
#include <fstream>
#include <iostream>
struct Record {
int id;
double value;
};
int main() {
Record r1{1, 12.34};
{
std::ofstream out("record.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(&r1), sizeof(Record));
}
Record r2{};
{
std::ifstream in("record.bin", std::ios::binary);
in.read(reinterpret_cast<char*>(&r2), sizeof(Record));
}
std::cout << "id=" << r2.id << ", value=" << r2.value << "\n";
}단, 이 방식은 문자열(std::string), 포인터 멤버, 가상 함수가 있는 타입에는 그대로 적용하면 안 됩니다.
파일 포인터 이동: seekg, seekp, tellg, tellp
바이너리 파일은 고정 크기 레코드를 랜덤 접근하기 좋습니다.
#include <fstream>
#include <iostream>
struct Record {
int id;
int score;
};
int main() {
{
std::ofstream out("records.bin", std::ios::binary);
Record arr[3] = {{1, 80}, {2, 90}, {3, 70}};
out.write(reinterpret_cast<const char*>(arr), sizeof(arr));
}
std::ifstream in("records.bin", std::ios::binary);
if (!in) return 1;
// 3번째 레코드(인덱스 2)로 이동
std::streamoff offset = 2 * static_cast<std::streamoff>(sizeof(Record));
in.seekg(offset, std::ios::beg);
Record target{};
in.read(reinterpret_cast<char*>(&target), sizeof(target));
std::cout << "id=" << target.id << ", score=" << target.score << "\n";
}바이너리 입출력 시 주의사항
구조체 패딩(Padding)
컴파일러가 멤버 사이에 패딩 바이트를 넣을 수 있어 sizeof(struct)가 기대와 다를 수 있습니다.
엔디안(Endianness)
다른 플랫폼 간 파일 호환이 필요하면 바이트 순서를 명시적으로 통일해야 합니다.
포인터/동적 메모리 멤버 금지
포인터 값 자체는 메모리 주소이므로 파일에 저장해도 의미가 없습니다.
버전 관리
파일 포맷이 바뀔 수 있으므로 헤더에 버전 필드를 두는 습관이 좋습니다.
바이너리 입출력 점검 요약
- 바이너리 모드는
std::ios::binary로 연다. write/read는 바이트 단위로 동작한다.- 고정 크기 레코드는
seekg/seekp로 빠르게 랜덤 접근할 수 있다. - 구조체 직렬화는 패딩/엔디안/포인터 멤버를 항상 고려해야 한다.