Skip to content

64. 파일 구조체


1. 키워드

  • 파일 구조체


2. 파일에서 구조체를 읽고 쓰기

  • 컴퓨터에서 파일은 메모장이나 기타 텍스트 편집기로 열 수 있는 텍스트 파일과 특정 프로그램에서만 열 수 있는 바이너리 파일로 나눌 수 있다.
  • 특히 우리가 자주 쓰는 워드(.docx), 파워포인트(.pptx), 엑셀(.xlsx), PDF(.pdf), 그림 파일(.jpg, .png, .gif, .bmp)은 바이너리 형식으로 되어 있다.


  • 만약 100, 200, 300, 400이라는 정보를 바이너리 형식과 텍스트 형식으로 저장한다면 다음과 같은 모양이 된다.


001


  • 숫자를 저장한다면 바이너리 형식은 숫자를 그대로 저장하지만, 텍스트 형식은 숫자를 문자열 형태(ASCII)로 저장한다.
  • 따라서 바이너리 형식은 같은 정보를 저장하더라도 텍스트 형식보다 차지하는 공간이 적고, 처리 속도가 빠르다.
  • 이번에는 구조체를 활용하여 바이너리 파일을 처리하는 방법을 알아보자.


3. 파일에 구조체 쓰기

  • 파일에 구조체의 내용을 쓰려면 fwrite 함수를 사용한다.


fwrite(버퍼, 쓰기크기, 쓰기횟수, 파일포인터);
성공한 쓰기 횟수를 반환, 실패하면 지정된 쓰기 횟수보다 작은 값을 반환


  • 이제 100, 200, 300, 400을 바이너리 형식을 저장해 보자.


#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>              // fopen, fwrite, fclose 함수가 선언된 헤더 파일

#pragma pack(push, 1) // 1바이트 크기로 정렬
struct Data
{
    short num1; // 2바이트
    short num2; // 2바이트
    short num3; // 2바이트
    short num4; // 2바이트
};
#pragma pack(pop) // 정렬 설정을 이전 상태(기본값)로 되돌림

int main()
{
    struct Data d1;

    d1.num1 = 100;
    d1.num2 = 200;
    d1.num3 = 300;
    d1.num4 = 400;

    FILE *fp = fopen("data.bin", "wb"); // 파일을 쓰기/바이너리 모드(wb)로 열기

    fwrite(&d1, sizeof(d1), 1, fp); // 구조체의 내용을 파일에 저장

    fclose(fp); // 파일 포인터 닫기

    return 0;
}


  • 리눅스나 OSX에서 data.bin 파일의 내용을 보려면 xxd 명령어를 사용한다.


xxd data.bin
00000000: 6400 c800 2c01 9001                      d...,...


  • 먼저 파일에 저장할 구조체를 정의한다.
  • 여기서는 100, 200, 300, 400을 저장할 것이므로 short 타입 멤버 4개를 넣는다.
  • 그리고 각 멤버의 크기 그대로 파일에 저장할 수 있도록 구조체를 1바이트 크기로 정렬한다.
  • 사실 여기서는 가장 큰 자료형이 short 타입이고 크기가 2의 배수라 정렬을 안 해도 크기 그대로 저장된다.


#pragma pack(push, 1) // 1바이트 크기로 정렬
struct Data
{
    short num1; // 2바이트
    short num2; // 2바이트
    short num3; // 2바이트
    short num4; // 2바이트
};
#pragma pack(pop) // 정렬 설정을 이전 상태(기본값)로 되돌림


  • 만약 GCC 버전이 4.0 미만이라면 #pragma pack(push, 1), #pragma pack(pop) 대신 __attribute__((aligned(1), packed))로 정렬을 해준다.


struct Data
{
    short num1; // 2바이트
    short num2; // 2바이트
    short num3; // 2바이트
    short num4; // 2바이트
} __attribute__((aligned(1), packed)); // GCC 4.0 미만: 1바이트 크기로 정렬


  • Data 구조체로 변수 d1을 선언한 뒤 각 멤버에 100, 200, 300, 400을 저장한다.


struct Data d1;

d1.num1 = 100;
d1.num2 = 200;
d1.num3 = 300;
d1.num4 = 400;


  • 다음과 같이 fopen 함수에 파일 모드를 "wb"로 지정하여 파일을 쓰기/바이너리 모드(wb)로 연다.


FILE *fp = fopen("data.bin", "wb"); // 파일을 쓰기/바이너리 모드(wb)로 열기


  • 이제 fwrite 함수를 사용하여 구조체 변수 d1을 파일에 저장한다.
  • fwrite 함수에는 값의 메모리 주소를 넣어야 하므로 &d1과 같이 변수의 주소를 넣어준다.
  • 이때 동적 메모리를 할당한 포인터도 가능하다.
  • 그리고 쓰기 크기는 구조체의 크기를 구해서 넣고, 쓰기 횟수는 1을 넣는다.
  • 마지막에는 파일 포인터 fp를 넣어준다.


fwrite(&d1, sizeof(d1), 1, fp); // 구조체의 내용을 파일에 저장


  • 파일 쓰기가 끝났다면 fclose 함수로 파일 포인터를 닫는다.


fclose(fp); // 파일 포인터 닫기


  • 구조체 변수 d1data.bin 파일에 저장한 모습을 그림으로 표현하면 다음과 같은 모양이 된다.
  • x86 플랫폼에서는 정수가 리틀 엔디언으로 저장되므로 0x6464 00이 된다.


002


  • 이번에는 구조체에서 각 멤버의 크기를 다양하게 만들어서 파일에 써보자.


#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>              // fopen, fwrite, fclose 함수가 선언된 헤더 파일
#include <string.h>             // strcpy, memset 함수가 선언된 헤더 파일

#pragma pack(push, 1) //  1바이트 크기로 정렬
struct Data
{
    char c1;     //  1바이트
    short num1;  //  2바이트
    int num2;    //  4바이트
    char s1[20]; // 20바이트
};
#pragma pack(pop) // 정렬 설정을 이전 상태(기본값)로 되돌림

int main()
{
    struct Data d1;
    memset(&d1, 0, sizeof(d1)); // 구조체 변수의 내용을 0으로 초기화

    d1.c1 = 'a';                    // 문자 저장
    d1.num1 = 32100;                // 2바이트 크기의 숫자 저장
    d1.num2 = 2100000100;           // 4바이트 크기의 숫자 저장
    strcpy(d1.s1, "Hello, world!"); // 문자열 저장

    FILE *fp = fopen("data2.bin", "wb"); // 파일을 쓰기/바이너리 모드(wb)로 열기

    fwrite(&d1, sizeof(d1), 1, fp); // 구조체의 내용을 파일에 저장

    fclose(fp); // 파일 포인터 닫기

    return 0;
}


  • 리눅스나 OSX에서 data2.bin 파일의 내용을 보려면 xxd 명령어를 사용한다.


xxd data2.bin
00000000: 6164 7d64 752b 7d48 656c 6c6f 2c20 776f  ad}du+}Hello, wo
00000010: 726c 6421 0000 0000 0000 00              rld!.......


  • 파일에 저장할 구조체를 보면 1바이트 크기의 char 타입, 2바이트 크기의 short 타입, 4바이트 크기의 int 타입, 20바이트 크기의 char 타입 배열이 멤버로 들어있다.
  • 여기서 int 타입을 기준으로 구조체 정렬이 되면 파일에 썼을 때도 char c1;은 실제 크기보다 큰 공간을 차지하게 되므로 반드시 1바이트 크기로 정렬을 해준다.


#pragma pack(push, 1) //  1바이트 크기로 정렬
struct Data
{
    char c1;     //  1바이트
    short num1;  //  2바이트
    int num2;    //  4바이트
    char s1[20]; // 20바이트
};
#pragma pack(pop) // 정렬 설정을 이전 상태(기본값)로 되돌림


  • 이제 구조체 변수를 선언한 뒤 각 멤버에 값을 저장한다.
  • 이때 구조체 변수는 반드시 memset 함수를 사용하여 0으로 초기화해 준다.
  • 만약 0으로 초기화하지 않으면 배열 s1 부분에는 이전에 메모리에서 쓰던 값이 들어갈 수 있다.


struct Data d1;
memset(&d1, 0, sizeof(d1)); // 구조체 변수의 내용을 0으로 초기화

d1.c1 = 'a';                    // 문자 저장
d1.num1 = 32100;                // 2바이트 크기의 숫자 저장
d1.num2 = 2100000100;           // 4바이트 크기의 숫자 저장
strcpy(d1.s1, "Hello, world!"); // 문자열 저장


  • 이제 fopen 함수로 파일을 쓰기/바이너리 모드(wb)로 열고, fwrite 함수로 구조체의 내용을 파일에 쓴다.


FILE *fp = fopen("data2.bin", "wb"); // 파일을 쓰기/바이너리 모드(wb)로 열기

fwrite(&d1, sizeof(d1), 1, fp); // 구조체의 내용을 파일에 저장


  • 파일 쓰기가 끝났다면 fclose 함수로 파일 포인터를 닫는다.


fclose(fp); // 파일 포인터 닫기


  • 구조체 변수 d1data2.bin 파일에 저장한 모습을 그림으로 표현하면 다음과 같다.
  • x86 플랫폼에서는 정수가 리틀 엔디언으로 저장된다.


003


4. 파일에서 구조체 읽기

  • 파일에서 구조체를 읽으려면 fread 함수를 사용한다.


fread(버퍼, 읽기크기, 읽기횟수, 파일포인터);
성공한 읽기 횟수를 반환, 실패하면 지정된 읽기 횟수보다 작은 값을 반환
#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>              // fopen, fread, fclose 함수가 선언된 헤더 파일

#pragma pack(push, 1) // 1바이트 크기로 정렬
struct Data
{
    char c1;     //  1바이트
    short num1;  //  2바이트
    int num2;    //  4바이트
    char s1[20]; // 20바이트
};
#pragma pack(pop) // 정렬 설정을 이전 상태(기본값)로 되돌림

int main()
{
    struct Data d1;

    FILE *fp = fopen("data2.bin", "rb"); // 파일을 읽기/바이너리 모드(rb)로 열기

    fread(&d1, sizeof(d1), 1, fp); // 파일의 내용을 읽어서 구조체 변수에 저장

    printf("%c %d %d %s\n", d1.c1, d1.num1, d1.num2, d1.s1);

    fclose(fp); // 파일 포인터 닫기

    return 0;
}

// a 32100 2100000100 Hello, world!


  • 먼저 파일에서 읽은 내용을 저장할 구조체를 정의한다.
  • 앞에서 data2.bin 파일에 1바이트 크기의 char 타입, 2바이트 크기의 short 타입, 4바이트 크기의 int 타입, 20바이트 크기의 char 타입 배열에 값을 넣어서 저장했으므로 파일에서 값을 읽을 때도 똑같은 구조체를 만들어준다.
  • 또한, 구조체의 각 멤버 크기 그대로 읽을 수 있도록 1바이트 크기로 정렬한다.


#pragma pack(push, 1) // 1바이트 크기로 정렬
struct Data
{
    char c1;     //  1바이트
    short num1;  //  2바이트
    int num2;    //  4바이트
    char s1[20]; // 20바이트
};
#pragma pack(pop) // 정렬 설정을 이전 상태(기본값)로 되돌림


  • 바이너리 파일을 읽어서 구조체에 저장할 때는 구조체 멤버의 크기뿐만 아니라 순서도 중요하다.
  • 만약 구조체 멤버의 순서가 달라진다면 값의 일부만 가져오거나 여러 개의 값을 묶어서 가져올 수도 있으므로 주의해야 한다.


  • 이제 fopen 함수로 data2.bin 파일을 읽기/바이너리 모드(rb)로 연다.
  • 그리고 fread 함수의 버퍼는 구조체 변수의 주소 &d1, 읽기 크기는 구조체 크기, 읽기 횟수는 1을 넣어준다.
  • 마지막에는 파일 포인터 fp를 넣는다.


struct Data d1;

FILE *fp = fopen("data2.bin", "rb"); // 파일을 읽기/바이너리 모드(rb)로 열기

fread(&d1, sizeof(d1), 1, fp); // 파일의 내용을 읽어서 구조체 변수에 저장


  • printf로 구조체 변수의 값을 출력해 보면 앞에서 저장한 값이 출력된다.
  • 특히 파일에는 값이 리틀 엔디언 방식으로 저장되어 있다 하더라도 printf 함수로 값을 출력해 보면 사람이 읽을 수 있는 형태로 나오기 때문에 엔디언 문제는 걱정하지 않아도 된다.


printf("%c %d %d %s\n", d1.c1, d1.num1, d1.num2, d1.s1);


  • 즉, data2.bin 파일을 읽어서 구조체에 저장하면 다음과 같은 모양이 된다.


004


  • 파일 읽기가 끝났다면 fclose 함수로 파일 포인터를 닫는다.


fclose(fp); // 파일 포인터 닫기


  • 파일에서 구조체를 읽고 쓸 때 반드시 구조체를 1바이트 크기로 정렬해야 된다는 점만 기억하자.

References