💡 자료구조

전화번호부 v3.0 - 배열 재할당, 라인단위 입력과 문자열 tokenizing

ji-hyun 2021. 6. 22. 22:16

포인터는 변수의 주소를 가리키는 또 다른 변수라 생각하면된다. 그래서 포인터라고도 하지만 포인터 '변수'라고도 불리운다. 주소를 가리킨다는 말은 실제로는 포인터는 대상체의 주소값을 지닌다는 말이다.


이중 포인터란

그럼 포인터도 일종의 변수기 때문에, 포인터를 가리키는 포인터도 존재한다. 이를 이중 포인터라고 한다.
그런데 이런 이중 포인터의 선언 타입은 첫번째 포인터의 선언 타입을 따라간다. 결국 이중포인터도 최초의 변수 즉 최종 대상체가 되는 값의 타입을 기준으로 선언 되는 것이다.

int a = 7; 
int *p1; 
int **p2; 

p1 = &a; 
p2 = &p1; 

printf("%d ", *p1); 
printf("%d", **p2);


출력 결과는 7 7 이 나온다.




2021.06.19 - [자료구조] - 전화번호부 v2.0 - 파일을 저장하고 로드하기, 알파벳 순으로 정렬

 

전화번호부 v2.0 - 파일을 저장하고 로드하기, 알파벳 순으로 정렬

2021.06.19 - [자료구조] - 전화번호부 v1.0 전화번호부 v1.0 오늘은 권오흠 교수님께서 전화번호부를 만드는 예제를 가르쳐주셨다. 여태까지 배웠던 C언어의 기초를 응용해서 전화번호부 예제를 만들

ts2ree.tistory.com

이번 시간은 전화번호부 v2.0 의 업그레이드 버전인 v3.0 을 알아볼 것이다.

 

전화번호부 v3.0 실습 결과물

 

이전 버전과의 차이점은..

 

 

- read directory.txt 를 입력해야 하는데 read 라고만 입력했을 경우, v2.0은 파일의 이름을 입력할 때까지 프로그램은 아무런 응답을 하지 않는다. 그에 대해 v3.0은 File name required 와 같이 적절한 반응을 할 수 있도록 개선할 것이다. 혹은 이름은 입력했는데 전화번호를 입력하지 않았거나 파일을 저장하는데 잘못된 형식으로 명령을 내릴때도 적절한 반응을 하도록 만들 것이다.

 

- 이전까지는 단어의 입력으로 받았으나, 이제는 라인 단위로 입력 받을 수 있도록 개선할 것이다. v2.0 은 단어 read 만 입력하고 엔터를 쳐도 반응이 없다.(단어 단위로 받는 v2.0) 하지만 v3.0은 라인 단위로 입력 받아 read 라고만 쓰고 엔터를 쳐도 바로 반응을 할 수 있도록 만들 것이다. (즉 올바른 명령어인지 아닌지 분석하게 만들 것이다 = 문자열 토크나이징)

 

- 사람 수가 초과하게 될 경우 배열 재할당을 통해 배열 크기를 개선해볼 것이다.

 

 

 

 

 

 

 

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

#define INTI_CAPACITY 3 /* 배열 재할당을 테스트하기 위해서 일부터 작은 값으로*/ 
#define BUFFER_SIZE 50 

char **names;
char **numbers; 

int capacity = INTI_CAPACITY; /* size of arrays */ 
int n = 0; /* number of people in phone directory */ 

/* function prototypes here */ 

char delim[] = " "; 

int main() { 

	init_directory(); // 이 함수에서 배열 names와 numbers를 생성한다. 
	process_command(); // 사용자의 명령을 받아 처리하는 부분을 별개의 함수로 만들었다. 
    
	return 0; 
}

 

전역 변수 선언과 main() 함수 부분이다. names 와 numbers 는 배열로 선언하면 안된다. 이 예제는 배열을 재할당 해야하기 때문이다. 배열로 선언하면 안되는 이유를 저번 게시물에서 설명하였다.

2021.06.16 - [자료구조] - 동적할당 malloc 함수

 



 

void init_directory() { 
	names = (char **)malloc(INTI_CAPACITY * sizeof(char *)); 
    numbers = (char **)malloc(INTI_CAPACITY * sizeof(char *)); 
}

names와 numbers를 동적 메모리 할당으로 선언한다. 그리고 malloc( ) 안에서 할당될 메모리의 byte 수를 지정한다.
배열의 각 칸에 저장되는 타입은 각 단어의 주소를 저장할 (char *) 타입이고 INIT_CAPACITY 는 배열의 크기이다.



 


이전에는 단어 입력으로 받았다면, 이번에는 라인 단위로 입력 받는다.

 

int read_line(char str[], int limit) {
	int ch, i = 0;

	while ((ch = getchar()) != '\n')  // 엔터 만나기 전까지 반복한다
		if (i < limit-1)   
			str[i++] = ch;
	
	str[i] = '\0';  
	return i;
}

 

limit 변수는 배열 str 의 크기이다. limit 보다 더 긴 line의 경우 뒷부분이 짤린다. 여러가지 문자열 함수가 있으나(fgets, getline 등), read_line 함수를 굳이 만들어 쓰는 이유는 역시 저번 게시물에 설명을 하였다.

2021.06.18 - [자료구조] - 문자열 예제

 

 

 

 

 

 

< strtok 함수 >

 

 

strtok 함수를 살펴보면, str 을 delim 으로 나눈 후(토크나이징) 첫번째 토큰의 시작 주소를 반환해주는 함수이다. 그리고 while 문에서는 token 값이 NULL 이 아닌 경우에만 반복한다.

 

두번째 strtok 함수를 호출할때, 첫번째 매개변수는 NULL이 된다는 사실을 기억하자.

 

 

 

strtok 함수의 원리를 살펴보면 다음과 같다.

 

 

먼저 delim이 구분자이기 때문에 앞에 공백은 건너뛰게 되고 t가 token의 시작주소가 된다. strtok 함수의 특이한 점은 함수가 적용되고 난 후 뒤에 문자열의 끝을 알리는 널문자가 붙게 된다는 것이다.

그래서 printf 로 출력하면 printf는 널문자 전까지 출력하게 되어 time 이 출력되어진다.

 

 

그 다음 strtok 함수의 첫번째 매개변수는 NULL 이 되어야 다음 시작 주소로 넘어갈 수 있다.

 

 

 

 

strtok 함수의 특징 정리

  • strtok 은 원본 문자열을 변화시킨다 ('\0' 를 삽입한다.)
  • 따라서 만약 원본 문자열이 보존되어야 한다면 복사한 후 strtok 을 해야한다.
  • strtok 은 새로운 배열을 생성하지 않는다. 즉, strdup 와는 다르다.

 

 

 

 

 

 

 

int main(){
	char *str = "now # is the time # to start preparing ## for the exam##";
    char delims[] = "#";
    char *token;
    char *tokens[100];
    int i = 0;
    
    token = strtok(str, delims);
    while(token != NULL){
    	printf("next token is: "%s:%d\n", token, strlen(token));
        token = strtok(NULL, delims);
        tokens[i++] = token;
        }
        
        return 0;
}

 

오류가 뜬다. 왜냐하면 *str 은 string literal 이기에 변경할 수 없기 때문이다. 그래서 strtok 를 할 수 없게 되는 것이다. (strtok 함수의 특징을 살펴보면 원본 문자열을 변화시키는 특징을 가지고 있다.)

그러나 str[] = "~" 은 가능하다. 잘 모르겠다면 밑에 게시물 클릭!

2021.06.16 - [자료구조] - 문자열

 

 

 


C언어에서는 문자열을 char 형을 원소로 하는 배열이나 char 형을 원소로 하는 포인터 형식으로 문자열을 표현할 수 있어요.
그리고 문자열 데이터를 표현할 때 쉽게 표현할 수 있게 쌍 따옴표를 사용하여 문자열을 표현할 수 있어요.

 

#define MAX_NAME_LEN 50
char name[MAX_NAME_LEN + 1] = "hello";
const char *str = "yahoo";

 

char 형식 원소로 배열을 선언하면 문자열을 구성하는 문자 데이터를 기억하는 메모리를 할당하여 조회 및 변경이 가능하죠.
하지만 char 형을 원소로 하는 포인터는 데이터를 기억하는 저장소의 역할을 하는 것은 아니예요.

 

예를 들어 char 형을 원소로 하는 두 개의 배열의 초기값을 “hello”로 설정하면 각각의 배열을 위해 할당한 메모리에 독립적으로 문자열을 설정해요.
그리고 각각의 배열의 원소에 접근하여 변경하는 것이 가능하죠.

 

하지만 두 개의 포인터 변수에 초기값을 “yahoo”로 설정하면 읽기 전용 메모리에 문자열을 배치한답니다.
그리고 두 개의 포인터 변수는 해당 메모리 주소를 값으로 갖는 것이죠.
이 때 읽기 전용 메모리에 할당한 문자열을 문자열 리터럴 상수라 부르며 내용을 변경할 수 없어요.

 

 

출처 : https://ehpub.co.kr/tag/%EB%AC%B8%EC%9E%90%EC%97%B4-%EB%A6%AC%ED%84%B0%EB%9F%B4/


 

 

 

 

 

+ )

 

 

names 와 numbers 배열이 두 배로 늘어난 효과를 얻을 수 있다.

 

 

 

 

 

 

 

실습코드

 

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

#define INIT_CAPACITY 3   /* 배열 재할당을 테스트하기 위해서 일부러 아주 작은 값으로 */
#define BUFFER_SIZE 20


char ** names;   /*  char *타입의 배열의 이름이므로 char ** 타입의 변수이다 */
char ** numbers;

int capacity = INIT_CAPACITY; /* size of arrays */

int n = 0; /* number of people in phone directory */



void add();
void find();
void status();
void remover();
void load();    
void save();

void init_directory();
void process_command();
void reallocate();



char delim[] = " ";


int main() {
	init_directory();   /* 이 함수에서 배열 names 와 numbers 를 생성한다. */
	process_command();    /* 사용자의 명령을 받아 처리하는 부분을 별개의 함수로 만들었다. */
	return 0;
}


void init_directory() {
	names = (char **)malloc(INIT_CAPACITY * sizeof(char *));
	numbers = (char **)malloc(INIT_CAPACITY * sizeof(char *));
}


int read_line(char str[], int limit)   /* 배열 str의 크기이다. 즉 limit 보다 더 긴 line의 경우에는 뒷부분이 짤린다. */
{
	int ch, i = 0;
	while ((ch = getchar()) != '\n')   /* 줄바꿈 문자가 나올 때까지 읽는다. */
		if (i < limit - 1)   /* 배열의 용량을 초과하지 않을 때만 저장한다. */
			str[i++] = ch;

	str[i] = '\0';    /* 마지막에 null character를 추가해준다. */

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


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);   /* 명령어에 이어지는 2개의 토큰은 각각 이름과 전화번호이다.*/
			argument2 = strtok(NULL, delim);

			if (argument1 == NULL || argument2 == NULL) {
				printf("Invalid arguments.\n");
				continue;
			}

			add(argument1, argument2);
			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;
			}

			remover(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 command format.\n");
				continue;
			}

			save(argument2);
		}

		else if (strcmp(command, "exit") == 0)
			break;
	}
}


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 save(char *fileName) {
	int i;
	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]);
	}
	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];
		numbers[i + 1] = numbers[i];
		i--;
	}

	names[i + 1] = strdup(name);
	numbers[i + 1] = strdup(number);
	n++;
}



char *strdup(char *s)
{
	char *p;
	p = (char *)malloc(strlen(s) + 1);
	if (p != NULL)
		strcpy(p, s);
	return p;
}


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


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


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

void remover(char *name) {

	int i = search(name); /* returns -1 if not exists */

	if (i == -1) {
		printf("No person named '%s' exists.\n", name);
		return;
	}

	int j = i;
	for (; j < n - 1; j++) {
		names[j] = names[j + 1];
		numbers[j] = numbers[j + 1];
	}

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

void reallocate()
{
	int i;

	capacity *= 2;           /* 먼저 크기가 2배인 배열들을 할당한다. */
	char **tmp1 = (char **)malloc(capacity * sizeof(char *));
	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(numbers);     /* 원본 배열 names와 numbers는 더이상 필요없다. 가비지는 free 함수를 이용해 반환한다. */

	names = tmp1;   /* names와 numbers가 새로운 배열을 가리키도록 한다. (배열의 이름은 포인터 변수이다) */
	numbers = tmp2;
}