1. 함수 포인터란?
- 프로그램에서함수의 이름은 메모리에 로드된 그 함수의 실행코드 영역의 시작주소를
의미한다.
- 함수에 대한 포인터는 바로 함수의 시작주소값을 갖는 포인터이다.
함수 포인터라는 것 역시 32비트 체제하에서 4바이트의 메모리를 갖는 포인터 변수 입니다. 일반 포인터 변수와 다른 점은 일반 포인터 변수가 변수들의 주소값을 저장하는 반면에 함수 포인터는 함수의 주소값을 갖는다는 것입니다. 함수는 code 부분입니다. 즉 프로그래머가 짠 코드가 컴파일 되어서 기계 코드로 변화된 것, 그것이 바로 code 입니다. 프로그램이 실행되기 위해서는 이 code가 메모리에 올라가 있어야 하는 것입니다. 여기서 어떤 함수의 호출은 이 code중에서 그 함수 부분으로 jump(이동) 하는 것이지요. 바로 이 함수 부분이라는 것이 그 함수의 주소값이 되는 것이고 이 함수의 주소값을 저장하는 포인터가 함수 포인터입니다.
C 언어의 경우 함수자체를 변수로 만들수는 없습니다. 대신 함수를 포인터하는 것은 가능한데, 이것을 통해서 함수를 포인터 처럼 사용할수 있으며, 이 포인터가 가리키고 있는 곳의 함수를 실행시킬수도 있습니다.
예) main 함수와 printf 함수의 시작 주소값을 출력합니다.
#include <stdio.h>
main()
{
printf("address of main : %u \n", &main);
printf("address of printf : %u \n", &printf);
}
2. 함수 포인터의 용도
- 함수에 접근하기 위해 사용된다.
- 함수에 함수 자체를 실인수로 전달하기 위해 사용된다.
- 함수의 처리결과가 함수일 때 그 함수에 대한 포인터를 돌려주기 위해 사용된다.
함수 포인터에 대한 연산은 허용되지 않습니다.
3. 함수 포인터의 선언
- 다른 포인터 변수와 마찬가지로 함수 포인터도 먼저 선언하고 사용해야 한다.
- 함수 포인터의 선언은 일반적으로 다음의 형식을 사용한다.
자료형 (*함수포인터명)(인자목록);
이 형식은 명시된 자료형을 돌려주고, 인자목록에 포함된 인자를 받는 함수에 대한
포인터를 선언한다.
- 함수 포인터 선언의 구체적인 예
ⓐ int (*f1)(int a);
ⓑ char (*f2)(char *p[]);
ⓒ void (*f3)();
ⓐ 하나의 int형 인자를 받아들이고, int형 자료를 돌려주는 함수에 대한 포인터 f1을
선언한다.
ⓑ char형에 대한 포인터 배열을 인자로 받아, char형의 값을 돌려주는 함수에 대한
포인터 f2를 선언한다.
ⓒ아무런 인자도 받지 않고 결과값도 돌려주지 않는(void) 함수에 대한 포인터 f3를
선언한다.
- 함수 포인터 선언과 포인터를 돌려주는 함수 선언의 차이
ⓐ int (*f1)(int a);
ⓑ int *f2(int a);
ⓐ 함수 포인터 : 한 개의 int형 인자를 받아, int형 값을 결과로 돌려주는 함수에 대한
포인터 f1을 선언한다.
ⓑ 포인터를 돌려주는 함수의 선언 : 한 개의 int형 인자를 받아, int형 포인터 값을
결과로 돌려주는 함수를 선언한다.
함수에 대한 포인터의 선언은 반드시 포인터의 이름과 간접연산자(*) 주위에 ( )를 사용해야 합니다.
4. 함수 포인터의 초기화
- 함수 포인터를 선언하고 나면, 이 포인터가 어떤 함수를 지시하도록 초기화해야 한다.
- 함수 포인터를 초기화할 때, 인자목록과 return 자료형이 일치해야 한다.
- 함수 이름은 이름 자체로 주소를 의미한다. 따라서 함수포인터에 함수의 주소값을
초기화하려면 다음과 같이 한다.
예)
int add(int a, int b); => 함수의 prototype
int (*f1)(int x, int y); => 함수 포인터 선언
int add(int a, int b) { return a + b; } => 실제 함수 정의 부분
f1 = add; => 적합
f1 = &add; => 적합
f1 = add(); => 오류(f1은 포인터, add()의 결과는 int)
f1 = &add(); => 오류(&부적당)
5. 함수 포인터의 활용
- generic한 함수(혹은 알고리즘)의 작성을 가능하게 한다
- 잘 사용하면 유지/보수를 수월하게 한다.
- 함수의 이름 자체는 배열의 이름처럼 한번 정해지면 바꿀 수 없는 포인터 상수이다.
그러나 함수 포인터는 변경이 가능하며 필요할 때마다 다른 함수를 지시하도록 설정할
수 있다.
예)
#include <stdio.h>
int add(int a, int b); /* 함수의 prototype */
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
main()
{
int (*fun)(int x, int y); /* 함수 포인터 선언 */
int a, b;
char c;
printf("Input (num op num) : ");
scanf("%d %c %d", &a, &c, &b);
switch (c)
{
case '+' :
fun = add;
break;
case '-' :
fun = sub;
break;
case '*' :
fun = mul;
break;
case '/' :
fun = div;
break;
}
printf("%d %c %d = %d\n", a, c, b, fun(a,b));
}
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a/b;
}
입력에 따라 함수포인터 fun에 지정되는 함수가 결정된다.
- 함수 포인터의 배열: 여러개의 함수포인터를 배열에 저장하여 사용할 수 있다.
int (*fun[3])(int, int);
int 형 자료 두 개를 입력으로 받아, int형 결과를 돌려주는 함수포인터 3개를 저장할 수
있는 배열이다.
예) 두 정수 a,b를 읽어 합,차,곱을 구하는 예제로 함수포인터의 배열을 사용한다.
#include <stdio.h>
int add(int a, int b); /* 함수의 prototype */
int sub(int a, int b);
int mul(int a, int b);
main()
{
char op[] = {'+', '-', '*'};
int (*fun[])(int x, int y) = {add, sub, mul};
int a, b;
printf("Input number(2) : ");
scanf("%d %d", &a, &b);
for (i = 0; i < 3; i++)
printf("%d %c %d = %d\n", a, op[i], b, fun[i](a,b));
}
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}
int mul(int a, int b)
{
return a*b;
}
- 함수 포인터를 인자로 전달하기: 함수명을 인자로 전달하거나 함수 포인터 자체를
함수의 인자로 보내고 받을 수 있다. 함수명을 실인수로 사용할 경우, 호출당한
함수의 가인수는 함수 포인터가 된다.
예)
#include <stdio.h>
int print_add(int a, int b)
{
printf("%d + %d = %d\n", (a, b, a+b);
}
int add(void (*fp)(int, int), int x, int y)
{
fp(x, y);
}
main()
{
add(print_add, 10, 5);
}
add(think); => think(); 함수의 시작 번지 값이 add() 함수의 매개변수이다.
add(think()); => think(); 함수의 리턴 값이 add() 함수의 매개변수이다.
- 함수 포인터를 이용해 특정 번지로 점프하기
예1)
#include <stdio.h>
void main(void)
{
unsigned int goaddr = 0x8120; /* 8120H 번지임을 나타낸다. */
void (*gofunc)(void); /* 함수 포인터 선언 */
gofunc = (void(*)()) goaddr; /* 초기화 */
(*gofunc)(); /* 함수포인터 실행 */
}
위 예에서는 void (*gofunc)(void); 로 선언된 함수 포인터가 실제적으로 가리켜야 할
목적지 함수가 따로 없는 것처럼 보인다. 그러나 잘 보면 목적 함수는 다음과 같음을
알 수 있다.
void (*goaddr)(); : 목적함수
그리고 위 목적함수는 하나의 형(type)으로써 뒤의 goaddr을 cast한다.
이제 (*gofunc)(); 로 실행되면 컴퓨터의 PC(Program Counter) 또는
IP(Instruction Pointer)는 (*gofunc)(); 함수로부터 void (*goaddr)(); 함수로 넘어간다.
그리고 void (*goaddr)(); 함수의 시작번지는 0x8120 번지가 되는 것이다. 따라서
컴퓨터의 PC는 0x8120번지로 점프하게 되는 결과를 낳는다.
예2)
#include <stdio.h>
void main(void)
{
void (*gofunc)(void); /* 함수 포인터 선언 */
gofunc = (void (*)()) 0x8120; /* 시작주소 초기화 */
(*gofunc)(); /* 함수포인터 실행 */
}
위 프로그램은 번거롭게 goaddr이라는 변수를 생략하고 직접 (*gofunc)();의 시작주소를
지정하였다. 이로서 확실히 알 수 있는 것은 포인터 함수의 시작주소의 캐스팅 형이
(void (*)())이 된다는 것이다.
예3)단 한줄로 나타내보자.
#include <stdio.h>
void main(void)
{
(*((void (*)()) 0x8120))();
}
이 한줄은 바로 0x8120번지로 점프하라는 명령어와 같다!
그러나 TurboC++ 3.0 에서는 위 명령이 유효하나 IC96에서는 무효하다.
(다음과 같은 에러 메시지가 뜬다.)
iC-96 FATAL ERROR --
internal error: invalid directionary access, case 3
COMPILATION TERMINATED
그러나 방법은 있다. 아래와 같이 하면 IC96에서 에러 없이 멋지게 만들어낼 수 있다.
#include <80c196.h> /* 표준 인클루드 파일 */
void main()
{
(*(void (*)(void))(*(void (**)(void))0x8120))();
}
실제로 위와 같은 선언은 특히 Embedded System Programming에서 많이 사용되는
방법이다. 특히 80C196에서 이중 인터럽트 벡터 지정 시에 유용하게 사용될 수 있다.
출처 : 네이버 카페 임베디드시스템(Device Control)