본문 바로가기

Information Technology/C

[C언어] 전화번호부 v.3

이번 포스팅에서는 이전 포스팅에서 구현했던 전화번호부를 보완해서,

1. 옳지 않은 명령어를 걸러내고 토크나이징(tokenizing)해서

2. 저장된 사람의 수가 배열의 용량을 초과할 경우 동적 메모리 할당으로 배열의 크기를 키우는

기능을 추가하겠습니다.

 

명령어 오류를 처리하기 위해선 이전의 전화번호부처럼 buf에 하나의 단어만 저장해서 명령어를 처리하는 것이 아니라,

한 줄 단위로, 즉 엔터(\n) 키를 입력받기 전까지의 문자열을 통째로 처리해야 합니다.

 

1. 문자열을 줄 단위로 읽어서 토크나이징(tokenizing)

int read_line(char str[], int limit)	// limit으로 받은 길이 만큼의 문자열을 읽어냄
{
	int ch, i = 0;
	// getchar()는 한 개의 문자만 입력 받는데, 그 리턴 타입이 int형
	while ((ch = getchar()) != '\n') // 엔터키(\n, new line) 전까지 문자열을 읽어냄
	{
		if (i < limit - 1)	// null 문자를 저장할 마지막 한 칸을 남겨두고 저장. 
			str[i++] = ch;	// 문자를 저장하고 i를 1 증가
	}
	/*
	while(i<limi-1 && (ch=getchar())!='\n')) -> 이렇게 되면 엔터키를 입력받지 않더라도 배열의 길이를 넘어서면 while문 탈출. 즉, 문자열 전체를 입력 못 받음
	*/
	str[i] = '\0';	// 배열 마지막에 null 문자 저장

	return i;
}

 

	{
		char str[] = "hi   my name is   woo ";
		char delim[] = " "; // delimeter도 문자열 배열로 전달. " " 문자를 전부 무시해서 tokenize
		char* token;
		token = strtoc(str, delim);	
        	// str에서 delime까지의 문자열을 tokenize 해서 그 문자열의 첫 번째 주소를 token에 리턴

		while (token != NULL) // 문자열의 끝에서 더 이상 tokenize할 문자가 없으면 NULL을 리턴
		{
			printf("next token is: %s:%d\n", token, strlen(token)); 
            	// strlen은 정확히 문자열 개수만 출력
			token = strtok(NULL, delim);	
            	// 두 번째 호출부터는 반드시 첫 번째 매개변수로 NULL을 줘야함 
            	// strtok이 c 라이브러리 중 논란이 많은 이유..
			// 그렇게 하면 이전에 tokenize한 문자열의 뒷부분 부터 다시 호출됨
		}
	}

위 그림이 strtok이 작동하는 방식입니다.

이 예제에서는 delimeter를 공백 문자(" ")로 받았습니다. strtok은 첫 번째 매개변수로 받은 str 중 두 번째 매개변수로 받은 delim, 즉 공백 문자를 싹 무시합니다.

그렇게 첫 번째 token에서 strtok은 str의 첫 번째 문자인 공백(" ")을 무시하고, 그다음 문자열 중 공백이 아닌 첫 번째 문자열인 t의 주소를 char* 타입의 변수 token에 저장합니다. 하지만 여기서 strtok의 역할이 그친다면 time 뒤의 모든 문자열까지 token에 저장됩니다. 때문에 strtok은 delim을 제외한 다음 문자열이 또 다른 delim을 만날 때까지의 문자열만 tokenize 하고 그 뒤에 '\0'문자를 붙여줘서 하나의 문자열로 만들어줍니다.

그리고 다음 strtok 호출부터는 첫 번째 매개변수로 NULL을 줘야 합니다. 하나의 문자열에서 연속된 strtok 호출에서는 이전 strtok에서 tokenize 한 문자열 이후부터의 문자열에 대해 tokenizing을 진행하여 tokenize 된 단어의 첫 번째 문자 주소를 token에 저장해줍니다.

즉, strtok은 원본 문자열을 변화시킨다는 것이고, 이는 strtok에 전달된 str이 string literal이 아니라는 말과 같습니다.

 string literal은 변경이 불가능하기 때문입니다.

char name[10] = "Ryu";      // char array -> 문자열 변경 가능
char *name = "Ryu";	    // string literal -> 문자열 변경 불가능

위의 코드처럼, char [10]과 같이 문자열을 char array로 저장할 경우 strtok에 사용된 str처럼 문자 변경이 가능하지만,

char *name처럼 string literal 방식으로 문자열을 저장할 경우 name에 대한 변경이 불가능합니다.

char str[] = "hi   my name is   woo ";
	char delim[] = " "; // delimeter도 문자열 배열로 전달
	char* token;
	
	printf("Before: %s\n", str);	// tokenize 이전의 str 출력
	token = strtok(str, delim);	
	while (token != NULL) 
	{
		printf("next token is: %s:%d\n", token, strlen(token)); 
		token = strtok(NULL, delim);
	}
	printf("After: %s", str);	// tokenize 이후의 str 출력

strtok이 실행되기 이전과 이후에 문자열 str이 어떻게 변화되는지 확인하기 위해 위와 같이 코드를 작성했습니다.

코드를 실행시켜보면, strtok을 실행하기 전에는 완전했던 str이, strtok이 실행된 후에는 hi만 남아있음을 볼 수 있습니다.

이렇게 결과가 나오는 이유는 strtok이 새로운 배열을 생성하는 strdup와 달리, 그냥 배열 str 내부에 null character를 추가 및 수정하면서 token의 첫 번째 주소만 호출하기 때문입니다. 즉 strtok에서는 tokenize 된 문자열을 새로운 배열에 만드는 것이 아니라는 걸 알고 계시면 좋겠습니다.


#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>

#define INIT_CAPACITY 3
#define BUFFER_SIZE 50

char** names;	// char* 을 저장하는 배열의 이름이므로 char**
char** numbers;

int capacity = INIT_CAPACITY;
int n = 0;	// 저장된 사람 수

char delim[] = " "; // token들을 구분하는 delimeter

int main()
{
	init_directory();
	process_command();

	return 0;
}

void reallocate() // 배열의 크기를 그대로 증가시키는 건 불가능. 더 큰 배열을 만들고 거기에 옮겨야 함 
{
	int i;
	
	capacity*=2;
	char **tmp1 = (char**)malloc(capacity*(sizeof(char*)));
	char** tmp2 = (char**)malloc(capacity*(sizeof(char*)));
	for(int i=0; i<n; i++)
	{
		tmp1[i]= names[i];
		tmp2[i]= numbers[i];
	}
	free(names);	// 동적 할당되었기 때문에 free 선언을 하지 않으면 계속 heap 영역에 남아있게 됨. 
	free(numbers);
	
	names = tmp1;	// names가 새로운 배열을 가리키도록. 배열의 이름 = 포인터 주소   
	numbers = tmp2;	// numbers가 새로운 배열을 가리키도록   
 } 

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);
}

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];
		number[i+1] = numbers[i];
		i--;
	}
	names[i+1] = strdup(name);		// name과 number는 load 함수의 지역 변수이기 때문에 스코프를 벗어나면 사라짐  
	numbers[i+1] = strdup(number);	// 때문에 strdup를 통해 새로운 메모리 주소를 동적으로 할당받아 저장함 
	n++;

}

int search(char* buf)
{
	
	for (int i = 0; i < n; i++)
	{
		if(strcmp(names[i],buf)==0)
			return i;
	}
	return -1;
}

void find(char*name)
{
	int index = search(name);
	if(index==-1)
		printf("No person named %s exists\n", name);
	else
		printf("%s\n", numbers[index]);
}

void init_directory()	// 전화번호부 초기화
{
	names = (char**)malloc(INIT_CAPACITY * sizeof(char*));		// byte 단위로 할당. 
	numbers = (char**)malloc(INIT_CAPACITY * sizeof(char*));	// 때문에 직접 숫자 지정보다는 sizeof 연산자를 사용하는게 바람직
}

int read_line(char str[], int limit)	// limit으로 받은 길이 만큼의 문자열을 읽어냄
{
	int ch, i = 0;
	// getchar()는 한 개의 문자만 입력 받는데, 그 리턴 타입이 int형
	while ((ch = getchar()) != '\n') // 엔터키(\n, new line) 전까지 문자열을 읽어냄
	{
		if (i < limit - 1)	// null 문자를 저장할 마지막 한 칸을 남겨두고 저장. 
			str[i++] = ch;	// 문자를 저장하고 i를 1 증가
	}
	/*
	while(i<limi-1 && (ch=getchar())!='\n')) -> 이렇게 되면 엔터키를 입력받지 않더라도 배열의 길이를 넘어서면 while문 탈출. 즉, 문자열 전체를 입력 못 받음
	*/
	str[i] = '\0';	// 배열 마지막에 null 문자 저장

	return i;
}

void remove(char* name)
{
	int i= search(name);
	if(i==-1)
	{
		printf("No person named %s exists.\n", name);
		return;
	}
	
	for(int j=i;j<n-1; j++)
	{
		names[j] = names[j+1];
		number[j] = numbers[j+1];
	}
	n--;
	printf("%s was deleted successfully.\n", name);
}

void status()
{
	for (int i = 0; i < n; i++)
	{
		printf("%s %s\n", names[i], numbers[i]);
	}
	printf("Total %d people.\n", n);
}

void save(char* filename)
{
	FILE *fp = fopen(filename, "w");
	if(fp==NULL)
	{
		printf("Open failed.\n");
		return;
	}
	for(i=0; i<n; i++)
	{
		fprintf(fp, "%s %s\n", names[i], numbers[i]); // 파일 포인터 fp가 가리키는 파일에 fprintf를 통해 문자열을 저장 
	}
}

void process_command()
{
	char command_line[BUFFER_SIZE];	// 한 라인을 통째로 읽어오는 버퍼 
	char *command, *argument1, *argument2;	// 읽어온 라인을 토크나이징하여 저장할 char 타입 
	
	while(1)
	{
		pritnf("$ ");
		if(read_line(command_line, BUFFER_SIZE)<=0)	// 아무 명령 없이 엔터를 실행 
			continue; // 다시 while문의 시작으로 돌아감  
		command = strtok(command_line, delim);	// strtok을 이용해서 command를 받아옴  
		if(command==NULL) continue;	// 역시 명령문이 없으면 continue 
		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); // command 이후의 이름을 tokenize해서 저장 
		 	argument2 = strtok(NULL, delim); // 이름 이후의 번호를 tokenize해서 저장
			if(argument1==NULL||argument2==NULL) // 이름이 없거나 번호가 없을 때 
			{
				printf("Invalid arguments.\n");
				continue;
			}
			add(argument1, argument2);	// 이름과 번호를 add 함수를 통해 저장  
			printf("%s was added successfully.\n", argument1);  
		  }
		else if(strcmp(command, "find")==0)
		{
			argument1 = strtok(NULL, delime);
			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(arguments1);
		}	
		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 command formant.\n");
				continue;
			}
			save(argument2);
		}
		else if(strcmp(command, "exit"));
			break;
	}
}

'Information Technology > C' 카테고리의 다른 글

[C언어] 연결리스트 - 개념과 기본 동작들(1)  (0) 2020.01.08
[C언어] 전화번호부 v.4  (0) 2020.01.07
[C언어] 전화번호부 v.2  (0) 2020.01.06
[C언어] 전화번호부 v.1  (0) 2020.01.06
[C언어] 문자열(2)  (0) 2020.01.05