콘텐츠로 건너뛰기
Home » [C/C++] 구조체 정보를 디스크에 읽고 쓰는 방법 (2/2)

[C/C++] 구조체 정보를 디스크에 읽고 쓰는 방법 (2/2)

지난 글에서는 구조체로 정의한 임의의 정보들을 디스크에 기록하는 방법을 예시를 통해 살펴보았다. 그런데 해당 글의 예제는 실제 기록하고 읽어들일 때 사용하는 함수(fread(), fwrite())의 사용법에 초점을 맞추었던 관계로 모든 정보가 구조체 내부에 들어있는 상황을 가정했다.

모든 정보가 구조체 속에 들어있는 상황은 구조체 멤버 중에 포인터가 없는 상황을 말한다. 모든 멤버의 정보는 구조체의 영역 내부에 존재하므로 구조체가 점유하고 있는 메모리 공간은 연속적이다. 따라서 이 경우 해당 구조체의 데이터를 메모리의 다른 어딘가로 복사할 수도 있고, 똑같은 방식으로 파일로 복사할 수도 있다. 바이트를 파일에 복사하는 것이 곧 디스크에 정보를 기록하는 것이었던 셈이다.

하지만 실제 세계의 많은 예에서는 포인터가 자주 쓰인다. 문자열이나 크기가 정해지지 않은 배열, 그 외에 내부 구조를 알 수 없는 Opaque 타입같은 것들이 있다. (물론 Opaque 타입 같은 경우에는 내부 구조를 알 수 없으므로 저장도 할 수 없다.) 내부 속성이 포인터로 되어 있다는 것은 실제 값이 객체의 외부에 나와 있다는 것이므로, 실질적인 전체 객체의 데이터가 조각조각 흩어져 있는 것으로 이해할 수 있다.

하지만 파일에 데이터를 저장하는 것은 연속된 메모리 공간을 디스크로 복사하는 것이므로, 실제 데이터를 구성하는 모든 값들을 하나의 연속된 바이트 스트림으로 변환하는 것과 사실상 같은 일을 수행하게 된다.

  • 기본적인 원시 타입 값들은 각각 그 크기가 알려진 고정값이다. 이들은 해당 크기만큼의 바이트를 하나씩 저장하면 된다.
  • 포인터로 참조되는 값의 크기가 고정되어 있다면 마찬가지로 해당 타입의 크기만큼의 바이트를 저장하면 된다.
  • 값의 크기가 고정되어 있지 않은 데이터를 참조하는 포인터의 경우, 크기 (보통 이런 데이터의 크기는 size_t (unsigned long long)를 사용한다.) 값을 1개 기록하고 이어서 해당 크기 만큼의 값을 기록한다.

그리고 각각의 멤버들을 기록한 순서대로 하나씩 읽어서 원래의 데이터를 복원하면 된다. 그러면 지난번 예제와 유사하게 데이터를 저장하고, 파일로부터 읽어서 복원하는 연습을 해보자.

먼저 다음과 같은 Friend 타입을 정의한다. 이전 예제와 비슷한데, 이름을 저장하는 멤버가 char[] 배열이 아니라 char *로 포인터로 구성되어 있다. 따라서 이름을 나타내는 문자열의 길이는 결정되지 않았다.


typedef struct _friend {
  int age;
  float height;
  char * name;
} Friend;

이 타입을 파일에 저장하고 다시 읽어내는 기능을 구현하기 위해서는 2개가 아닌 4개의 함수를 작성할 것이다. 2개는 입출력에 해당하는 기능이며, 다른 2개는 좀 편하게 쓰려고..;;;

  1. Friend 객체에 name 속성을 할당해주는 함수
  2. 생성된 Friend 객체의 정보를 표시하는 함수 (저장/복원한 결과를 확인하기 위해)
  3. Friend를 파일에 기록하는 함수
  4. 파일에 기록된 내용을 바탕으로 Friend 데이터를 복구하는 함수
void FriendSetName(Friend *friend, const char * newName);
void FriendShow(Friend friend);
void FriendWriteIntoFile(Friend *friend, FILE * f);
void FriendReadFromFile(Friend *friend, FILE * f);

편의함수 작성

먼저 Friend 객체의 name 속성을 조작할 수 있는 함수를 작성해보자. 주어진 문자열이 필요한 순간까지 유지될 것이라는 보장이 없으므로, 따로 마련한 메모리에 복사하고 해당 포인터를 friend.name으로 연결한다. 구조체 내부의 값을 변경해야 하므로 포인터 형식으로 인자를 받아야 한다.

void FriendSetName(Friend *friend, const char * newName)
{
	// copy name into friend
	if(friend->name == newName) { return; }
	if(friend->name != NULL) { free(friend->name); }
	size_t l = strlen(newName);
	friend->name = (char *)malloc(l + 1);
	strcpy(friend->name, newName);
}

다음은 테스트용으로 Friend 타입 값의 내용을 출력하는 함수를 작성한다. (간단)

void FriendShow(Friend friend)
{
	printf("Friend at 0x%x : %s (%d, %.1f)\n",
		&friend, friend.name, friend.age, friend.height);
}

기록하기

다음은 Friend 객체를 파일에 저장하는 실제 예시이다. 각각의 멤버를 순서대로 기록한다. 문자열의 길이 정보는 복원시에 필요하므로 따로 구해서 같이 기록해주어야 한다. 그외의 방법은 이전에 소개한 fwrite() 함수만 잘 사용하면 된다. 참고로 문자열의 길이를 구하는 strlen()함수는 종료문자(NULL)를 포함하지 않으므로 +1 한 크기만큼을 함께 저장해준다.

void FriendWriteIntoFile(Friend *friend, FILE *f)
{
	// Wrtie
	size_t l_name = strlen(friend->name) + 1;
	fwrite(&(friend->age), sizeof(int), 1, f);
	fwrite(&(friend->height), sizeof(float), 1, f);
	fwrite(&l_name, sizeof(size_t), 1, f);
	fwrite(friend->name, sizeof(char), l_name, f);
}

읽어오기

읽어오는 함수 부분은 조금까다롭다. 우선 가장 중요한 것은 기록된 순서와 똑같은 순서대로 값을 가져와야 한다는 것이다. 두 번째로 주의해야 할 것은 Friend 내부에는 문자열을 보관할 공간이 없다는 점이다. 따라서 읽어오는 시점에 문자열을 위한 메모리 공간은 반드시 따로 할당하여 생성해야 한다.

void FriendReadFromFile(Friend *friend, FILE *f)
{
	// Read
	size_t l_name;
	fread(&(friend->age), sizeof(int), 1, f);
	fread(&(friend->height), sizeof(float), 1, f);
	fread(&l_name, sizeof(size_t), 1, f);
	// Allocate space for name string
	friend->name = (char*)malloc(l_name);
	fread(friend->name, sizeof(char), l_name, f);
}

예제 작성

이제 이상의 내용들을 활용해서 데이터를 생성하고, 파일에 저장한 후 다시 다른 객체로 내용을 로드하는 과정을 따라보자.

  1. fr1을 생성하고 초기화한다.
  2. fr1의 내용을 출력한다.
  3. fr1을 파일에 저장한다.
  4. fr2를 생성한다.
  5. 3.에서 저장한 파일에서부터 fr2를 초기화한다.
  6. fr2의 내용을 출력한다.

즉 파일을 매개로 객체를 복사하는 동작인 셈이다.

int main() {
	Friend fr1, fr2;
	fr1.age = 20;
	fr1.height = 179.4;
	fr1.name = NULL;
	FriendSetName(&fr1, "A friend in need is a friend indeed.");
	FriendShow(fr1);
	printf("Save fr1 into a file.\n");
	FILE * f = fopen("a.dat", "wb");
	FriendWriteIntoFile(&fr1, f);
	fclose(f);
	printf("Saved.\n");
	printf("Read fr2 from file\n");
	fr2.name = NULL;
	f = fopen("a.dat", "rb");
	FriendReadFromFile(&fr2, f);
	fclose(f);
	FriendShow(fr2);
	printf("Completed.\n");
	// release
	free(fr1.name);
	free(fr2.name);
	return 0
};

다음은 출력결과. 두 객체의 메모리 상의 위치가 다른 것으로 서로 다른 객체인 것을 알 수 있고, 그 값이 동일하게 복구된 것을 확인할 수 있다.

Friend at 0x64fdc8 : A friend in need is a friend indeed. (20, 179.4)
Save fr1 into a file.
Saved.
Read fr2 from file
Friend at 0x64fdb0 : A friend in need is a friend indeed. (20, 179.4)
Completed