본문으로 건너뛰기

안동민 개발노트

본문 시작

바이너리 파일 입출력

바이트를 그대로 읽고 쓰는 바이너리 모드를 익히고 기본 타입·구조체 저장과 파일 위치 이동의 제약을 이해합니다.

앞 절까지는 텍스트 파일 중심의 입출력을 다뤘습니다.

이번 절에서는 바이너리(Binary) 파일 입출력을 다룹니다.

바이너리 파일은 사람이 직접 읽기 어렵지만, 텍스트보다 빠르고 저장 크기도 작아 성능이 중요한 프로그램에서 자주 사용됩니다.

HTML 다이어그램: /docs/cpp/ch8/ch8-4/1.html

텍스트 모드와 바이너리 모드의 차이

텍스트 모드는 줄바꿈 변환 같은 플랫폼 의존 처리가 개입될 수 있습니다.

바이너리 모드는 데이터를 바이트 그대로 읽고 씁니다.

  • 텍스트 모드: 사람이 읽기 쉬움, 가공/파싱 필요
  • 바이너리 모드: 사람이 읽기 어려움, 빠르고 원본 바이트 보존

파일을 바이너리 모드로 열 때는 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::fstreamstd::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

바이너리 파일 입출력은 열기 모드, 상태 플래그, 읽기·쓰기 단위, 닫기 책임으로 확인합니다.

HTML 다이어그램: /docs/cpp/ch8/ch8-4/2.html

바이너리 파일은 고정 크기 레코드를 랜덤 접근하기 좋습니다.

랜덤 접근 읽기 예시
#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)

다른 플랫폼 간 파일 호환이 필요하면 바이트 순서를 명시적으로 통일해야 합니다.

포인터/동적 메모리 멤버 금지

포인터 값 자체는 메모리 주소이므로 파일에 저장해도 의미가 없습니다.

버전 관리

파일 포맷이 바뀔 수 있으므로 헤더에 버전 필드를 두는 습관이 좋습니다.


바이너리 입출력 점검 요약

바이너리 파일은 빠르지만 바이트 해석 규칙을 코드가 책임지므로, 저장 포맷의 경계를 명시해야 합니다.

HTML 다이어그램: /docs/cpp/ch8/ch8-4/3.html

아래 보드는 바이너리 저장을 시작하기 전에 포맷을 고정하고, 읽는 쪽이 같은 규칙으로 해석할 수 있는지 확인하는 최소 점검표입니다.

HTML 다이어그램: /docs/cpp/ch8/ch8-4/4.html
  • 바이너리 모드는 std::ios::binary로 연다.
  • write/read는 바이트 단위로 동작한다.
  • 고정 크기 레코드는 seekg/seekp로 빠르게 랜덤 접근할 수 있다.
  • 구조체 직렬화는 패딩/엔디안/포인터 멤버를 항상 고려해야 한다.

결국 바이너리 I/O의 안정성은 파일을 여는 코드보다 파일을 해석하는 계약에서 결정됩니다.

헤더와 버전, 읽은 바이트 수, 구조체 저장 한계를 함께 확인하면 대부분의 오류를 초기에 걸러낼 수 있습니다.