본문 바로가기

CS/자료구조

[인프런|6,7,8강] C로 배우는 자료구조 | 전화번호부 v3.0

전화번호부 v3.0

 

전화번호부 v2.0(5강)에 더해 배열 재할당, 라인 단위 입력과 문자열 tokenizing 기능을 추가한다

 

 

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

 

#define INIT_CAPACITY 3   // 배열 재할당을 테스트하기 위해 작은 값으로 설정

#define BUFFER_SIZE 50

 

char ** names;

char ** numbers;     // char * 타입 배열이므로 char **이다.

 

int capacity = INIT_CAPACITY;   // size of arrays

int n = 0;                              // 전화번호부 사람 수

 

char delim[] = " ";

 

int main() {

  init_directory();  

  process_command();

 

  return 0;

}

 

 

  •  init_directory()

void init_directory() {

  names = (char **)malloc(INIT_CAPACITY * sizeof(char *));

  numbers = (char **)malloc(INIT_CAPACITY * sizeof(char *)); 

  // 할당할 메모리의 byte수를 지정한다. 직접 숫자로 지정하는 것보다 이렇게 sizeof 연산자를 사용하는 것이 바람직

  // malloc() 괄호 안에 필요한 byte 수를 지정.

}

 

 

 

  • int read_line()

int read_line(char str[], int limit)

{

  int ch, i =0;

  

  while ((ch = getchar()) != '\n')    // 줄바꿈이 나올때까지 읽는다.

    if ( i < limit-1 )  // 배열 용량을 초과하지 않을 때만 저장한다. 마지막에 '\0' 이 들어가서 limit -1 이다.

      str[i++] = ch;

 

  str[i] = '\0';   // 문자열이 끝이라는 걸 표시하기 위해 마지막에 null character('\0')를 추가해준다.

 

  return i;  // 실제로 읽은 문자수를 반환한다.   

}

fgets, getline 등의 함수를 쓸 수 있지만 필요한 용도 딱 맞기 쓰기 위해 read_line()과 같은 함수를 만들어 쓴다.

 

문자열을 한 줄씩 받기 때문에 실제 프로그램에 쓸 수 있도록 단어 단위로 끊어주어야 한다.

그때 문자열을 자르는 것을 tokenizing 이라고 한다. 잘자린 문자열은 token이라고 한다.

 

 

  • strtok을 이용한 문자열 자르기(tokenizing)

#include <stdio.h>

#include <string.h>

 

int main() {

  char str[] = "now # is the time # to start preparing ### for the exam#";

  char delim[] = "#";

  char *token;

  

  token = strtok( str, delim ); // 첫번째 토큰의 시작주소를 준다.

 

  while ( token != NULL ) {

    printf( "next token is: %s:%d\n", token, strlen(token));

    token = strtok( NULL, delim );

    // 이어지는 토큰을 얻기 위해서는 그 다음부터는 매개변수로 NULL이 주어진다. 위와 같이 설정했을 때 다음 위치의 주소를 반환해준다.

  }

  return 0;

}

 

delim[] = " "; 인 경우는 위와 같다.

연속되는 구분자는 무시된다. ex) "       "

 

printf()문의 경우 \0이 나올 때까지 문자열을 출력해준다. 
strtok은 자를때마다 \0을 넣어준다. 그렇기에 printf()를 쓸떄 잘라진 token 단위로 출력이 가능해진다.

 

*str = "test abcd";    // string literal 변경불가 이런 경우 strtok에 쓸 수 없다.

str[] = "test abcd";   // char array 변경가능

 

printf("After: #%s#\n", str);

결과가 now만 나오는 이유는 strtok에서 \0 앞에서부터 구분자까지 자르고서 token 끝에 \0을 넣어주었으므로 token인 "now \0 is the time # ~ ~"로 저장되므로 printf문에서 str을 출력하려는 경우 앞에 토큰인 now만 출력되게된다.

 

 

다시 원래 프로그램을 넘어가면

 

  • process_command()

void process_command() {

  char command_line[BUFFER_SIZE];   // 한 라인을 통채로 읽어오기 위한 버퍼

  char *command, *argument1, *argument2;

 

  while(1) {

    printf("$ ");

    

    if (read_line(command_line, BUFFER_SIZE) <= 0)    // 명령줄을 통채로 읽는다.

       continue;

    command = strtok(command_line, delim);

    if (command == NULL) continue;          // 첫 번쨰 토큰은 명령어 이다.

    if (strcmp(command,  "read" ) == 0) {

      argument1 = strtok(NULL, delim);   // read 명령에서 두번쨰 토큰은 파일명이다.

      if (argument1 == NULL) {

        printf("File name required.\n");

        continue;

      }

      load(argument1);    // 파일명을 인자로 주면서 load를 호출한다.

    }

    else if (strcmp(command, "add") == 0) {

      argument1 = strtok(NULL, delim);

      argument2 = strtok(NULL, delim);

 

      if (argument1 == NULL || argument2 == NULL) {

        printf("Invalid arguments. \n");

        continue;

      }

      add(argument1, argument2);   // 이름과 전화번호를 인자로 주면서 add()를 호출한다.

      printf("%s was added successfully. \n", arugment1);

    }

    else if (strcmp(command, "find") == 0) {

      argument1 = strtok(NULL, delim);

      if (argument1 == NULL) {

        printf("Invalid arguments. \n");

        continue;

      }

      find(argument1);

    }

    else if (strcmp(command, "status") == 0) {

      status();

    }

    else if (strcmp(command, "delete") == 0) {

      argument1 = strtok(NULL, delim);

      if (argument1 == NULL) {

        printf("Invalid arguments. \n");

        continue;

      }

      remove(argument1);

    }

    else if (strcmp(command, "save") == 0) {

      argument1 = strtok(NULL, delim);

      argument2 = strtok(NULL, delim);

 

      if (argument1 == NULL || strcmp("as", argument1) ! = 0 || argument2 == NULL) {

        printf("Invalid arguments. \n");

        continue;

      }

      save(argument2);

    }

    else if (strcmp(command, "exit")==0)

      break;

 

  } 

}

 

 

  • load()

void load(char *fileName) {

  char buf1[BUFFER_SIZE];

  char buf2[BUFFER_SIZE];

 

  FILE *fp  = fopen(fileName, "r");

  if (fp==NULL) {

    printf("Open failed.\n");

    return;

  }

  while ((fscanf(fp, "%s", buf1)!=EOF)) {

    fscanf(fp, "%s", buf2);

    add(buf1, buf2);

  }

  fclose(fp);

}

 

 

  • add()

void add(char *name, char *number) {

 

  if (n>=capacity)    // 배열이 꽉찬 경우 배열 재할당

    reallocate();

  int i=n-1;

  while(i>=0 && strcmp(names[i], name) > 0) {  

    names[i+1] = names[i];

    numbers[i+1] = numbers[i];

    i--;

  }

  names[n+1] = strdup(name);

  numbers[n+1] = strdup(number);

  n++;

}

배열이 다 찬 경우 realloc()을 통해 배열을 재할당한다.

꽉 찼는지는 n==capacity인지로 확인한다.

 

  • reallocate()

void reallocacte() {

  int i;

  capacity *= 2;

  char **tmp1 = (char **)malloc(capacity*sizeof(char *));   // 먼저 크기가 2배인 배열들을 할당

  char **tmp2 = (char **)malloc(capacity*sizeof(char *));

  

  for (i=0; i<n; i++) {   // 원본 배열 names와 numbers의 값을 새로운 배열에 모두 복사한다.

    tmp1[i] = names[i];

    tmp2[i] = numbers[i];

  }

  free(names);   // 원본배열은 더이상 필요가 없으므로 공간을 불필요하게 차지하게 하면 성능 저하가 발생하므로 free로 메모리 할당에서 해제해준다.

  free(numbers); 

  

  names = tmp1;

  numbers = tmp2;

}

 

 

  • find()

void find(char *name) {

  int index = search(name);

  if (index==-1) {

    printf("No person named '%s' exists/ \n", buf);

  else

    printf("%s\n", numbers[index]);

  }

}

 

  • search()

int search(char *name) {

  int i;

  for (i=0; i<n; i++)

    if (strcmp(name, names[i])==0) {

      return i;

   }

   return -1;

}

find 함수에서 호출해서 해당 이름의 사람이 어느 위치에 있는지 인덱스를 반환한다.

 

  • status()

void status() {

  int i;

  for (i=0; i<n; i++)

      printf("%s %s\n", names[i], numbers[i]);

  printf("Total %d persons. \n", n);  

}

 

  • remove()

void remove() 

  int index = search(name); // return -1 if not exists

  if (index == -1) {

    printf("No persion named '%s' exists.\n", name);

    return;

  }

  int j = index;

  for (; j<n-1; j++) {   // 정렬은 이미 되어 있으므로 한 칸씩 앞으로 가져오기만해도 정렬은 유지된다.

    names[j] = names[j+1];

    number[j] = numbers[j+1];

    n--;

    printf("'%s' was deleted successfully. \n", name);    

  }

}

 

  • save()

void save() {

  int i;

  FILE *fp = fopen(fimeName, "w");   //해당 이름의 파일이 없다면 fopen 함수에서 새로 만들어준다.

  if (fp==NULL) { 

    printf("Open failed.\n");

    return;

  }

  

  for(i=0;i<n;i++) {

     fprint(fp, "%s %s\n", names[i], numbers[i]);

  }

  fclose(fp);  // 파일을 닫지않아도 당장에는 문제가 생기지 않지만 시스템 자원을 계속 소모하여 프로그램의 성능이 나빠지거나 심한경우 프로그램이 다운될 수 있다.

}

 

 

- 배열 재할당

배열이 꽉 찬 경우 malloc을 이용하여 사용하며 더 큰 사이즈의 빈 배열을 만들고 기존 배열과 같은 위치에 인덱스들에서 값을 복사해온다.

복사가 끝나면 기존배열은 free로 배열에서 할당 해제해주며

기존 포인터 변수가 복사된 새 배열을 가르키게 만들어주면 배열 재할당은 끝난다.


- 라인 단위 입력과 문자열 tokenizing 

scanf의 경우 단어 단위로 입력을 받는다.

하지만 라인 단위로 입력을 받고 싶으므로 다음 입력 사이의 구분을 \n으로 한다.

라인으로 입력 받은 명령을 프로그램에서 동작하도록 세분화 하는 작업을 tokenizing이라 하며 strtok() 함수를 쓴다.

구분자를 이용하여 끊어내며 끊어진 token 뒤에는 \0을 넣어주어서 다음 token과 구분되게 해준다.