프로그램에서 특정 부분의 로직이 유동적이라서, 어떻게 처리해야 할 지는 개발자마다 다르게 처리해야 하는 경우가 있습니다. void *는 어떤 데이터를 처리해야 할 지 모르는 경우라면, 이번에는 로직을 개발자가 원하는 대로 구현할 수 있도록 하고 싶은 경우가 있습니다.
예를들면,
#include <stdlib.h>
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
qsort(3)라는 표준함수가 있습니다. 이 함수는 데이터를 정렬하는 함수입니다. 처리하려는 데이터는 어떤 유형의 데이터 도 처리하기 위해서 위와 같이 void * 형태로 입력을 받습니다. void *는 크기를 알 수 없으므로 갯수(nmemb)와 크기(size)를 별도의 파리미터로 입력받습니다. 문제는 sorting을 할 기준이 무엇인지는 개발하는 개발자만 알 수 있습니다. 예를들면 데이터에 이름, 생일 등 다양한 데이터가 있고, 무엇을 기준으로 sorting할 지는 업무 요건을 알고 있는 개발자만이 할 수 있습니다. 그러므로 sorting하는 방식에 대한 함수는 정해져 있지 않고 유동적으로 선택할 수 있어야 합니다. 이와 같이 특정 로직이 아닌 다양한 형태의 로직이 호출되어야 하는 경우, 함수 포인터를 사용하게 됩니다.
위의 qsort에서 마지막 파라미터가 함수 포인터입니다.
int (*compar)(const void *, const void *);
이 것은 함수 포인터 변수명은 compar이고 return 타입은 int입니다. 파라미터는 void *를 두 개 넘기는 함수입니다.
함수 포인터의 활용 사례
우리가 어떤 업무를 처리할 때에 어떤 함수의 return 값에 의해서 처리해야 할 다음 로직을 처리해야 하는 경우가 있습니다. 예를들면, 아래와 같이 어떤 경우에는 대소문자 구분을 하고, 어떤 경우에는 대소문자 구분없이 비교를 해야하는 경우가 있습니다.
1). 함수 포인터를 사용하지 않은 경우
#include <string.h>
#include <stdio.h>
int main(int argc, char **argv)
{
char str1[100];
char str2[100];
int comp = 0;
......
if(strcmp(argv[1], "yes") == 0) {
comp = strcmp(str1, str2);
} else {
comp = strcasecmp(str1, str2);
}
if(comp >= 0) {
...
} else {
...
}
......
return 0;
}
2). 함수 포인터를 사용하는 경우
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
int (*compare)(const char *, const char *);
char str1[100];
char str2[100];
int flag = 0;
......
if(strcmp(argv[1], "yes") == 0) {
compare = strcmp;
} else {
compare = strcasecmp;
}
if(compare(str1, str2) >= 0) {
...
} else {
...
}
......
return 0;
}
위의 1번의 경우는 옵션에 따라서 strcmp()와 strcasecmp()를 직접호출하였습니다. 위의 2번의 경우에 옵션에 따라서 비교함수를 compare라는 함수 포인터에 대입을 하였습니다. 사실 위의 경우 함수 포인터를 사용하나 마나 장단점이 별로 없는 소스일 수 있습니다. 문제는 위의 옵션에 따라서 strcmp()와 strcasecmp()가 자주 언급해야 한다면 1번의 경우 상당히 귀찮아집니다. 한번의 함수 포인터의 대입으로 이후부터는 일관성있는 로직과 프로그램의 복잡도를 확 줄일 수 있게 됩니다. 위의 경우에는 그래도 위와 같이 분기를 통해서 업무 처리가 가능한 경우도 있습니다. 그러나 공용으로 사용하는 라이브러리(예, qsort)를 만들어서 제공하는 개발자의 경우 새로운 로직이 추가될 때마다 qsort( )함수를 수정해야 합니다.
이와 같이 특정 파라미터, 특정 return값의 함수를 저장하는 변수를 함수 포인터라고 합니다.
함수 포인터 변수 선언하기
만약, printf함수로 여러곳에 디버깅을 해서 개발을 했다가 나중에 운영 중에는 로그를 남기지 않게 하고 싶거나 또는 로그를 파일로 저장하고 싶을 수 있습니다. 그러면 처음부터 함수 포인터로 선언하여 사용하여 운영상태 등에서는 출력방식을 변경할 수 있도록합니다. 그럴려면 printf를 대입시킬 함수 포인터 변수를 선언해야 합니다. 함수 포인터 변수를 선언하는 방법을 알아보겠습니다.
다음은 함수 포인터 변수 선언하는 절차입니다.
1, 원래 함수의 프로토타입 표시
int printf(const char *fmt, ...);
2). 파라미터 변수명 없애기
int printf(const char *, ...);
3). 함수명을 괄호로 싸기
int (printf)(const char *, ...);
4). 함수명 앞에 *를 붙이기
int (*printf)(const char *, ...);
5). 함수명을 변수명으로 바꾸기
int (*print)(const char *, ...);
위와 같이 print라는 함수 포인터 변수를 만들었습니다. 변수 선언시에 변수명을 빼면 변수의 타입이 되듯이 위의 print 함수 포인터 변수 선언 한것에서 print를 빼버리면 함수 포인터의 타입이 됩니다. 가끔 타입 캐스팅을 해야 하는 경우가 있습니다. 이 경우에
print = (int (*)(const char *, ...)) printf;
처럼 사용해야 하는 경우가 있습니다.
예를들면
qsort(data, 100, sizeof(struct my_data), (int (*)(const void *, const void *)) strcmp);
처럼 strcmp를 그냥 사용하고 싶은데, strcmp의 prototype이 (int(*)(const char *, const char *)) 이므로 함수에 대한 type casting이 필요합니다.
예제). 함수 포인터를 통한 로그 처리 방법 선택하기
#include <stdio.h>
#include <string.h>
int (*log)(const char *, ...);
/*
아무런 출력이 없는 함수
*/
int no_log(const char *fmt, ...)
{
return;
}
/*
파일로 출력하는 함수
*/
int file_log(const char *fmt, ...)
{
FILE *fp;
if((fp = fopen("log.txt", "a")) == NULL) {
return -1;
}
...
}
int main(int argc, char **argv)
{
if(argc == 1) {
log = no_log;
} else if(strcmp(argv[1], "-file") == 0) {
log = file_log;
} else if(strcmp(argv[1], "-debug") == 0) {
log = printf;
} else {
log = no_log;
}
....
log("test\n");
....
return 0;
}
위와 같이 로그 처리를 다양한 방법으로 변경할 수 있숩니다. 필요시에는 db_log( )함수를 만들어서 로그의 내용을 DBMS에 저장할 수도 있을 것입니다.
함수 포인터와 함수와의 관계
포인터 변수와 배열에 대해서 예전에 살펴본적이 있습니다. 이들의 차이는 무엇인지 기억하시겠지만, 배열은 포인터 상수라고 얘기를 한적이 있습니다. 배열의 변수가 곧 그 배열의 시작 위치이며, 다른 번지를 대입할 수 없다고... 포인터 변수는 다른 배열을 대입할 수 있는 변수의 역할을 한다고 하였습니다.
마찬가지로 함수 포인터와 함수와 관계는 포인터와 배열과의 관계와 비슷합니다. 함수는 함수명이 곧 함수의 로직이 시작되는 번지이며 다른 함수를 대입할 수 없는 상수입니다. 함수 포인터는 함수의 주소를 저장하는 변수이며 함수 포인터는 별도의 메모리 번지를 가집니다.
#include <stdio.h>
#include <string.h>
int (*log)(const char *, ...);
int main(int argc, char **argv)
{
log = printf;
printf("printf = %p\n", printf);
printf("&printf = %p\n", &printf);
printf("log = %p\n", log);
printf("&log = %p\n", &log);
return 0;
}
결과>
printf = 0x400420
&printf = 0x400420
log = 0x400420
&log = 0x601048
위와 같이 함수와 함수 포인터의 메모리 번지 관계를 확인할 수 있습니다.
함수 포인터 타입형 정의하기
새로운 데이터 타입을 정의하는 방법으로는 typedef 예약어를 통하여 만듧니다. 만약에 같은 함수 포인터를 여러개 선언하거나 타입 캐스팅이 자주 발생하는 경우 길게 표현하는 것이 귀챦을 수 있습니다. 이를 위하여 함수 포인터 타입형을 정의하는 것에 대해서 알아보겠습니다.
새로운 데이터 타입 정의하는 방법은
typedef int INT32;
위와 같이 변수를 선언하는 것 처럼, 앞에 typdef를 붙이면 변수명 부분이 데이터 타입명으로 됩니다.
이후에는 INT32 i, j, k; 처럼 사용 가능합니다.
함수 포인터도 마찬가자입니다. 함수 포인터 변수 선언하듯이 앞에 typedef만 붙이고 변수명을 함수 포인터 타입명으로 바꾸어 주면 됩니다.
typedef int (*PRINTF_T)(const char *, ...);
PRINTF_T log, debug;
log = (PRINTF_T)printf;
위와 같이 log, debug 변수가 int(*)(const char *, ...)함수 포인터 변수가 됩니다.
'C언어 > 문법' 카테고리의 다른 글
27. 구조체(struct) - 비트 필드(bit field) (2) | 2019.12.05 |
---|---|
26. 구조체(struct) - 기초 (0) | 2019.12.04 |
24. 포인터(Pointer) - void * 포인터 (0) | 2019.12.02 |
23. 포인터(Pointer) - 다중 포인터 (0) | 2019.11.28 |
22. 포인터(Pointer)와 배열(Array) (6) | 2019.11.27 |