72.1 파일에 구조체 쓰기

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

  • fwrite(버퍼, 쓰기크기, 쓰기횟수, 파일포인터);
    • size_t fwrite(void const *_Buffer, size_t _ElementSize, size_t _ElementCount, FILE *_Stream);
    • 성공한 쓰기 횟수를 반환, 실패하면 지정된 쓰기 횟수보다 작은 값을 반환

이제 100, 200, 300, 400을 바이너리 형식으로 저장해보겠습니다. 다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

file_write_struct_short.c

#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;
}

Visual Studio에서 Ctrl+F5 키를 눌러 프로그램을 실행하면 .c 파일이 있는 폴더에 data.bin 파일이 생성됩니다. Visual Studio에서 파일(F) > 열기(O) > 파일(F) ... 메뉴를 클릭한 뒤 data.bin 파일을 열어보면 다음과 같이 16진수 형태로 내용이 표시됩니다(메모장이나 기타 텍스트 편집기로 열면 내용을 정확히 볼 수 없습니다).

data.bin

00000000  64 00 C8 00 2C 01 90 01                               d...,...

리눅스나 OS X에서 data.bin 파일의 내용을 보려면 xxd 명령을 사용합니다.

$ xxd data.bin
0000000: 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("hello.bin", "wb"); // 파일을 쓰기/바이너리 모드로 열기

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

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

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

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

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

그림 72‑2 구조체의 내용을 파일에 쓰기

앞의 예제에서는 구조체 멤버의 자료형이 short로 크기가 같았습니다. 이번에는 구조체에서 각 멤버의 크기를 다양하게 만들어서 파일에 써보겠습니다.

file_write_struct.c

#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;
}

Visual Studio에서 Ctrl+F5 키를 눌러 프로그램을 실행하면 .c 파일이 있는 폴더에 data2.bin 파일이 생성됩니다. data2.bin 파일을 Visual Studio에서 열어보면 다음과 같이 16진수 형태로 내용이 표시됩니다.

data2.bin

00000000  61 64 7D 64 75 2B 7D 48  65 6C 6C 6F 2C 20 77 6F  ad}du+}Hello, wo
00000010  72 6C 64 21 00 00 00 00  00 00 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 플랫폼에서는 정수가 리틀 엔디언으로 저장됨)

그림 72‑3 파일에 구조체 내용 쓰기