[C/C++] 구조체로 구성된 애플리케이션 정보를 디스크에 기록하는 법 2

[이 글은 혼자서 연구하는 C/C++의 예제의 내용을 사용하고 있습니다.]

지난 글에서는 구조체로 구성되는 어떤 정보들을 디스크에 기록하는 방법을 예시를 통해 살펴보았다.  하지만 지난 예제에서는 단지 기록과 읽어들이는 작업에 초점을 맞추면서 아주 단순한 구조체가 레코드를 이루고 있었고, 저장이나 복원에 관해서는 단지 구조체 배열을 그대로 쓰고 읽는 수준이었다.

이번에는 구조체내에서 포인터를 다루는 경우를 반영해 예제를 새로 작성한다. 포인터 자체는 메모리의 번지값이므로 디스크에 기록하는 것이 무의미하며, 그 포인터가 가리키는 실제 값을 저장하는 것이 중요하다. (물론 이는  &연산자를 빼고 쓰면 간단하다) 문제는 기록시에 얼마만큼의 양을 기록할 것인지 여부이다. 만약 이전 예제에서 친구의 이름이 실행 시에 입력을 받게 되고, 따라서 가변적이라면 어떻게 처리할 수 있을까.

파일에 정보를 기록할 때는 복원에 필요한 정보를 모두 기록해주는 것이 맞다. 따라서 이 경우에는 기록할 문자열의 길이를 구해서 함께 저장해주고, 이를 읽어 문자열을 얼마나 읽어야 할지에 대해 알 수 있도록 해야 한다. 또한 구조체 내에서 문자열에 대한 포인터만 가지고 있으므로 문자열을 입력받을 때는 매번 메모리를 할당해야 하며, 반대로 메모리를 해제하는 작업도 잊지 않고 해주어야 한다.

#include <stdio.h>
#include <string.h>
#define MYFILENAME "/Users/sooop/temp/friends.dat"
void writeFriends();
void readFreinds();
void deleteAll();

typedef struct __frinend{
    char *name;
    int age;
    double height;
} friend;

struct __header{
    char desc[100];
    int ver;
    int count;
};

friend friends[32];
int num = 5;

int main(void)
{
    int i, sel;

    // default data
    friends[0].name = (char*)malloc(32);
    strcpy(friends[0].name, "Lee A");
    friends[0].age = 23;
    friends[0].height = 180.1;
    friends[1].name = (char*)malloc(32);
    strcpy(friends[1].name, "Lee B");
    friends[1].age = 23;
    friends[1].height = 182.1;
    friends[2].name = (char*)malloc(32);
    strcpy(friends[2].name, "Lee C");
    friends[2].age = 23;
    friends[2].height = 184.1;
    friends[3].name = (char*)malloc(32);
    strcpy(friends[3].name, "Lee D");
    friends[3].age = 23;
    friends[3].height = 188.1;
    friends[4].name = (char*)malloc(32);
    strcpy(friends[4].name, "Lee E");
    friends[4].age = 23;
    friends[4].height = 180.1;

    for(;;)
    {
        printf("\n\n\n\n1.Show 2.Save 3.Del 4.Read 9.Quit : ");
        sel = getchar();
        switch(sel){
            case '1':
                for(i=0;i<num;i++)
                {
                    printf("\n");
                    printf("Name:%s,\tAge:%d,\tHeight:%.1f\n",friends[i].name, friends[i].age, friends[i].height);
                }

                printf("\nCompleted.\n");
                break;
            case '2':
                writeFriends();
                break;
            case '3':
                deleteAll();
                break;
            case '4':
                readFreinds();
                break;
            case '9':
                deleteAll();
                return 0;
        }
    }           

    deleteAll();
    return 0;
}

void deleteAll()
{
    int i;
    for(i=0;i<num;i++)
    {
        if(friends[i].name != NULL){
            free(friends[i].name);
        }
    }
    memset(friends, 0, sizeof(friends));
    num = 0;

    printf("\nCleared.\n");
}

void writeFriends()
{
    struct __header h;
    int len;
    int i;

    FILE *f = fopen(MYFILENAME,"wb");
    /* write header*/
    strcpy(h.desc,"Friends");
    h.count = num;
    h.ver = 110;

    fwrite(&h, sizeof(struct __header), 1, f);

    for (i=0;i<num;i++)
    {
        len = strlen(friends[i].name);
        fwrite(&len,sizeof(int),1,f);
        fwrite(friends[i].name, len, 1, f);
        fwrite(&friends[i].age, sizeof(int), 1, f);
        fwrite(&friends[i].height, sizeof(double), 1, f);
    }

    printf("\nSaved.\n");
    if(f) fclose(f);
}

void readFreinds()
{
    int i, len;
    struct __header h;

    FILE *f = fopen(MYFILENAME,"rb");

    if(f == NULL){
        printf("\nCan't Open File.\n");
        return;
    }
    // reading header
    deleteAll();
    fread(&h, sizeof(struct __header), 1, f);
    num = h.count;

    for(i=0;i<num;i++)
    {
        fread(&len,sizeof(int),1,f);
//        printf("%d length\n", len);
        friends[i].name = (char*)calloc(1,len+1);
        fread(friends[i].name, len, 1, f);
        fread(&friends[i].age, sizeof(int), 1, f);
        fread(&friends[i].height, sizeof(double), 1, f);
    }

    printf("\n\n%d %s loaded.\n", num, (num>1)?"records":"record");

    if(f) fclose(f);
}

저장하는 부분

헤더를 저장하는 부분은 이전 버전과 별 다를 게 없다. 대신 문자열의 길이를 저장하기 위해 len이라는 변수를 하나 더 사용한다. 결국 저장되는 데이터의 구조는 다음과 같이 될 것이다.

[설명][버전][개수]
[길이][이름값][나이값][신장값]
[길이][이름값][나이값][신장값]

읽어오는 부분

읽어오는 부분은 조금 더 복잡해졌다. 헤더 부분까지를 먼저 읽어서 저장된 레코드의 개수가 몇 개인지를 먼저 파악한다. 여기까지는 지난 번과 동일하다. 이전 버전은 모든 레코드의 크기가 동일했기 때문에 그냥 구조체 배열 전체를 읽어와버리면 됐는데, 이번에는 하나 하나 읽어와서 메모리에 차곡차곡 넣어준다는 점이 다르다. 또한 구조체에는 문자열의 주소 정보만을 담고 있으니, 문자열을 로드하기 위해서는 메모리를 할당받아야 한다는 점도 잊지 말자. 메모리를 할당할 때는 문자열의 끝을 알리는 널문자를 위한 공간을 위해 1바이트를 더 할당해주어야 한다.

1. len에 이름 문자열의 길이를 읽어와 담는다.
2. name에 저장될 실제 문자열의 길이를 알았으므로 메모리의 임의의 영역을 할당받아둔다. 이때 할당 받는 크기는 len보다 1커야 한다. 널문자까지 포함해야 하기 때문이디다.
3. 그런 다음 파일로부터 문자열을 len만큼 읽어온다. 나이와 신장에 대한 정보는 같은 방법으로 (단 읽어와야 하는 바이트 수는 고정되어 있다) 읽어오면 된다.

지우는 부분

지우는 부분 역시 이름을 포인터로 잡으면서 발생한 부수적인 추가 작업이다. 레코드를 삭제할 때는 해당 레코드에서 이름 값의 메모리를 해제하고, 해당 레코드가 점유하고 있던 구조체 요소를 0으로 채운다. 이전 예제에서는 구조체 배열 전체의 크기만큼 0으로 채웠으나, 이 번에는 할당해준 메모리를 해제하는 작업만이 추가되었다고 보면 간단하다.

그외에 예외 등으로 프로그램을 종료하기 이전에는 반드시 malloc, calloc 등으로 할당받은 메모리 영역들을 해제해야 한다. 그래서 메모리 해제 부분을 포함하여 삭제 부분을 따로 함수로 만들었다.