💡 자료구조

전화번호부 v5.0 - 구조체에 대한 포인터, 동적 메모리 할당

ji-hyun 2021. 7. 11. 22:47
typedef struct person {
	char *name;
	char *number;
	char *email;
	char *group;
} Person;

Person directory[CAPACITY];   /* 구조체 배열 */
int n = 0;        /* number of people in phone directory */



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



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

 

Person directory[CAPACITY]; 는 Person 타입의 배열이다. status() 함수에서 print_person() 인자에 배열 한개씩 넘겨준다. print_person 에서는 그 배열 한개가 Person 타입(구조체 타입)인 것이다.

 

사실 이때 이 둘은 같은 변수가 아니다. 그냥 넘겨주는 것 같지만 사실은 복사가 되서 넘겨주는 것이다.

 

 

구조체의 복사본이 매개변수로 전달되는 것이다. 호출 시에 모든 멤버(이름, 전화번호, ..)들이 일일히 복사된다.

 

 

 

 

 

void some_function() {
 … 
 Person thePerson = get_person(“John”); 
 …
}



Person get_person(char *name){
	...
    	return directory[i];
}

 

return 문에서도 같다. get_person() 함수의 작동 순서는 name 문자열을 받으면 그에 해당되는 directory[ i ] 를 찾아 return을 해야 한다. 이때 바로 넘겨주는 것이 아니다.

 

 

 

return을 하게 되면 이름 없는 임시 객체가 생성하게 된다.

 

 

 

 

return이 되면 이름 없는 복사본이 복사가 되는 것이다. 이것을 thePerson에 넘겨준다. 이것은 thePerson에 넘겨주기까지 두 번의 복사과정이 일어나게 된다.

 

 

 

 

add() 함수에서도 똑같다. 알파벳 순으로 정렬된 배열에 사람 한명을 삽입하려면 나머지 사람들을 한 칸씩 뒤로 옮겨야 하는데 이때도 구조체 멤버들(이름, 전화번호, ...) 하나하나를 복사시켜야 한다.

 

 

따라서 전화번호부 v5.0은 전화번호부 v4.0의 이런 한계를 극복하기 위해서 구조체 포인터를 사용할 것이다.

 

 

typedef struct person {
 char *name;
 char *number;
 char *email;
 char *group;
} Person;

	Person * directory[CAPACITY];
	int n = 0;

 

이전에는 person 타입의 배열이었다면 v5.0에서는 preson * 타입이다. 이해를 돕기 위해 그림을 가져왔다.

 

 

 

이전에는 directory 의 각 칸이 person 타입이었다. 하지만 person * 타입으로 바뀌게 되면서 directory의 각 칸은 person * 타입이다. (=각 칸이 주소를 저장하는 포인터 변수이다.)

 

 

 

 

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



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

 

directory 의 각 칸이 포인터 타입이기 때문에 print_person 의 매개변수는 포인터변수가 된다. 하지만 이것도 복사가 이루어지긴 한다.(C언어의 특성 : call-by-value) 그러나 이때는 주솟값이 복사가 된다는 점에서 이전 버전과 차이가 있다.

 

(*p).name 에서 *p 는 p가 가리키고 있는 값이다. 이때 괄호를 써주는 이유는 점 연산자가 별표 연산자보다 우선순위가 높기 때문이다. 

 

 

(*p).name은 C언어에서 -> 연산자를 사용해서 표현이 간결하게 해준다.

 

 

 

 

 


배열 재할당을 통해 크기를 키우기

 

#define INIT_CAPACITY 100

typedef struct {
 	char *name;
 	char *number;
 	char *email;
 	char *group;
} Person;


Person ** directory;
int capacity;
int n; 


void init() {
 directory = (Person **)malloc(INIT_CAPACITY*sizeof(Person *));
 capacity = INIT_CAPACITY;
 n = 0;
}

 

구조체를 포인터로 사용해야 하는 것은 위에서 배웠었고 여기서는 배열 재할당을 통해 크기를 키울 것이다. 전 게시물에서 malloc 함수를 통해 크기를 키우는 법을 배웠었다.

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

 

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

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

ts2ree.tistory.com

 

 

directory의 각 칸은 person * 타입이고 동적할당 malloc 함수를 써야하므로 이중포인터를 선언해야한다. 밑에 그림을 같이 보면 이해하기 쉬울 것 같다.

 

 

동적할당 malloc 함수를 쓰려면 시작 주소를 반환해야 한다. 그래서 이중 포인터가 되는 것이다.

 

 

 

 

 

 

void add(char *name, char *number, char *email, char *group) {
 	if (n>=capacity)
 		reallocate();  /* 사람 수가 용량을 초과하면 재할당을 해준다. */
        
	int i=n-1;
 	while (i>=0 && strcmp(directory[i]->name, name) > 0) {
 		directory[i+1] = directory[i];
 		i--;
 	}
    
 	directory[i+1] = (Person *)malloc(sizeof(Person));  /* 설명 참고 */
 
 	directory[i+1]->name = name;   /* directory의 각 칸은 포인터이다. */
 	directory[i+1]->number = number;
 	directory[i+1]->email = email;
 	directory[i+1]->group = group;
 
 	n++;
}

 

add 함수는 사람을 추가하기 위해서는 한 칸씩 뒤로 이동해야하는데 이때 추가할 부분은 아무것도 없는 빈 공간이 된다. 따라서 malloc 함수를 통해 구조체 타입을 정의해준 것이다.

 

 

 

 

 

 

void remove(char *name) {
 	int i = search(name); /* returns -1 if not exists */
 	if (i == -1) {
 		printf("No person named '%s' exists.\n", name);
 		return;
 }
 
 	Person *p = directory[i];
 	for (int j=i; j<n-1; j++) 
 		directory[j] = directory[j+1];
 	n--;
 	release_person(p);
 	printf("'%s' was deleted successfully. \n", name);
}

void release_person(Person *p) {
 	free(p->name);
 	if (p->number != NULL) free(p->number);
 	if (p->email != NULL) free(p->email);
 	if (p->group != NULL) free(p->group);
 	free(p);
}

 

directory의 각 칸은 동적할당으로 형성되었다고 봐도 무방하다. 멤버들 모두 strdup 함수를 써서 만들었다는 것은(handle_add 함수에서 strdup 사용) 결국 strdup 함수 내부에 malloc 함수가 존재하는 것이다. remove 함수로 지워질 해당 멤버들은 garbage 로 남기 때문에 free 해주는 것이 좋다. 그래서 지워질 p 만 free 시켜주는 것이 아닌 p의 멤버에 대해서도 free를 한다.