ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C언어/자료구조] 전화번호부 v3.0 (인프런)
    CS/자료구조&알고리즘 2020. 1. 12. 16:51

    ※ 인프런 무료강좌 C로 배우는 자료구조(권오흠 교수님)를 보고 개인적인 복습을 위해 정리한 내용입니다.


    전화번호부 v3.0의 개선사항

    https://skm1104.tistory.com/29 전화번호부 v1.0

    https://skm1104.tistory.com/30 전화번호부 v2.0

    v1.0과 v2.0에서는 배열의 크기가 고정되어 있었고 사용자가 잘못된 명령어를 입력했을 때 적절히 반응할 수가 없었다.

    이러한 상황에도 대처할 수 있도록 개선된 v3.0을 만들어보자.

     

    1) 저장된 사람의 수가 배열의 용량을 초과할 경우

    동적 메모리 할당으로 배열의 크기를 키운다.

    names, numbers를 배열로 선언하지 않고 포인터로 선언해야한다.

    char ** names;
    
    char ** numbers;

    char * 타입 배열의 이름(=배열의 시작주소)을 담는 배열이므로, char ** 타입 (이중 포인터 타입)이다.

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    #define INIT_CAPACITY 3
    #define BUFFER_SIZE 50
    
    char ** names;
    char ** numbers;
    char delim[] = " ";
    
    int capacity = INIT_CAPACITY; /*size of arrays*/
    int n = 0;
    
    void init_directory() {
    	names = (char **)malloc(INIT_CAPACITY * sizeof(char *));
    	numbers = (char **)malloc(INIT_CAPACITY * sizeof(char *));
    }
    
    int main() {
    	init_directory();
    	process_command();
    
    	return 0;
    }

    malloc 함수

    (타입지정)malloc(할당할 메모리의 byte수);

    직접 숫자로 지정하는 것보다 sizeof 연산자를 사용하는 것이 바람직하다. (코드의 호환성을 위해서)

     

    2) 잘못된 명령어에 대해서 적절히 반응한다.

    잘못된 명령어를 입력하면, 다음과 같은 메세지를 출력하고

    Invalid arguments.

    Invalid command format.

    File name required.

    사용자가 다시 입력할 수 있도록

    즉시 그 다음 프롬프트로 넘어간다.

     

    그러기 위해서는 사용자의 명령을 입력받을 때, 단어 단위가 아니라 라인 단위로 입력을 받아서 분석하고 처리하는 방식으로 프로그램을 수정해야 한다.

     

    라인 단위로 입력받기

    line 단위의 입력은 fgets, getline 등의 함수를 사용할 수도 있지만

    그런 함수들의 기능이 내가 원하는 기능과 완전히 일치하지 않는 경우

    직접 함수를 만들어서 사용하기도 한다.

     

    직접 만든 read_line 함수

    int read_line(char str[], int limit) {
    	int ch, i = 0;
    
    	while((ch = getchar()) != '\n') 
    		if (i < limit-1)
    			str[i++] = ch;
    
    /* 
    	while (i < limt-1 && (ch = getchar()) != '\n')
    		str[i++] = ch;
    */
    	
    	str[i] = '\0';
    
    	return i;
    }

     

    int limit은 배열 str의 크기이다. limit보다 더 긴 line의 경우에는 뒷부분이 짤린다.

    줄바꿈 문자가 나올 때까지 읽고 ( ch = getchar() ≠ '\n' )

    배열의 용량을 초과하지 않을 때만 저장한다 ( i < limit-1 )

    배열의 마지막에는 null charactor를 추가해준다 ( str[i] = '\0' )

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

     

    getchar() 함수문자를 하나씩 읽고, 정수 형태로 반환한다.

     

    /* while (i < limt-1 && (ch = getchar()) != '\n') str[i++] = ch; */

    주석처리한 부분처럼 if문의 조건을 while문 조건에 포함시키면,

    라인을 끝까지 읽지 않고 limit 전까지만 읽게 된다.

    그래서 다 읽지 못한 나머지 부분을

    그 다음 실행에서 마저 읽게 되어, 원치않는 결과를 얻게 된다.

    따라서 우선은 라인을 끝까지 읽되, 저장은 그 중에 line을 넘지 않는 부분까지만 하도록 코드를 작성해야한다.

     

    라인 분석하기

    문자열 tokenizing :

    구분 문자(delimiter)를 이용하여 하나의 긴 문자열을 작은 문자열들로 나누는 것.

    잘라진 작은 문자열들은 보통 token이라고 부른다.

    C언어에서는 문자열 tokenizing을 위해 주로 strtok 함수 (string.h 라이브러리 제공) 를 이용한다.

     

    strtok 함수

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

    #include <stdio.h>
    #include <string.h>
    
    int main(void) {
    	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);
    	}
    
    	return 0;
    }
    
    /*
    출력결과 
    
    next token is: now :4
    next token is:  is the time :13
    next token is:  to start preparing :20
    next token is:  for the exam:13
    
    */

    구분자가 두 개 이상 연속해서 나올 때(###) :

    구분자와 구분자 사이에 길이가 0인 토큰(문자열)이 있는 것으로 간주하여 처리하거나, 그냥 건너뛰고 다음 토큰으로 넘어가도록 처리할 수 있는데,

    strtok 함수에서는 건너뛰는 방식을 사용한다. 즉, 구분자가 연속해서 나올 때는 그것을 묶어서 하나의 구분자로 인식한다.

     

    strtok함수 사용법

    token = strtok (문자배열, 구분자);

    (구분자는 문자배열 형태로 넘겨준다. ( char delim[] = "#"; )

     

    처음 호출을 하면, 첫번째 토큰을 리턴한다.

    토큰에는 자른 문자열의 첫번째 문자의 시작주소가 담긴다. (token은 주소값을 담는 char 포인터 변수)

    두번째 호출을 하면, 두번째 토큰을 리턴하는데

    이 때부터는 strtok의 첫번째 매개변수가 문자배열 str이 아닌 NULL이어야 한다.

    token = strtok(NULL, delim);

     

    printf 함수는 항상 '\0'를 문자열의 끝으로 인식하고, 문자열의 끝까지 출력을 한다.

    따라서 printf 함수로 token을 출력했을 때의 결과를 보면,

    strtok 함수가 단순히 자른 문자열의 시작 주소를 찾아주는 것뿐만 아니라(자른 문자열의 시작 주소는 구분자가 아닌 첫번째 문자의 주소이다. )

    자른 문자열의 끝에 null charactor('\0')도 추가한다는 것을 알 수 있다.

     

    ※ strtok은 새로운 배열을 생성하는 것이 아니라, 원본 문자열을 변화시킨다. (새로운 배열을 만들어 반환하는 strdup와는 다름)

    strtok은 원본 문자열의 구분자를 null charactor로 바꾼다. (토큰과 토큰 사이에 '\0'을 삽입한다.)

    따라서 만약 원본 문자열을 보존해야한다면, 복사한 후 strtok을 해야한다.

     

    ※ 문자 배열(charactor array)과 string literal의 차이

    문자 배열의 값은 수정이 가능하지만, string literal(char *str = " "; 로 정의)은 수정이 불가능하다.

    그런데 strok의 첫번째 인자로는 수정이 가능한 문자 배열이 들어가야 한다.

    string literal을 첫번째 인자로 넘겨주면 오류가 발생한다.

     

    수정사항을 적용해 변경된 함수들

    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;
    
    
    		/* 첫번째 토큰으로 strcmp하기 */
    		
    		if ((strcmp(command), "read") == 0) {
    			argument1 = strtok(NULL, delim);
    			if (argument1 == NULL) {
    				printf("File name required. \n");
    				continue;
    			}
    			load(argument1);
    
    		} 
    
    		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(arguments1, arguments2);
    			printf("%s was added successfully. \n", argument1);
    		} 
    
    		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);
    			argumnet2 = strtok(NULL, delim);
    
    			if (argument1 == NULL || strcmp(argument1, "as") != 0 || argument2 == NULL) {
    				printf("Invalid command format. \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 falied. \n");
    		return;
    	}
    	while (fscanf(fp, "%s", buf1)!=EOF)) {
    		fsancf(fp, "%s", buf2);
    		add(buf1, buf2);
    	}
    	fclose(fp);
    }

     

     

    save 함수

    void save(char *fileName) {
    	int i;
    	FILE *fp = fopen(fileName, "r");
    	if (fp ==NULL) {
    		printf("Open falied. \n");
    		return;
    	}
    	for(i=0; i<n; i++) {
    		fprintf(fp, "%s %s\n", names[i], numbers[i]);
    	}
    	fclose(fp);
    }

     

    status 함수는 이전 버전과 동일하다.

     

    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[i+1] = strdup(name);
    	numbers[i+1] = strdup(number);
    
    	n++;
    }

     

    reallocate 함수

    void reallocate() {
    	int i;
    	
    	/*capacity를 두 배로 늘리고, 새로운 임시 배열 두 개를 재할당받는다*/
    	capacity *= 2;
    	char **tmp1 = (char **)malloc(capacity*sizeof(char *));
    	char **tmp2 = (char **)malloc(capacity*sizeof(char *));
    
    	/*원본 배열의 값들을 새로운 배열에 복사한다*/
    	for (i=0; i<n; i++) {
    		tmp1[i] = names[i];
    		tmp2[i] = numbers[i];
    	}
    
    	/*garbage가 된 원본 배열들을 메모리에서 해제한다*/
    	free(names);
    	free(numbers);
    
    	/*names와 numbers(포인터 변수)가 새로운 배열을 가리키도록 한다*/
    	names = tmp1;
    	numbers = tmp2;
    
    }

    free() 함수로 garbage를 반환하지 않아도, 오류는 발생하지 않지만

    원본 배열을 가리키고 있던 names와 numbers가 새 배열(tmp1, tmp2)를 가리키게 되면

    원본 배열의 주소를 가지고 있는 포인터 변수가 사라지기 때문에

    원본 배열은 아무도 사용할 수 없는 garbage가 된다.

    또한 원본 배열들은 init_directory 함수에서 동적 메모리 할당으로 할당받은 배열이기 때문에

    그냥 두면 없어지지 않고 계속 존재하게 된다.

    메모리에 이런 garbage가 많이 쌓여있으면 프로그램의 성능에 영향을 미치거나, 심한 경우 프로그램의 메모리 공간이 부족해질 수도 있기 때문에

    free함수로 garbage처리를 해주는 습관을 들이는 것이 좋다.

    댓글

Designed by Tistory.