C에서의 문자열 배열과 문자열 배열을 동적으로 할당하기

문자열의 배열을 2차원배열을 쓸 때의 문제점

C언어는 문자열을 다루는 자료형이 없다. 대신에  C에서 문자열은 널 문자로 끝나는 문자(char) 타입의 배열을 사용하여 저장한다.  만약 일련의 문자열 집합을 다루려면 문자열의 배열을 써야 할테다.  여러 개의 문자열을 배열에 담고 싶다면 이차원 배열을 쓰는 방법을 생각할 수 있는데,  이 경우 크기가 일정하게 고정된 영역을 여러 개 생성해야 하고, 만약 각 문자열의 길이가 제각각 다르다면 불필요하게 낭비되는 메모리가 제법 될 수 있다.

2차원 배열의 문자열 배열
2차원 배열을 사용하여 구성한 문자열 배열

메모리를 비롯하여, 부족한 하드웨어 자원을 알뜰 살뜰 아껴서 최대의 성능을 내기 위해 개발된 초기 C언어 관점에서 이러한 2차원 배열의 사용은 그리 추천하지 않는다. C에서의 문자열 배열과 문자열 배열을 동적으로 할당하기 더보기

[C] conio.h에 대해

conio.h 는 MS-DOS용 C컴파일러에 포함되어 있는 헤더파일로, 표준 C 라이브러리에는 포함되지 않고 있다. (따라서 여기서 정의한 함수를 표준 함수처럼 쓰면 다른 플랫폼에서는 심벌이 없다고 컴파일 되지 않는다.) 이 헤더에는 콘솔 입출력과 관련된 함수들이 정의되어 있다.

멤버 함수들

  • kbhit – 키보드가 눌려졌는지 확인
  • getch – 콘솔에서 버퍼나 에코(타이핑한 키가 화면에 표시)없이 한 개의 키 값을 입력 받음
  • getche – 콘솔에서 키보드 입력으로 한 개의 키 입력을 받음. 에코됨.
  • ungetch – 키보드 버퍼로 문자 1개를 넣음
  • cgets – 콘솔로부터 문자열을 직접 입력 받음
  • gscanf – 콘솔 입력으로부터 포매팅된 입력을 받음
  • cputs – 콘솔에 직접 문자열을 출력
  • cprintf –  포맷을 사용해 콘솔에 문자열을 출력
  • clrscr – 화면을 지움

 

[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

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

텍스트가 아닌 애플리케이션의 고유한 데이터를 파일에 기록하는 방법에 대해서 알아보자. 애플리케이션 내에서 정의한 어떤 정보는 다양한 형태와 구조를 가질 수 있기 때문에, 평문 텍스트를 저장하는 것과는 다르다. 이 글에서는 간단한 데이터 타입을 구조체를 통해 정의해보고, 이렇게 만들어진 데이터를 어떻게 디스크에 저장하고 또 다시 불러올 수 있는지에 대해서 알아볼 것이다.

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

[C/C++] 가변인수를 받는 함수

C함수의 가변인수

C함수도 가변인수를 사용할 수 있다. 대표적인 예가 printf이다. printf에는 서식 문자열을 비롯하여 서식에 채워질 값들을 컴마로 연결하여 나열할 수 있다. 이러한 가변인수는 어떻게 만들어서 사용할 수 있는지 살펴보자.

가변인수 함수의 선언

가변 인수 함수는 가변적으로 받을 인수 대신 을 사용하는 것으로 가변 인수 함수임을 컴파일러에게 알릴 수 있다. 인수에 …이 들어있는 경우 컴파일러는 인수의 개수나 타입에 대해서는 전혀 신경을 쓰지 않게 된다. (결국 이는 함수 구현 시 프로그래머가 일일이 체크해야 하는 부분이다.) 이 때 중요한 것은 고정적으로 사용하는 인수는 반드시 하나 이상 있어야 한다. 즉 int myFunc(…) 과 같이 쓰는 것은 안된다. 최소한 하나의 인수는 고정적으로 정의되어 있어야 한다. 올바른 가변 인수 함수의 선언은 int myFunc(int argc, …) 과 같은 모양이 되어야 한다.

가변 인수의 사용

컴파일러는 가변 인수에 몇 개의 인수가 들어오는지 전혀 신경쓰지 않는다. 따라서 가변이라고 하더라도 몇 개의 인수를 받을지는 알 수 있는 방법이 있어야 한다. 대부분의 가변 인수는 고정인수로 가변 인수의 개수를 받는 편을 택한다.

가변 인수의 획득

가변인수는 va_list 타입의 지역변수 ap를 선언하고 이를 통해 개별 인수를 획득할 수 있다. 다음과 같은 방식을 사용한다.

/*가변인수 함수 내*/

va_list ap;
va_start(ap,마지막_고정인수);
루프시작{
    arg = va_arg(ap, 변수의 타입);
    /*모든 가변 인수를 획득할 때 까지 이 루프를 반복한다*/
}
va_end(ap);

마지막 고정인수는 가변 인수를 획득하기 위해 꼭 필요하다. 왜냐하면 가변인수는 포인터 배열로 전달되기 때문에 시작번지를 알고 있어야 한다. 그 가변 인수의 시작번지는 마지막 고정인수의 번지의 바로 옆 위치가 되기 때문이다. (*함수의 인수로 전달된 변수들은 해당 함수의 지역 변수와 동등하게 취급된다.)

이렇게 루프를 돌며 각 변수의 타입을 지정하여 해당 인수를 획득할 수 있다. 특이한 점은, va_arg()라는 함수가 변수의 타입을 인수로 받고 있는데, 정상적인 C문법에서는 변수 타입을 인수로 받을 수 없다. 아마 C함수가 아니라 매크로로 짜여진 함수가 아닐까 생각된다. 즉 ap는 문자열형 포인터 변수이고, 루프를 돌면서 해당 포인터에서 지정된 타입형의 크기만큼의 값을 읽어서 그 내용을 꺼내주는 역할을 할 것이다.

이렇게 모든 가변 인수를 획득한 다음, va_end(ap); 구문을 써서 마무리 작업을 해줘야 한다. (사실 할 필요는 없는데 혹시 모르니.)

이 때, 계속해서 강조하는 것은 컴파일러는 어디서부터 어디까지가 인수로 넘겨진 값인지 알 수 없으니, 적절한 개수만큼의 인수를 얻어와야 한다. 가장 쉬운 방법은 고정 인수 중 하나에 가변 인수의 개수를 알려주는 방법이 있고, 혹은 문자열 배열을 처리하는 것과 마찬가지로 0 혹은 -1과 같이 특이한 값을 마지막 가변인수로 넣어주도록 하는 방법이 있을 수 있다.

다음은 가변 인수를 통해 함수에 집어 넣는 숫자들을 모두 더한 합을 리턴해주는 예제이다. 고정 인수로 가변 인수의 개수를 넘겨주는 방식을 택했다. va_list를 사용하기 위해서는 stdarg.h 를 반입해야 한다.

#include <stdio.h>
#include <stdarg.h>

int getSum(int num, ...)
{
	int sum = 0;
	int i;
	va_list ap;
	int arg;

	va_start(ap, num);
	for(i=0;i<num;i++){
		arg = va_arg(ap,int);
		sum += arg;
	}

	va_end(ap);
	return sum;

}

int main(void)
{
	printf("1+2=%d\n",getSum(2,1,2));
	printf("1+2+3+4+5=%d\n",getSum(5,1,2,3,4,5));

	return 0;
}

 그럼 printf는 어떻게 하고 있나

처음에 언급했던 printf는 그럼 어떻게 하고있나? 이 함수를 쓸 때는 가변인수의 개수를 명시하지도 않고 있고, 심지어 가변 인수의 타입도 뒤죽 박죽이다. 하지만 고정인수로 보내는 “서식 문자열”에 포함된 %d, %s, %c 등의 포맷팅 문자들의 개수를 세고, 또 각각의 문자가 받아야 할 인수의 타입을 설명해주므로 적절히 처리할 수 있을 것 같다. 아무튼 가변 인수 함수는 이런 식으로 만들어 진다.