일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- C언어
- list
- 아두이노 소스
- html
- 수광 소자
- 컴퓨터 그래픽스
- 아두이노 컴파일러
- set
- Algorithm
- 시스템프로그래밍
- Visual Micro
- Deque
- 자료구조
- queue
- Arduino
- 아두이노
- vector
- LineTracer
- WinAPI
- map
- c++
- priority_queue
- 운영체제
- Stack
- directx
- arduino compiler
- stl
- 라인트레이서
- 통계학
- Array
- Today
- Total
Kim's Programming
C언어 - 포인터 고급(2/4) 본문
함수 포인터
함수 포인터의 정의
함수포인터(Pointer To Function)란 함수를 가리키는 포인터를 말합니다. 포인터란 본래 메모리상의 번지를 저장하는 변수인데 함수도 메모리에 존재하며 시작 번지가 있으므로 포인터 변수로 가리킬 수 있습니다. 일반적인 포인터는 변수가 저장되어 있는 번지를 가리키지만 함수 포인터는 함수의 시작 번지를 가리킨다는 점에서 다릅니다. 함수 포인터와 구분하기 위해서는 변수를 가리키는 일반적인 포인터를 특별히 데이터 포인트라고 부르기도 합니다. 정수형을 가리키는 int *pi는 정수형 변수의 번지를 가지며 실수형을 가지는 double *pd는 실수형 변수의 번지를 가집니다. 데이터 포인터는 단순히 가리키는 대상체의 타입만 밝히면 되므로 선언 형식이 간단합니다. 반면 함수 포인터는 대상체인 함수의 형식이 좀 더 복잡하기 떄문에 선언형식도 다소 복잡하게 됩니다. 대상체가 되는 함수의 리턴 타입과 인수들의 목록까지도 같이 밝혀야 합니다. 함수 포인터를 선언하는 형식은 함수의 원형 선언 형식과 유사합니다. 다음은 함수 포인터의 기본형식입니다.
리턴타입(*변수명)(인수의 목록):
함수의 원형을 써 놓고 함수명을 변수명으로 바꾸고 앞에 *을 붙인후 *변수명을 괄호로 싸면 됩니다. 함수 포인터도 변수이므로 고유한 이름을 가져야 하며 명칭 규칙에 맞게 적당히 이름을 붙여야 합니다. 리턴값과 인수 목록은 그대로 유지하되 단 형식 인수의 이름은 생략해도 상관이 없습니다. 함수 원형 선언에서 형식 인수의 이름이 별 의미가 없듯이 함수 포인터를 선언할 때도 인수의 타입만 의미가 있습니다. 다음 함수를 가리키는 함수포인터를 선언한다고 하겠습니다.
int function(int a)
정수형 인수를 하나 취하며 정수형을 리턴하는 function이라는 함수의 원형입니다. 이런 함수를 가리킬 수 있는 함수 포인터 pf를 선언하는 절차는 다음과 같습니다.
- int pf(int a); //함수명을 변수명으로 바꾼다.
- int *pf(int a); //변수명 앞에 *을 붙인다
- int (*pf)(int); //변수를 괄호로 싼다. 형식인수의 이름은 생략가능
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include<stdio.h> int function(int a) { return a * 2; } void main() { int i; int(*pf)(int a); pf = function; i = (*pf)(3); printf("%d\n", i); } | cs |
1 2 3 | int (*pf)(char *); void (*pf2)(double); pf1=pf2; //에러 | cs |
1 2 3 4 | int function(int a); void (*pf3)(char *,double); pf3 = function; //에러 | cs |
1 2 3 | int (*pf1)(char *); void (*pf2)(double); pf1=(int (*)(char *))pf2; | cs |
1 2 3 4 5 | int (*pf1)(char *) //변수명 삭제 int (*)(char *) //전체를 괄호로 감쌈 (int (*)(char *)) | cs |
1 | pf3=(void(*)(char *,double))function; | cs |
캐스트 연산자가 길어보이지만 그 원리는 알면 어렵지는 않습니다. T형에 대해 T형 배열과 T형 포인터를 항상 선언할 수 있으므로 함수 포인터에 대해서도 배열과 포인터를 선언할 수 있습니다. function타입의 함수를 가리킬 수 있는 함수 포인터를 요소로 가지는 크기 5의 arpf배열은 다음과 같이 선언합니다.
1 | int(*arpf[5])(int); | cs |
1 | int (**ppf)(int); | cs |
1 2 3 | typedef int (*PFTYPE)(int); PFTYPE pf; | cs |
함수 포인터를 선언하는 문장에서 변수명을 원하는 타입 이름으로 바꾸고 앞에 typedef만 붙이면 됩니다. 이후 컴파일러는 PFTYPE이라는 명칭을 int (*)(int)타입으로 인식하므로 PFTYPE으로 함수 포인터 변수를 쉰게 선언할 수 있으며 캐스트 연산자로 사용할 수 있습니다. 또한 함수 포인터로부터 배열이나 포인터 같은 파생 변수를 선언하는 것도 훨씬 더 간편합니다. 또한 함수 포이터로부터 배열이나 포인터 같은 파생 변수를 선언하는 거도 훨씬 더 간단합니다.
1 2 | PFTYPE arpf[5]; PFTYPE *ppf; | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include<stdio.h> #include<conio.h> int function2(int a) { return a * 2; } int function3(int a) { return a * 3; } void main() { char ch; int i = 5; int(*pf)(int a) = 0; printf("i값을 2배 하려면 2를 3배 하려면 3을 누르세요\n"); ch = _getch(); { if (ch == '2') { pf = function2; } if (ch == '3') { pf = function3; } } printf("결과 = %d\n", (*pf)(i)); } | cs |
1 2 3 4 5 6 7 8 | if(ch=='2') { printf("결과값 = %d\n",function(2)); } else { printf("결과값 = %d\n",function(2)); } | cs |
- 선택해야 할 함수가 두개 이상인 경우, 예를 들어 수십개의 함수 중 하나를 호출해야 한다면 함수포인터 배열을 선언하고 그 첨자를 선택하는 것이 더 쉽습니다.
- 함수를 선택하는 시점과 실제로 호출하는 시점이 완전히 분리 되어 있는 경우도 함수 포인터를 쓰는 것이 유리합니다. 호출할 함수에 대한 조건 점검은 필요할 때 한번만 하고 선택된 함수는 별다른 조건 점검없이 함수 포인터로 바로 호출할 수 있습니다.
- 호출할 함수가 DLL 같은 외부 모듈에 있고 이 함수를 동적으로 연결할 경우는 컴파일할 때 함수의 존재가 알려지지 않으므로 반드시 함수 포인터를 사용해야합니다. 함수 포인터를 사용하면 이름으로부터 원하는 함수의 번지를 찾아 호출할 수 있습니다.
1 | void qsort(void *base, size_t num, size_t width, int(*compare)(const void *, const void *)); | cs |
이 함수는 base 번지에서부터 width폭을 가지는 num 개의 값을 일정한 기준에 따라 정렬하는데 내부적으로 퀵 소트 알고리즘을 사용합니다. 퀵 소트 알고리즘은 가장 효율적인 정렬 알고리즘으로 알려져 있어서 일반적인 정렬 알고리즘으로 알려져있어서 일반적인 정렬에 자주 사용됩니다. 이 함수를 호출하기 위해서는 정렬 대상과 함께 비교 함수를 전달해야 하는데 네 번째 인수 compare가 비교 함수를 지정하는 함수 포인터입니다.
무작위로 흩어져있는 어떤 값을 일정한 기준에 따라 정렬하기 위해서는 순서대로 각 값의 대소를 비교하여 서로 교체하는 과정을 여러 번 거쳐야합니다. qsort 하수는 값을 비교하는 순서를 결정하고 비교 결과에 따라 값을 교체하는 알고리즘을 내부적으로 처리하되 단 값을 비교하는 연산은 직접 할 수 없습니다. 그래서 호출측에서 값을 비교하는 함수를 compare인수로 제공해야 합니다. 다음 소스는 배열에 저장된 정수값들을 오름차순으로 정렬하여 출력합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include<stdio.h> #include<stdlib.h> int compare(const void *a, const void *b) { if (*(int *)a == *(int *)b) { return 0; } if (*(int *)a > *(int *)b) { return 1; } return -1; } void main() { int i; int ar[] = { 12, 34, 123, 12, 54, 76, 128, 43, 56, 123, 15, 23 }; qsort(ar, sizeof(ar) / sizeof(ar[0]), sizeof(int), compare); for (i = 0; i < sizeof(ar) / sizeof(ar[0]); i++) { printf("%d번째 = %d\n", i, ar[i]); } } | cs |
실행 결과는 다음과 같습니다.
정렬 알고리즘대로 정렬하면 되는데 왜 사용자가 비교 함수를 제공해야하는가 하면 비교 방법이 값의 성질에 따라 천차만별로 달라질 수 있기 떄문입니다. 12봐 34가 크고 76보다 13이 큰거는 누가 봐도 분명합니다. 하지만 24와 078은 수치로 비교하면 078이 더 크지만 문자로 비교하면 24가 더 커서 애매합니다. 또한 문자의 경우 대소문자를 구분할 것인지, 문자 중간에 있는 밑주르 쉼표, 공백, 대시 같은 문자들도 비교 대상에 포함되는지, 한글과 영문의 우선순위는 어떻게 할 것인지 오름차순인지 내림차순인지 등 아주 복잡한 문제들이 많습니다. 여기에 이차 정렬까지 고려하면 완벽한 일반화는 불가능합니다.
그래서 qsort 함수는 값을 직접 비교할 수 없으며 호출측으로 두 값을 비교해 달라는 요청을 하기 위에 compare 함수를 부릅니다. 두 값을 비교하는 방식은 단순한 타입이 아니라 능동적인 동작이기 떄문에 함수가 필요합니다. 이떄 qsort 함수는 비교할 값들을 가리키는 두 개의 포인터를 전달하며 호출측은 두 포인터로부터 값의 대소 관계를 판별하는 함수를 만든 후 이 함수의 주소를 qsort에게 단달해야 합니다. 지금 다루고 있는 주제는 정렬이 아니므로 qsort 함수에 대해서는 나중에 포스팅 하겠습니다.
함수 포인터 리턴
함수 포인터를 인수로 전달할 수 있다면 함수 포인터를 리턴하는 함수도 만들 수 있을 것입니다. 이런 함수는 실용성이 지극히 떨어지지만 어쨋든 문법은 이런 함수를 만드는 것을 허용하는데 함수 포인가 단순한 변수일 뿐이므로 이런 변수를 리턴하는 함수도 당연히 만들 수 있어야 합니다. 함수 포인터를 리턴 하는 함수의 원형은 다음과 같습니다.
1 | fp의 리턴타입 (*함수명)(인수목록))(fp의 인수목록) | cs |
이 원형에서는 fp는 리턴되는 대상 함수를 의미합니다. 함수 자체의 인수와 리턴할 함수 포인터의 인수 목록을 같이 적어야 하기 때문에 원형이 복잡해집니다. 다음 소스는 함수포인터를 리턴하는 소스입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include<stdio.h> int f1(int a, double b) { return 1; } int f2(int a, double b) { return 2; } int(*SelectFunction(char ch))(int, double) { if (ch == 'a') { return f1; } else { return f2; } } void main() { int (*fp)(int, double); fp = SelectFunction('a'); printf("리턴값 = %d\n", fp(1, 2.3)); } | cs |
SelectFunction 한수의 원형을 말로 풀어본다면 "나는 char형을 인수로 취하며 정수형과 실수형을 인수로 취하고 정수형을 리턴하는 함수 포인터를 리턴한다." 라고 할 수 있습니다. 이 함수가 리턴하는 포인터의 타입은 int(*)(int, double)형이며 위 소스는 이런 함수가 2개 있습니다. SelectFunction 함수는 인수로 전달된 char형 인수가 'a'이면 f1을 리턴하고 그렇지 않으면 f2를 리턴합니다. 결국 SelectFunction 함수는 주어진 조건을 계싼하여 적절한 함수를 선택한다고 할 수 있습니다. 선언문이 생소하지만 typedef를 거치면 볼만 합니다.
1 2 | typedef int (*PF)(int, double); PF SelectFunction(char ch) | cs |
함수 포인터
가변 인수 함수
가변 인수란 말 뜻 그대로 인수의 개수와 타입이 미리 정해져있지 않다는 뜻이며 그런 인수를 사용하는 함수를 가변 인수 함수라고 합니다. 가변 인수 함수의 가장 좋은 예는 C언어의 가장 기초 함수인 printf입니다. 이 함수는 서식 문자열과 서식에 대응되는 임의 타입의 인수들을 개수에 상관없이 전달받을 수 있습니다. 다음은 printf 함수의 호출의 예입니다.
1 2 | printf("키는 %d이고 나이는 %d입니다.",height,age); printf(" %d, %f %f,",1,3.14,3,1415); | cs |
각 printf 함수로 전달되는 인수의 개수와 타입이 모두 다르지만 정상적으로 컴파일되고 실행됩니다. 반명 strcpy(str,dest,5)같은 호출은 즉시 에러 처리됩니다. 이런 함수들은 가변 인수를 받아들이지 않기 떄문에 헤더 파일에 적힌 원형대로 정확하게 인수의 개수와 타입을 맞춰서 호출해야합니다. 그렇다면 printf 함수는 어떤 원형을 가졌길래 가변인수를 처리할 수 있을까요? 다음은 printf함수의 원형입니다.
1 | int printf( const char *format,...); | cs |
이 함수의 첫 번쨰 인수는 format이라는 이름의 문자열 상수인데 흔히 서식 문자열이라고 부릅니다. 두 번째 이후의 인수에는 타입과 인수 이름이 명시되어 있지 않으며 대신 생략기호(ellipsis)인 ...이 적혀있습니다. 생략 기호는 컴파일러에게 이후의 인수에 대해서는 개수와 타입을 점검하지 않도록 하는데 이 기호에 의해서 가변 인수가 가능해집니다. 컴파일러는 ...이후의 인수에 대해서는 개수가 몇 개든지 어떤 타입이든지 상관하지 않고 있는 그대로 함수에게 넘기므로 임의 타입의 인수들을 개수에 상관없이 전달할 수 있습니다. 대신 전달된 인수의 정확한 타입을 판별하여 꺼내쓰는 것은 함수가 알아서 해야합니다. 컴파일러는 인수를 마음대로 취할 수 있도록 허락은 해 주지만 뒷일에 대해서는 책임지지는 않습니다. 생략 기호 이전에 전달되는 인수를 고정 인수라고 하는데 printf의 경우 format 인수가 바로 고정 인수입니다. 고정 인수는 원형에 타입과 개수가 분명히 명시되어 있으므로 원형대로 정확하게 전달해야합니다. printf가 아무리 가변 인수를 지원한다고 하더라고 printf(2) 나 printf(1231241241)같은 호출은 안됩니다. printf의 인수는 반드시 const char* 타입의 서식 문자열이어야 하며 두 번째 인수부터 가변 인수입니다. 그래서 정수 하나를 출력할 때는 printf(i)가 아니라 printf("%d",i);로 호출해야합니다.
가변인수 함수를 사용하는 것은 별로 어렵지 않습니다. printf 함수의 경우 고정 인수인 서식 문자열을 먼저 전달하고 서식의 개수와 타입에 맞는 인수들을 순서대로 전달하기만 하면 됩니다. 그렇다면 이런 가변 인수를 취할 수 있는 함수를 어떻게 만드는지 알아 보겠습니다. 관건은 자신에게 전달된 임의의 타입의 인수들을 순서대로 꺼내서 정확한 값을 읽는 겁니다. 가변 인수 함수의 개략적 구조는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 | void VarFunction(int Fix,....) { va_list ap; va_start(ap,Fix); while(모든 인수 다 읽을 때 까지) { va_arg(ap,인수타입); } va_end(ap); } | cs |
물론 함수의 이름이나 원형, 고정 인수의 개수등은 필요에 따라 마음대로 작성할 수 있습니다. 마지막 인수 자리에 ...만 있으면 가변 인수 함수가 됩니다. 가변 인수 함수 내부에서는 인수를 읽기 위해 이상한 매크로 함수들을 많이 사용하는데 각각을 분석해 보겠습니다.
::va_list ap
함수로 전달되는 인수들은 스택(Stack)이라는 기억 장소에 저장되며 함수는 스택에서 인수를 꺼내씁니다. 스택에 있는 인수를 읽을 때 포인터 연산을 해야하는데 현재 읽고 있는 번지를 기억하기 위해 va_list형의 포인터 변수 하나가 필요합니다. 변수 이름은 ap로 되어 있는데 아마도 Argument Pointer의 약자일 것입니다. ap는 어디까지나 지역변수일 뿐이므로 이름은 마음대로 정할 수 있되 관습적으로 가변 인수를 다루는 매크로에서는 ap라는 이름을 사용합니다. va_list 타입은 char *형으로 정의되어 있습니다. 가변 인수를 읽기 위한 포인터 변수를 선언했다고 생각하면 됩니다.
::va_start(ap,마지막고정인수)
이 명령은 가변 인수를 읽기위한 준비를 하는데 ap 포인터 변수가 첫 번째 가변 인수를 가리키도록 초기화합니다. 첫 번째 가변 인수의 번지를 조사하기 위해서 마지막 고정인수를 전달합니다. va_start 내부에서는 ap가 마지막 고정 인수 다음 번지를 가리키도록 해 주므로 이후부터 ap번지를 읽으면 순서대로 가변 인수를 읽을 수 있습니다.
::va_arg(ap,인수타입)
가변 인수를 실제로 읽는 명령입니다. va_start가 ap를 첫 번째 가변 인수 번지로 맞추어 주므로 ap위치에 있는 값을 읽기만 하면 됩니다. 단, ap 번지에 있는 값이 어떤 타입인지를 지정해야 이 매크로가 값을 제대로 읽을 수 있으므로 두 번쨰 인수로 읽고자 하는 값의 타입을 지정합니다. 예를 들어 ap 위치에 있는 정수 값을 읽고자 한다면 va_arg(ap,int)를 호출하고 실수값을 읽고자 한다면 va_arg(ap,double)이라고 호출하면 됩니다. 물론 리턴되는 값은 인수타입에 맞는 변수로 대입받아야합니다. 이 명령은 ap위치에서 타입에 맞는 값을 읽어 리턴하며 또한 ap를 가변 인수 위치로 옮겨줍니다. 그래서 va_arg를 반복적으로 호출하면 전달된 가변 인수를 순서대로 읽을 수 있습니다.
하지만 이 명령에서 조금 이상한 점이 발견되는데 int나 double 같은 타입 이름이 어떻게 함수의 인수로 전달될 수 있냐는 겁니다. 함수의 인수로는 값이 전달되는 것이 정상인데 타입명이 어떻게 함수의 인수가 될 수 있는가 말입니다. 타입명은 함수의 인수가 될 수 없습니다. 그럼에도 불구하고 va_arg가 타입명을 인수로 받아들일 수 있는 이유는 va_arg가 진짜 함수가 아니라 매크로 함수이기 떄문입니다. va_arg의 두 번째 인수는 내부적으로 sizeof 연산자와 캐스트 연산자로 전달되기 떄문에 타입명이 될 수 있습니다.
::va_end(ap)
이 명영은 가변 인수를 다 읽은 후 뒷정리를 하는데 별다른 동작은 하지 않으며 실제로 없어도 전혀 지장이 없습니다. 이 명령이 필요한 이유는 호환성 떄문인데 플랫폼에 따라서는 가변 인수를 읽은 후에 뒷처리를 해야하는 경우도 있기 떄문입니다. 적어도 인텔 계열의 CPU에서는 va_end가 아무일도 하지는 않습니다. 다른 플랫폼에서는 va_end가 중요한 역할을 할 수 있기 떄문에 관례적으로 넣어주는 것이 좋습니다.
다음예제는 GetSum함수에 첫 번째 인수로 전달된 num의 개수만큼 정수 인수들의 합계를 구해 리턴합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include<stdio.h> int GetSum(int num, ...) { int sum = 0; int i; va_list ap; int arg; _crt_va_start(ap, num); for (i = 0; i < num; i++) { arg = _crt_va_arg(ap, int); sum += arg; } _crt_va_end(ap); return sum; } void main() { printf("1+2=%d\n", GetSum(2, 1, 2)); printf("3+4+5+6=%d\n", GetSum(4, 3, 4, 5, 6)); printf("10~15=%d\n", GetSum(6, 10, 11, 12, 13, 14, 15)); } | cs |
GetSum 함수의 첫 번째 인수 num은 전달될 정수 인수의 개수를 가지는 고정 인수이며 이 인수 다음에 합계를 구하고 싶은 num개의 정수값을 나열하면 됩니다. 인수의 개수가 몇 개이든 간에 전달된 모든 값의 합계를 구해 리턴할 것입니다. 실행 결과는 다음과 같습니다.
GetSum함수에서 가변 인수들을 어떻게 읽는 지 분석해 보겠습니다. va_list형의 포인터 ap를 선언하고 va_start(ap,num) 호출로 ap가 마지막 고정 인수 num 다음의 위치, 즉 첫 번째 가변 인수를 가리키도록 초기화했습니다. 그리고 num만큼 루프를 돌면서 _crt_va_arg(ap,int)호출로 ap 위치에 있는 int값을 계속 읽어 sum에 누적시킵니다. 모든 가변 인수를 다 읽었으면 _crt_va_end(ap)로 뒷정리를 하고 계산된 sum값을 리턴하였습니다. 앞에서 보인 기본형식대로 _crt_va_ 매크로를 사용하여 가변 인수를 읽어 처리하기만 하면 되므로 사용만을 목적으로 한다면 그리 어렵지 않습니다.
'Programming > C' 카테고리의 다른 글
C언어 - 포인터 고급(4/4) (0) | 2015.09.07 |
---|---|
C언어 - 포인터 고급(3/4) (0) | 2015.09.06 |
C언어 - 포인터 고급(1/4) (0) | 2015.08.31 |
C언어 - 구조체(Structure)(2/2) (23) | 2015.08.21 |
C언어 - 구조체(Structure)(1/2) (1) | 2015.08.20 |