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

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

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

하지만 실제 세상에서는 구조체 내부에 배열을 직접 쓰는 일보다는 포인터를 쓰는 일이 더 흔하다. 이 말은 어떤 단위를 구성하는 구조체를 정의했을 때, 그 단위 객체의 속성을 이루는 정보들은 구조체가 점유하는 메모리 영역의 외부에 있을 수 있다는 말이다.

struct Friend1 {
  char name[64];
  int age;
  double height;
};

struct Friend2 {
  char* name;
  int age;
  double height;
};

위 코드에서 두 구조체를 보면 struct Friend1의 경우에는 내부에 char 배열을 가지고 있다. 따라서 실제 문자열 데이터는 구조체가 점유하고 있는 메모리 공간내에 존재한다. 하지만 두 번째 struct Friend2에는 내부에 char* 타입의 포인터를 가지고 있다. 이 말은 이 구조체의 name 멤버는 구조체 외부에 저장되어 있는 문자열 데이터를 가리키게 된다는 말이다.

따라서 fwrite를 사용해서 파일에 이를 기록하는 경우, 실제 문자열 값은 저장되지 않는다. name 필드는 메모리 주소값을 가리키는 부호 없는 정수와 같기 때문에 숫자값만이 저장될 것이고, 다음 번 프로그램 실행 시에는 이 숫자값이 가리키는 곳에는 문자열이 아닌 엉뚱한 값이 저장되기 때문에 프로그램이 제대로 동작하지 않을 것이다.

파일에 정보를 기록할 때, xml이나 json과 같은 특정한 포맷을 따르지 않고 직접 정의한 포맷을 사용하려 한다면 두 가지 원칙을 잘 기억하고 따라야 한다. 첫번째는 필요한 모든 정보를 모두 기록해야 한다는 것과, 두 번째는 기록된 정보들은 명시적이든 암묵적이든 온전하게 읽어서 복구할 수 있어야 한다는 것이다.

Friend1, Friend2 의 내부의 정보 구성은 대략 다음과 같다. (각 타입의 align에 따라서 Friend1의 전체 크기 중에는 비어서 쓰지 않는 공간이 있을 수 있겠지만 이는 무시한다.) Friend2.name 은 포인터이며 이는 외부의 문자열을 참조하며, 그 길이는 현재 알지 못한다.

# Friend1
[name(char[64]) .....  ][age(int)][height(double)]
 ~~~~~~~~~~~~~~~~~~~~~~

# Friend2
[name(char*)][age(int)][height(double)]
   ↳ [char][char][char][char].....

따라서 Friend2를 저장할 때에는 구조체 정보 자체를 저장하는 것이 아니라 모든 멤버를 쪼게어 다음과 같이 저장한다.

  1. 문자열은 그 크기를 알아야 하므로 멤버 name이 가리키는 문자열의 길이를 구한다.
  2. 구해진 문자열의 길이를 저장한다.
  3. 그 길이만큼의 문자열을 저장한다.
  4. age 등 크기가 고정된 다른 멤버들을 저장한다.

만약 크기가 고정되지 않은 문자열이나 배열등의 포인터를 저장해야 한다면 모두 길이를 먼저 기록하고 각각의 데이터를 기록해야 한다.

#define MYFILENAME "friends2.dat"

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

int num = 4;
Friend friends[256] = { .... };

데이터가 이렇게 생겼을 때 이를 파일에 저장하는 함수를 만들어보자.

// Friend[] 타입의 데이터를 파일에 기록
// 데이터 개수는 num 에 있음
// 각 데이터를 저장하는 순서는 다음과 같음
// [name길이][name .....(가변크기).......][age][height]
//
void writeFriend() {
  int i, l;
  FILE * f;
  f = fopen(MYFILENAME, "wb");
  if(f == NULL) {
    printf("\nCan't open file.\n");
    return;
  }

  // 데이터 개수 기록
  fwrite(&num, sizeof(int), 1, f);
  for(i=0; i<num; i++) {
    // 각 데이터 기록
    // 이름 길이 기록
    l = strlen(friends[i].name);
    fwrite(&l, sizeof(int), 1, f);
    // 이름 기록
    fwrite(friends[i].name, sizeof(char), l, f);
    // 그외 숫자값 멤버 기록
    fwrite(&friends[i].age, sizeof(int), 1, f);
    fwrite(&friends[i].height, sizeof(double), 1, f);
  }
  fclose(f);
}

기록할 문자열의 길이를 strlen 으로 구하기만 하면 기록하는 과정은 낱개의 여러 데이터를 순차적으로 기록하는 것이며 그리 어려운 점은 없어 보인다. 다음은 저장된 데이터를 읽어와서 friends 배열을 복원하는 함수를 작성할 차례이다. 그런데 이 과정에서 현재 메모리의 데이터를 초기화하는 부분이 들어간다. 이 부분을 따로 deleteFriends() 함수로 분리하고 작성할 것이다.

void deleteFriends()
{
  // 데이터 초기화
  // 각 데이터의 이름 영역을 해제하고 데이터 배열을 비운다.
  // name으로 사용되는 영역은 모두 동적으로 할당해주었으므로
  // 직접 해제하여야 한다.
  int i;
  for(i=0;i<num;i++) {
    free(friends[i].name);
  }
  memset(friends, 0, sizeof(friends));
  num = 0;
}

실제 데이터를 읽어서 구조체를 복원하는 동작은 다음과 같은 순서로 실행한다.

  1. Friend 타입 구조체 변수를 하나 선언한다.
  2. 첫번째로 int 값 하나을 읽는다. 이는 name의 길이이다.
  3. 1의 길이크기 + 1 만큼의 char 타입 메모리를 할당받는다.
  4. 2에 1의 크기만큼 데이터를 읽는다. (문자열) 그리고 마지막 바이트는 0으로 넣어준다.
  5. 4의 문자열 주소를 Friend.name 으로 할당한다
  6. int 값 하나를 읽는다 (Friend.age)
  7. double 값 하나를 읽는다. (Friend.height)
  8. 구조체의 정보들이 초기화가 마쳐졌으므로 friends 배열에 넣어준다.
void readFriends() {
  int i, l;
  FILE * f;
  f = fopen(MYFILENAME, "rb");
  if(f==NULL) {
    printf("\nCan't open file.\n");
    return;
  }

  // 파일을 열었으니 배열을 초기화한다.
  deleteFriends();
  
  // 배열 크기를 읽고 그만큼 반복
  fread(&num, sizeof(int), 1, f);
  for(i=0;i<num;i++){
    // 변수 선언
    Friend p;
    char * name;
    // 이름 길이를 읽고, 필요한 만큼 메모리 할당
    // 그 크기만큼 파일에서 이름을 읽는다.
    fread(&l, sizeof(int), 1, f);
    name = malloc(sizeof(char)*(l+1));
    fread(name, sizeof(char), l, f);
    name[l] = 0;
    p.name = name;
    // age, height 읽기
    fread(&p.age, sizeof(int), 1, f);
    fread(&p.height, sizeof(double), 1, f);
    
    // 배열에 추가
    friends[i] = p;
  }
}

전체적인 코드는 아래와 같다. 배열의 초기화 코드를 작성하기 귀찮아서 수동으로 입력받아서 생성할 수 있게 하였다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MYFILENAME "friends2.dat"

void writeFriends();
void readFriends();
void addFriends();
void deleteFriends();

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

typedef struct _header {
  char desc[100];
  int version;
  int count;
} header;

int num = 0;
Friend friends[20] = {};

int main(void) {
  int i;
  char sel;

  for(;;) {
    printf("\n\n\n1.show  2.save  3.delete  4.read 5.add 9.quit:  ");
    sel = getchar();
    switch(sel) {
      case '1':
        for(i=0;i<num;i++) {
          printf("no. %i\nName: %s\nAge: %d\nHeight: %.1f\n\n",
                  i+1,
                  friends[i].name,
                  friends[i].age,
                  friends[i].height
              );
        }
        break;
      case '2':
        writeFriends();
        break;
      case '3':
        deleteFriends();
        break;
      case '4':
        readFriends();
        break;
      case '5':
          addFriends();
          break;
      case '9':
          return 0;
      default:
          printf("\nEnter again.\n");
    }
    fflush(stdin);
  }
  return 0;
}


void writeFriends() {
  FILE *f;
  f = fopen(MYFILENAME, "wb");
  if(f==NULL) {
    printf("Fail to open file\n");
    return;
  }
  // 헤더기록
  header h;
  char* msg = "friends";
  strcpy(h.desc, msg);
  h.version = 101;
  h.count = num;
  fwrite(&h, sizeof(header), 1, f);

  int i, l;
  // 각각의 데이터 기록
  for(i=0;i<num;i++) {
    // [(int)글자수][문자열][int][double]
    l = strlen(friends[i].name);
    fwrite(&l, sizeof(int), 1, f);
    fwrite(friends[i].name, sizeof(char), l, f);
    fwrite(&(friends[i].age), sizeof(int), 1, f);
    fwrite(&(friends[i].height), sizeof(double), 1, f);
  }
  if(f) {
    fclose(f);
  }
}

void addFriends() {
  char buffer[1024];
  int age;
  double height;
  int l;
  Friend f;
  printf("Name age height: ");
  scanf("%s %d %lf", buffer, &age, &height);

  l = strlen(buffer);
  char *name = malloc(sizeof(char) * (l + 1));
  memcpy(name, buffer, l);
  name[l] = 0;

  f.name = name;
  f.age = age;
  f.height = height;

  friends[num] = f
  num += 1;
}


void readFriends() {
  int i, l;
  header h;
  FILE * f;

  f = fopen(MYFILENAME, "rb");
  if(f==NULL) {
    printf("\nCan't open file.\n");
    return;
  }
  
  // 헤더 검사
  fread(&h, sizeof(header), 1, f);
  if(strcmp(h.desc, "friends") != 0) {
    printf("\nInvalid File Format\n");
    exit(1);
  }
  if(h.version != 101) {
    printf("\nNot valid data version\n");
    return;
  }

  // 데이터읽기
  deleteFriends(); // 읽기 전 초기화
  num = h.count;
  for(i=0;i<num;i++){
    Friend p;
    fread(&l, sizeof(int), 1, f);
    char *name = malloc(sizeof(char)*(l+1));
    fread(name, sizeof(char), l, f);
    name[l] = 0;
    p.name = name;
    fread(&p.age, sizeof(int), 1, f);
    fread(&p.height, sizeof(double), 1, f);
    friends[i] = p;
  }
}

void deleteFriends()
{
  // 데이터 초기화
  // 각 데이터의 이름 영역을 해제하고
  // 데이터 배열을 비운다.
  int i;
  for(i=0;i<num;i++) {
    free(friends[i].name);
  }
  memset(friends, 0, sizeof(friends));
  num = 0;

}

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

이번 글은 구조체를 파일로 저장하고 읽어들이는 작업을 예제로 살펴보는 것이다. 원리는 간단하다. 첫번째로 파일 스트림을 사용해서 데이터를 읽고 쓴다. 여기에는 fwrite(), fread()라는 두 함수를 사용하는데 두 함수로 전달되는 인자는 파일 디스크립터와 읽고 쓰는 데이터의 크기로 사용법은 거의 비슷하다. 두 번째 원리는 각 멤버를 순서대로 기록하는 것이다. 이 때 순서는 중요한데, 왜냐하면 읽어들일 때에도 기록한 순서 그대로 읽어들이면 되는 것이다.

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

[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 등의 포맷팅 문자들의 개수를 세고, 또 각각의 문자가 받아야 할 인수의 타입을 설명해주므로 적절히 처리할 수 있을 것 같다. 아무튼 가변 인수 함수는 이런 식으로 만들어 진다.