이번 포스팅에서 다룰 전화번호부는 이름, 전화번호 외에 이메일 주소, 등등의 여러 항목을 추가시켜보겠습니다. 이전의 전화번호부에서는 이름과 전화번호만 저장하는 방식이었기에 단순히 names [] 배열과 numbers [] 배열 이렇게 각각의 두 배열에 데이터를 저장했습니다. 하지만 이름, 전화번호 외에 이메일 주소, 또 그룹 등등 한 사람에 대해 저장해야 하는 데이터가 많아지게 되면, 이전 전화번호부처럼 이름 배열, 번호 배열, 이메일 배열, 그룹 배열 등으로 각각의 배열을 만드는 것보다는 하나의 구조체를 만들어서 한 개인에게 속하는 데이터를 관리하는 것이 효율적입니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define CAPATICY 100
#define BUFFER_LENGTH 100
typedef struct person // typedef -> renaming
{
char* name;
char* number;
char* email;
char* group;
} Person; // person 구조체를 선언할 때 Person만 선언해도 상관 x
Person directory[CAPATICY]; // Person 타입의 배열
int n = 0; // 전화번호부에 저장된 사람의 수
int read_line(FILE* fp, char str[], int n) // 키보드 입력인 경우 파일 포인터 자리에 stdin 입력, 저장할 배열과 배열의 크기를 줌.
{
int ch, i = 0;
while ((ch = fgetc(fp) != '\n' && ch != EOF)) // 엔터키를 입력 받기 전까지의 문자열을 fp에 저장. 파일이 끝날 때 까지
if (i < n)
str[i++] = ch;
str[i] = '\0';
return i;
}
read_line 함수가 이전의 전화번호부와 다른 점은, 이제는 키보드 뿐만 아니라 파일을 통해서도 문자열을 읽고 저장한다는 점입니다. 때문에 파일 포인터 fp와, 문자열을 저장할 배열 str []과, 배열의 길이 n을 read_line 함수의 매개변수로 받습니다. 그리고 이전의 read_line에서는 문자열을 받을 때 getchar() 함수를 이용했다면, 전화번호부 v.4의 read_line 함수는 파일로부터 문자열을 읽는 fgetc()함수를 사용합니다. 전화번호부가 저장될 파일은 줄 바꿈 문자로 데이터를 구분할 것이기 때문에 줄 바꿈 문자를 읽거나, 파일의 끝에 도달하면 리턴하는 EOF를 반환받기 전까지 파일로부터 문자열을 읽습니다. 그렇게 while문을 통해 문자를 읽은 뒤에는, str의 마지막에 null 문자열을 저장해 완전한 C 스타일의 문자열로 만들어주고, str의 길이인 i를 리턴합니다.
void load(char* fileName)
{
char buffer[BUFFER_LENGTH];
char* name, * number, * email, * group; // 이름, 번호, 이메일, 그룹을 저장할 문자열(string literal이라 변경 x)
FILE* fp = fopen(fileName, "r");
if (fp == NULL)
{
printf("Open failed.\n");
return;
}
while (1)
{
if (read_line(fp, buffer, BUFFER_LENGTH) <= 0) // 한 번에 한 줄을 읽어서 가져옴
break;
name = strtok(buffer, "#"); // #을 구분자로 사용
number = strtok(NULL, "#"); // 하위 3개는 입력 값이 없다면 그냥 " " 공백으로 저장
email = strtok(NULL, "#");
group = strtok(NULL, "#");
add(name, number, email, group);
}
fclose(fp);
}
읽을 전화번호부 파일을 불러올 load 함수입니다. 전화번호부 파일로부터 한 줄 단위로 읽어오는 문자열을 저장할 buffer 배열을 선언해주고, 다음으로는 이름, 번호, email, 그리고 그룹을 저장할 char 포인터를 선언합니다. char 포인터는 곧 char 자료형 배열의 시작 주소입니다.
그다음으로는 파일 포인터를 통해 파일을 읽고, strtok를 통해 한 줄씩 tokenize를 통해 각각의 정보를 미리 선언한 char 자료형 변수에 저장하여 add 함수를 통해 directory에 저장합니다.
void add(char* name, char* number, char* email, char* group)
{
int i = n - 1;
while (i >= 0 && strcmp(directory[i].name, name) > 0) // 알파벳 순으로 이름을 정렬
{
directory[i + 1] = directory[i];
i--;
}
directory[i + 1].name = strdup(name); // strdup를 통해 동적 메모리 할당
directory[i + 1].number = strdup(number); // 데이터가 없는 경우에는 공백으로 저장
directory[i + 1].email = strdup(email);
directory[i + 1].group = strdup(group);
n++;
}
read 함수에서 name, number, email, group을 받아와 directory에 저장하는 add 함수입니다. add 함수는 read 모드와 add 모드에서 사용됩니다.
포인트는 이전 전화번호부처럼 알파벳 순으로 이름 정렬을 하고, strdup를 통해 동적 메모리 할당을 받은 데이터 각각의 주소를 directory 배열에 저장합니다.
int compose_name(char str[], int limit) // 입력받은 이름을 압축하여 저장하는 compose_name
{
char* ptr;
int length = 0; // 이름 문자열의 길이
ptr = strtok(NULL, " "); // 두 번째 토크나이징. 명령어 다음의 문자열을 저장
if (ptr == NULL) // 이름이 없다면 문자열의 길이를 0으로 리턴
return 0;
strcpy(str, ptr); // str에 토크나이징된 문자열을 저장. strtok에 의해 null 문자도 포함함
length += strlen(ptr); // 첫 번째 토큰의 길이
while ((ptr = strtok(NULL, " ")) != NULL)
{
if (length + strlen(ptr) + 1 < limit) // 오버 플로우 방지. NULL 문자열 때문에 +1
{
str[length++] = ' '; // 이름과 성 사이의 공백 생성
str[length] = '\0'; // strcat은 두 문자열이 모두 완전한 문자열. 즉 배열의 끝이 null 문자로 있을 때에만 동작
strcat(str, ptr); // string cat
length += strlen(ptr);
}
}
return length;
}
void handle_add(char* name)
{
char number[BUFFER_LENGTH], email[BUFFER_LENGTH], group[BUFFER_LENGTH];
char empty[] = " "; // 입력을 받지 않았을 때 대체 저장할 공백 문자 배열
printf(" PHONE: ");
read_line(stdin, number, BUFFER_LENGTH); // 키보드로부터 읽으므로 파일 포인터 -> stdin
printf(" EMAIL: ");
read_line(stdin, email, BUFFER_LENGTH);
printf(" GROUP: ");
read_line(stdin, group, BUFFER_LENGTH);
add(name, (char*)(strlen(number) > 0 ? number : empty), (char*)(strlen(email) > 0 ? email : empty), (char*)(strlen(group) > 0 ? group : empty));
// 이름은 매개변수로 받은 이름을 그대로 대입, 나머지 데이터들은 데이터가 입력되지 않았다면 empty 배열의 주소 저장
}
void save(char* fileName)
{
int i;
FILE* fp = fopen(fileName, "w");
if (fp == NULL)
{
printf("Open failed.\n");
return;
}
for (int i = 0; i < n; i++)
{
fprintf(fp, "%s#", directory[i].name);
fprintf(fp, "%s#", directory[i].number);
fprintf(fp, "%s#", directory[i].email);
fprintf(fp, "%s#", directory[i].group);
}
fclose(fp);
}
int search(char* name)
{
for (int i = 0; i < n; i++)
{
if (strcmp(name, directory[i].name) == 0) // 동일하다면 배열 인덱스 리턴
return i;
}
return -1; // 같은 값이 없다면 -1 리턴
}
void print_person(Person p) // 화면 상에 찾는 사람의 정보 출력
{
printf("%s: \n", p.name);
printf(" Phone: %s: \n", p.number);
printf(" Email: %s: \n", p.email);
printf(" Group: %s: \n", p.group);
}
void remove(char* name)
{
int i = search(name); // search 함수를 통해 삭제하려는 배열의 index 리턴
if (i == -1) // directory에 매칭되는 이름이 없을 때
{
printf("No person named %s exists.\n", name);
return;
}
int j = i;
for (; j< n - 1; j++)
{
directory[j] = directory[j + 1];
}
n--;
printf("%s was deleted successfully. \n", name);
}
void status() // 현재 저장된 정보를 모두 출력
{
for (int i = 0; i < n; i++)
{
print_person(directory[i]); // for문으로 처음부터 끝까지 directory 출력
}
printf("Total %d people.\n", n);
}
void find(char* name)
{
int index = search(name);
if (index == -1)
printf("No person named %s exists.\n", name);
else
print_person(directory[index]);
}
int main()
{
char command_line[BUFFER_LENGTH];
char* command, * argument;
char name_str[BUFFER_LENGTH];
while (1)
{
printf("$ ");
if (read_line(stdin, command_line, BUFFER_LENGTH) <= 0) // 키보드로부터 입력 받음. 실제로 입력받은 문자 수가 0보다 작음 = 아무것도 입력 안 하고 엔터를 누름
continue;
command = strtok(command_line, " "); // " "을 기준으로 command_line을 tokenize
if (strcmp(command, "read") == 0)
{
argument == strtok(NULL, " ");
if (argument == NULL) // 파일 이름이 없다면 continue에 의해 while문 다시 시작
{
printf("Invalid arguments.\n");
continue;
}
load(argument);
}
else if (strcmp(command, "add") == 0)
{
if (compose_name(name_str, BUFFER_LENGTH) <= 0) // compose_name을 통해 나머지 토큰을 압축해서 name_str에 이름 저장
{
printf("Name required.\n");
continue;
}
handle_add(name_str);
}
else if (strcmp(command, "find") == 0)
{
if (compose_name(name_str, BUFFER_LENGTH) <= 0) // compose_name을 통해 나머지 토큰을 압축해서 name_str에 이름 저장
{
printf("Name required.\n");
continue;
}
find(name_str);
}
else if (strcmp(command, "status") == 0) // 현재 상태
status();
else if (strcmp(command, "delete") == 0)
{
if (compose_name(name_str, BUFFER_LENGTH) <= 0) // compose_name을 통해 나머지 토큰을 압축해서 name_str에 이름 저장
{
printf("Invalid arguments.\n");
continue;
}
remove(name_str);
}
else if (strcmp(command, "save") == 0)
{
argument = strtok(NULL, " ");
if (strcmp(argument, "as") != 0)
{
printf("Invalid arguments.\n");
continue;
}
argument = strtok(NULL, " ");
if (argument == NULL)
{
printf("Invalid arguments.\n");
continue;
}
save(argument); // 마지막으로 tokenize된 이름의 파일에 저장!
}
else if (strcmp(command, "exit"))
break;
}
return 0;
}
'Information Technology > C' 카테고리의 다른 글
[C언어] 연결리스트 - 개념과 기본 동작들(2) (0) | 2020.01.09 |
---|---|
[C언어] 연결리스트 - 개념과 기본 동작들(1) (0) | 2020.01.08 |
[C언어] 전화번호부 v.3 (0) | 2020.01.06 |
[C언어] 전화번호부 v.2 (0) | 2020.01.06 |
[C언어] 전화번호부 v.1 (0) | 2020.01.06 |