F.R.I.D.A.Y.

함수 포인터를 배워야 하는 이유 : 코드 간결화 본문

DEV/C C++

함수 포인터를 배워야 하는 이유 : 코드 간결화

F.R.I.D.A.Y. 2019. 11. 1. 23:41
반응형

 많은 사람들이 C언어를 배우기 시작하다가 중간에 막히는 부분이 있습니다. 대표적으로 포인터가 있는데요, 이번에 배울 것 또한 포인터입니다. 이번에 배울 포인터는 기존의 포인터와는 약간 다른, 함수 포인터입니다.

 

# 코드를 직접 따라 작성해보세요.

# 설명하는 기술 자체가 입문자들이 사용하기엔 부담이 되는 기술입니다. 이해가 되지 않아서 좌절하지 마시고 '이런 기술이 있다'정도로만 알아두셔도 좋습니다.

 

 


포인터

 함수 포인터란 무엇일까요? 기존의 포인터[#포인터알아보기 포인터에 대한 내용은 다음 글을 참고하세요.]가 변수의 메모리 주소를 값으로 가지는 자료형이라 한다면, 함수 포인터는 함수들의 시작 주소를 값으로 가지는 자료형이라 생각하면 되겠습니다.

 함수 포인터는 기본적으로 다음과 같이 선언합니다.

#include <stdio.h>

void test(){
    printf("함수 포인터 예시\n");
}

int main(void){

    void (*fp)(); // 변수명 : fp
    
    fp = test;
    
    fp();
    
    return 0;
}

 이렇게 변수 fp를 선언하게 되면 매개변수가 없고 반환형이 void인 함수를 가리킬 수 있는 함수 포인터가 생성되는 것입니다.

 매개변수 있는 함수 포인터를 작성할 때는 아래와 같이 작성하면 됩니다.

반환타입 (*변수명)(매개변수 자료형);

 사용할 때도 간단합니다. 변수명에 괄호()를 붙여서 함수처럼 이용하면 됩니다. 매개변수를 넘기고 싶다면 괄호 안에 함수 쓰듯 작성해주면 됩니다.

#include <stdio.h>

void test(int a){
	printf("%d\n", a);
}

int main(void){
	void (*fp)(int) = test;
	fp(5);
}
더보기

# 참고

 실제 함수 포인터의 사용은 아래 코드처럼 작성합니다.

void (*fp)(void); // 함수 포인터 선언

(*fp)(); // 함수 포인터 사용

 그러나 위와 같이 작성하지 않아도 되는 것은, 이렇게 작성해도 작동하도록 지원하고 있기 때문입니다.


 함수 포인터는 프로그램을 만들 때 굳이 필요하지는 않은 기술이라고 생각합니다. 유지/보수를 하지 않는 프로그램을 만들게 된다면요. 다른 말로 하면 좋은 프로그램(코드)을 만들기 위해서는 이 기술이 필수라는 이야기입니다.

 어째서 필요한지는 아래 예시 코드를 이용해 알아보겠습니다.


배우기

 아래 <더보기>로 축소되어있는 코드는 이 링크(BAEKJOON 10845 : 큐 for C)에서 따온 코드입니다.

더보기
#include <stdio.h>
#include <stdlib.h>

typedef struct{
	int *arr;     // 데이터 메모리 포인터
	int maxLength;// 가용 가능한 공간 길이
	int length;   // 현재 데이터가 담긴 길이
}queue; // 큐의 정보를 담은 스트럭처

queue* newQueue(int maxLength){
	queue* temp = (queue *)malloc(sizeof(queue));
	temp->arr = (int *)malloc(sizeof(int) * maxLength);
	temp->maxLength = maxLength;
	temp->length = 0;
	return temp;
} // 새로운 큐를 생성하는 함수; 반환값 : 만들어진 큐의 주소

int delQueue(queue *q){
	if(q->arr && q->maxLength){
		free(q->arr);
		free(q);
		return 1;
	}
	return 0;
} // 생성되어있는 큐 삭제; 반환값: 삭제 성공 1, 삭제 실패(안함) 0

int push(queue* q, int value){
	if(q->maxLength <= q->length){
		q->maxLength *= 2;
		int *arr = (int *)malloc(sizeof(int) * q->maxLength);
		for(int i = 0 ; i < q->length;++i){
			arr[i] = q->arr[i];
		}
		free(q->arr);
		q->arr = arr;
	}
	q->arr[q->length++] = value;
	return 1;
} // 큐에 값 넣기; 반환값: 1

int pop(queue* q){
	if(q->length < 1){
		return -1;
	}else{
		int ret = q->arr[0];
		for(int i = 1; i < q->length;++i){
			q->arr[i-1] = q->arr[i];
		}
		q->length--;
		return ret;
	}
} // 큐에서 선입 값 뽑기; 반환값: 선입 값

int size(queue* q){
	return q->length;
} // 큐의 현재 길이 뽑기; 반환값: 큐의 현재 길이

int empty(queue* q){
	return q->length == 0;
} // 큐의 저장 상태 확인; 반환값: 큐에 임의의 값 저장됨 1, 큐가 비어있음 0

int front(queue* q){
	if(!q->length){
		// if length of q is zero
		return -1;
	}
	return q->arr[0];
} // 큐의 선입 값 확인; 반환값: 선입 값

int back(queue* q){
	if(!q->length){
		// if length of q is zero
		return -1;
	}
	return q->arr[q->length-1];
} // 큐의 최후입 값 확인; 반환값: 최후입 값

int main(void){
	queue* q = newQueue(10);
	
	int cmdCount;
	scanf("%d", &cmdCount);
	
	while(cmdCount--){
		char str[6];
		scanf("%s", str);
		int sum = 0;
		for(int i = 0 ; str[i]; ++i){
			sum += str[i];
		}
		
		switch(sum){
			case 448: // push
				{
					int num;
					scanf("%d", &num);
					push(q, num);
				}
				break;
			case 335: // pop
				printf("%d\n", pop(q));
				break;
			case 443: // size
				printf("%d\n", size(q));
				break;
			case 559: // empty
				printf("%d\n", empty(q));
				break;
			case 553: // front
				printf("%d\n", front(q));
				break;
			case 401: // back
				printf("%d\n", back(q));
				break;
		}
	}
	delQueue(q);
	return 0;
}

 

 이 코드는 충분히 문제의 답이 될 수 코드입니다. 이 코드를 잘 보면 입력된 명령어에 대해 분기를 나누어서(여기에선 switch 구문 사용) 그에 맞게 대응하고 있습니다.

 이 코드를 분기할 필요 없이 만들어보겠습니다. 불가능하다고 느끼실지 모르겠지만 이 함수 포인터를 이용한다면 충분히 가능한 이야기입니다.

Step 1. 코드 재정렬

 함수 포인터를 사용하기 위해선 먼저 아래와 같이 코드 재정렬을 진행해야 합니다. switch 구문을 볼까요?

	switch(sum){
		case 448: // push
			{
				int num;
				scanf("%d", &num);
				push(q, num);
			}
			break;
		case 335: // pop
			printf("%d\n", pop(q));
			break;
		case 443: // size
			printf("%d\n", size(q));
			break;
		case 559: // empty
			printf("%d\n", empty(q));
			break;
		case 553: // front
			printf("%d\n", front(q));
			break;
		case 401: // back
			printf("%d\n", back(q));
			break;
	}

 모두 사용자로부터 입력받거나 출력하는 함수 존재합니다. 이러한 코드를 각 함수(push, pop, size, empty, front, back) 안으로 넣어줍니다. 예외적으로 push 함수는 main 함수에서 값을 넘겨주므로 매개변수 개수도 조정해주어야 합니다.

 

 이렇게 조정을 끝내고 나면 각 함수는 아래와 같이 구성됩니다.

int push(queue* q){
	int value;
	scanf("%d", &value);
	
	if(q->maxLength <= q->length){
		q->maxLength *= 2;
		int *arr = (int *)malloc(sizeof(int) * q->maxLength);
		for(int i = 0 ; i < q->length;++i){
			arr[i] = q->arr[i];
		}
		free(q->arr);
		q->arr = arr;
	}
	q->arr[q->length++] = value;
	return 1;
} // 큐에 값 넣기; 반환값: 1

int pop(queue* q){
	int ret;
	if(q->length < 1){
		ret = -1;
	}else{
		ret = q->arr[0];
		for(int i = 1; i < q->length;++i){
			q->arr[i-1] = q->arr[i];
		}
		q->length--;
	}
	printf("%d\n", ret);
} // 큐에서 선입 값 뽑기; 반환값: 선입 값

int size(queue* q){
	printf("%d\n", q->length);
} // 큐의 현재 길이 뽑기; 반환값: 큐의 현재 길이

int empty(queue* q){
	printf("%d\n", q->length == 0);
} // 큐의 저장 상태 확인; 반환값: 큐에 임의의 값 저장됨 1, 큐가 비어있음 0

int front(queue* q){
	int ret;
	if(!q->length){
		// if length of q is zero
		ret = -1;
	}else{
		ret = q->arr[0];
	}
	printf("%d\n", ret);
} // 큐의 선입 값 확인; 반환값: 선입 값

int back(queue* q){
	int ret;
	if(!q->length){
		// if length of q is zero
		ret = -1;
	}else{
		ret = q->arr[q->length-1];
	}
	printf("%d\n", ret);
} // 큐의 최후입 값 확인; 반환값: 최후입 값

 이제 main 함수에서 필요 없는 부분을 고칩니다. switch문에서 사용하는 scanf()와 printf()들은 각 함수 안으로 들어갔으니 이 함수들은 없애고 여섯 개의 함수만 작성하도록 수정합니다.

	switch(sum){
		case 448: // push
			push(q);
			break;
		case 335: // pop
			pop(q);
			break;
		case 443: // size
			size(q);
			break;
		case 559: // empty
			empty(q);
			break;
		case 553: // front
			front(q);
			break;
		case 401: // back
			back(q);
			break;
	}

Step 2. 함수 구조 통일

 이제 여섯 개의 함수(push, pop, empty, front, back)의 원형을 비교해보겠습니다.

함수명 반환형 매개변수
push int queue*
pop int queue*
size int queue*
empty int queue*
front int queue*
back int queue*
더보기

# 코드로 비교하기

코드로 비교하면 아래와 같습니다.

int  push(queue* var);
int   pop(queue* var);
int  size(queue* var);
int empty(queue* var);
int front(queue* var);
int  back(queue* var);

 원형이 모두 같습니다.

 다행스럽게도 이번 코드는 이전 <Step 1. 코드 재정렬>을 통해 함수 원형이 모두 같아졌습니다.

 

 이제 main 함수를 고쳐보겠습니다.

 

Step 3. main 함수 고치기

 이번 설명은 분기를 하지 않고 여러 함수를 다양하게 사용할 수 있음을 보이기 위함이었습니다. 그럼 이번 포스트의 설명인 함수 포인터를 이용해 진행해보겠습니다. 이전 <Step 1>과 <Step 2>는 이번 단계를 위함이었습니다.

 

 이 기술을 사용하기 위해서는 어쩔 수 없는 메모리 낭비가 필요합니다. 요즘 컴퓨터는 메모리는 넘쳐나니 4KB 정도 사용하다고 해서 문제가 되지는 않지요.

더보기

 요즘은 극악의 효율성(메모리 사용량 ↓, 속도 ↑)의 두 마리 토끼를 잡기보단 메모리 사용량은 늘려 속도 향상에 기여하는 것으로 방향을 잡습니다. 남의 프로그램이 1GB씩 사용하는데 우리 프로그램이 조금 아낀다고 크게 달라지는 것도 아니고, 요즘 컴퓨터의 메모리는 대부분 4G에서 8G, 넘게는 16G 이상의 메모리를 장착하니 큰 걱정 없습니다.

  우리는 여기에서 시작할 겁니다.

int main(void){
    queue *q = newQueue(10);
    
    int cmdCount;
    scanf("%d", &cmdCount);
    
    while(cmdCount--){
    	char str[6];
        scanf("%s", str);
        int sum = 0;
        for(int i =0 ; str[i]; ++i){
        	sum += str[i];
        }
        
    }
    
    return 0;
}

 우리는 예시 코드를 가져온 곳에서 각 명령어에 맞는 숫자를 알아보았습니다.

 가장 큰 수가 559로군요? 넉넉히 600만큼의 함수 포인터를 만들어줍니다. 그리고 명령어에 맞는 인덱스에 함수를 대입해줍니다.

	int (*fp[600])(queue*);
	fp[448] = push;
	fp[335] = pop;
	fp[443] = size;
	fp[559] = empty;
	fp[553] = front;
	fp[401] = back;

 이제 이 함수 포인터를 사용해줍니다. 함수 포인터를 사용하고 난 뒤의 main 함수 내부는 아래와 같습니다.

int main(void){
    queue *q = newQueue(10);
    
	int (*fp[600])(queue*);
	fp[448] = push;
	fp[335] = pop;
	fp[443] = size;
	fp[559] = empty;
	fp[553] = front;
	fp[401] = back;
	
    int cmdCount;
    scanf("%d", &cmdCount);
    
    while(cmdCount--){
    	char str[6];
        scanf("%s", str);
        int sum = 0;
        for(int i =0 ; str[i]; ++i){
        	sum += str[i];
        }
        
        fp[sum](q);
		
    }
    
    return 0;
}

 이제 프로그램을 빌드 후 실행해보면 이전 코드와 같이 정상적으로 돌아감을 알 수 있습니다.

 

 모두 수정된 코드는 더보기를 눌러 확인할 수 있습니다.

더보기
#include <stdio.h>
#include <stdlib.h>

typedef struct{
	int *arr;     // 데이터 메모리 포인터
	int maxLength;// 가용 가능한 공간 길이
	int length;   // 현재 데이터가 담긴 길이
}queue; // 큐의 정보를 담은 스트럭처

queue* newQueue(int maxLength){
	queue* temp = (queue *)malloc(sizeof(queue));
	temp->arr = (int *)malloc(sizeof(int) * maxLength);
	temp->maxLength = maxLength;
	temp->length = 0;
	return temp;
} // 새로운 큐를 생성하는 함수; 반환값 : 만들어진 큐의 주소

int delQueue(queue *q){
	if(q->arr && q->maxLength){
		free(q->arr);
		free(q);
		return 1;
	}
	return 0;
} // 생성되어있는 큐 삭제; 반환값: 삭제 성공 1, 삭제 실패(안함) 0

int push(queue* q){
	int value;
	scanf("%d", &value);
	
	if(q->maxLength <= q->length){
		q->maxLength *= 2;
		int *arr = (int *)malloc(sizeof(int) * q->maxLength);
		for(int i = 0 ; i < q->length;++i){
			arr[i] = q->arr[i];
		}
		free(q->arr);
		q->arr = arr;
	}
	q->arr[q->length++] = value;
	return 1;
} // 큐에 값 넣기; 반환값: 1

int pop(queue* q){
	int ret;
	if(q->length < 1){
		ret = -1;
	}else{
		ret = q->arr[0];
		for(int i = 1; i < q->length;++i){
			q->arr[i-1] = q->arr[i];
		}
		q->length--;
	}
	printf("%d\n", ret);
} // 큐에서 선입 값 뽑기; 반환값: 선입 값

int size(queue* q){
	printf("%d\n", q->length);
} // 큐의 현재 길이 뽑기; 반환값: 큐의 현재 길이

int empty(queue* q){
	printf("%d\n", q->length == 0);
} // 큐의 저장 상태 확인; 반환값: 큐에 임의의 값 저장됨 1, 큐가 비어있음 0

int front(queue* q){
	int ret;
	if(!q->length){
		// if length of q is zero
		ret = -1;
	}else{
		ret = q->arr[0];
	}
	printf("%d\n", ret);
} // 큐의 선입 값 확인; 반환값: 선입 값

int back(queue* q){
	int ret;
	if(!q->length){
		// if length of q is zero
		ret = -1;
	}else{
		ret = q->arr[q->length-1];
	}
	printf("%d\n", ret);
} // 큐의 최후입 값 확인; 반환값: 최후입 값

int main(void){
    queue *q = newQueue(10);
    
    int (*fp[600])(queue*);
    fp[448] = push;
    fp[335] = pop;
    fp[443] = size;
    fp[559] = empty;
    fp[553] = front;
    fp[401] = back;
	
    int cmdCount;
    scanf("%d", &cmdCount);
    
    while(cmdCount--){
    	char str[6];
        scanf("%s", str);
        int sum = 0;
        for(int i =0 ; str[i]; ++i){
        	sum += str[i];
        }
        
        fp[sum](q);
		
    }
    
    return 0;
}



마치며

 직접 작성해보니 적어도 main 함수 부분은 함수 포인터를 사용한 부분이 더 깔끔하지 않던가요? 프로그램은 분기를 많이 하면 할수록 속도가 느려지는 것이 사실입니다. 이러한 점을 인지하고 있다면 조금이라도 더 빠른 프로그램을 만들 수 생각해요.

 함수 포인터의 좋은 점은 상당히 다양합니다. 여러분도 함수 포인터의 매력에 한번 빠져보셨으면 좋겠네요!

 


더 알아보기

 

 

함수 포인터를 배워야 하는 이유2 : callback 함수

우리나라 많은 사람들이 이용하는 Windows 운영체제는 주기적으로 업데이트를 거쳐 다양한 기능을 추가로 제공합니다. 운영체제를 다시 설치하지 않는데 어떻게 새로운 기능이 추가될 수 있을까요? # 이전 <함수..

pang2h.tistory.com

 제가 많은 도움을 받은 블로거 김성엽님께서 함수 포인터에 대한 더 자세한 글을 작성하셨으니 관심 있다면 아래 포스트를 참고해보세요 :)

 

함수 포인터에 대하여

: C 언어 관련 전체 목차http://blog.naver.com/tipsware/221010831969​1. 데이터 포인터C 언어를 배운 ...

blog.naver.com

# index

728x90
반응형

'DEV > C C++' 카테고리의 다른 글

주가 스팬 계산하기  (1) 2019.11.14
여러 괄호를 사용하는 VPS 찾기  (0) 2019.11.13
malloc VS calloc  (1) 2019.10.28
키워드와 예약어  (0) 2019.08.08
scanf()의 문제점  (0) 2019.06.25
Comments