본문 바로가기
IT/Programming

C 포인터, 확실히 알자(4) - 함수와 포인터

by Jany 2009. 3. 31.
반응형
1. 포인터, 다시 들여다 보기

지난 강좌에서, 저는 포인터를 메모리상의 주소(Address)를 다루는 변수라고 정의했습니다. 이말은 즉, 메모리에 올라갈 수 있는 것이라면 그 어떤 것이든 포인터를 이용해서 접근할 수 있다는 얘기가 됩니다. (이론상으로...)

우리는 주로 메모리에는 데이터가 있다고 생각합니다. 자료를 담는 변수, 배열, 구조체 등의 자료구조들 역시 메모리라는 공간 안에서 관리되는 것이죠. 그래서 지금까지는 변수와 배열을 다루는 포인터에 대해서 이야기해왔습니다.

그런데, 우리가 작성한 프로그램 코드는 대체 어디에 존재하는 걸까요? 네, 당연히 메모리입니다. 그 어떤 프로그램 코드도 일단 메모리에 올라간 상태라야 비로소 CPU가 실행할 수 있는 것이죠.

그렇다고 하면, 포인터로 프로그램의 코드를 참조하는 것도 가능하지 않을까요?

하지만 그러기 위해서는 일단 다음의 두 가지 조건이 갖춰져야합니다.

 (1) 코드가 있는 메모리 주소를 알아낼 것
 (2) 포인터가 참조하고 있는 대상을 데이터가 아닌 코드로 다룰 수 있게 하는 형(Type)

여기서 두번째에 해당하는 것을 C에서 지원하는게 딱 하나 있습니다.
바로 함수(Function) 입니다.


2. 함수를 가리키는 포인터

결론부터 말하면, C에서는 포인터가 함수도 가리킬 수 있습니다. 그리고 포인터를 이용해 함수를 간접적으로 실행시키는 것도 가능합니다.

자 그러면, 함수를 가리킬(참조할) 수 있는 함수 포인터를 선언해 봐야 겠지요.

#include <stdio.h>

// 함수의 프로토타입 선언
void func();

int main()
{
 void (*p_func)(); // void func() 형식의 함수를 가리키는 포인터 선언

 p_func = &func;
 (*p_func)();

 return 0;
}

void func()
{
 puts("Hello World !!");
}

위 예제에서 포인터 선언부를 주목해 봅시다.

 void (*p_func)();

이것은 return 값이 없고(void), 넘겨주는 인수도 없는() 함수를 선언하는 함수를 의미하는데, 사실상 함수의 프로토타입을 선언하는 것과 거의 유사한 형식입니다.

다만 차이가 있다면, 함수명 대신에 변수로 쓰일 이름이 들어갔다는 점과 그 변수명 앞에 * 기호가 붙음으로써 이것이 포인터 형이라는 것을 지시하고 있습니다. 바로 이것이 함수 포인터의 선언입니다.

그런데 이것은 포인터이기 때문에, 여기에다가 뭔가의 주소를 전달해 줘야 비로소 기능을 수행할 수 있습니다. 물론 함수 포인터이니까 함수의 주소를 전달해야 합니다.

 p_func = &func;

이렇게 func라는 함수의 주소를 전달했습니다. 여기서 함수명인 func가 마치 변수처럼 사용되었습니다.

그리고 포인터가 가리키고 있는 함수를 실행하기 위해서 * 연산자를 써서 다음과 같이 했습니다.

 (*p_func)();

이렇게 하면 직접 func() 함수를 호출하지 않고, 포인터 변수를 이용해서 간접적으로 함수를 실행할 수 있습니다.


3. 함수 이름과 포인터

자, 그런데 다음과 같은 예제는 과연 어떨까요?

#include <stdio.h>

void func();

// 빨갛게 강조된 부분에 주목하세요.
int main()
{
 void (*p_func)();

 p_func = func;
 p_func();


 return 0;
}

void func()
{
 puts("Hello World !!");
}

뭐가 달라졌는지 한번 볼까요.

 p_func = func;

func()의 주소를 포인터에게 전달할 때, 주소 연산자인 & 을 쓰지않고 그대로 전달했습니다. 이렇게 해도 아무 문제가 없이 실행되었다는 것은, func라는 함수 이름 자체가 그 함수의 주소를 포함하고 있다라고 해석할 수 있겠습니다.

// 정말인지 한번 실험해 봅시다 !!
#include <stdio.h>

void func();

int main()
{
 void (*p_func)();

 p_func = &func;
 printf("%d %d", func, p_func);

 return 0;
}

void func()
{
 puts("Hello World !!");
}
함수 이름이 포인터 변수인 것 처럼 값이 전달됩니다. 그리고 함수 이름이 가진 주소값과 포인터 변수가 가진 주소값이 서로 동일함을 확인할 수 있습니다.

 p_func();

이번엔 포인터가 가리키는 함수를 실행하는데 * 연산자를 쓰지 않고 바로 ()를 붙여서 호출했습니다. 그래도 이 예제는 문제없이 의도했던 결과를 보여줍니다.

이것은 함수 포인터 변수가 마치 함수의 이름을 사용하는 것처럼 똑같은 방식으로 함수를 호출할 수 있음을 보여줍니다. 그러면 여기서 한가지 결론을 내릴 수 있습니다.

"함수의 이름은 곧 함수의 포인터이다."

즉, 앞의 예제에서 p_func 와 func 라는 것은 서로 동일한 형(Type)입니다.

단순히 생각해보면, C에서 함수를 호출하는 문장은 결국 내부적으로 함수 포인터를 이용해서 호출하는 것이라고 봐도 무방하겠습니다.


4. 예제

잠시 정리하는 의미에서, 반환값과 인수가 있는 함수를 포인터를 이용해서 호출하는 예제를 보이겠습니다.

#include <stdio.h>

int sum(int, int);

int main()
{
 typedef int (*SUM_FUNC)(int, int);

 SUM_FUNC p_func;

 p_func = sum;
 printf("Sum: %d", p_func(10, 20));

 return 0;
}

int sum(int a, int b)
{
 return a + b;
}


함수의 형(type)은 반환 형(return type)과 인수(argument)의 개수와 형으로 구성된, 그 함수의 프로토타입 형식과 같습니다. C에서는 그 함수의 프로토타입을 보고 그 함수를 호출하는 데 필요한 정보를 얻습니다.

그런데 함수 포인터를 선언할 때마다 매번 프로토타입 형식을 적어주게 되면 타이핑의 양이 많아지게 되겠죠. 그래서 C에서는 typedef 키워드를 써서 좀더 편하게 쓸 수 있는 방법을 제공합니다.

 typedef int (*SUM_FUNC)(int, int);

이 문장은 int (int, int) 형식의 함수를 가리키는 포인터를 SUM_FUNC 라는 형(type)으로 대신 선언할 수 있게 해줍니다. 즉, 이 SUM_FUNC가 마치 int나 char 처럼 하나의 타입으로 취급되는 거죠. (그래서 키워드가 type define의 약자인 거죠.) 같은 형식의 함수 포인터를 여러개 선언하고자 할 때는 이처럼 typedef를 이용하면 편리합니다.


5. 함수의 동적 바인딩

함수 포인터를 이용하면, C에서도 일종의 동적 바인딩을 구현할 수 있습니다. 즉 실행중에(Run-time) 필요에 따라서 어떤 함수를 호출할지 마음대로 결정할 수 있는 것이죠.

이번엔 조금 재미있는 예제를 준비했습니다.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void funcA(void); // void funcA() 와 같은 의미
void funcB(void);
void funcC(void);

#define randomize() srand((unsigned)time(NULL)) // 난수 초기화 매크로
#define random(n) (rand() % (n)) // 난수 발생 매크로 0에서 n-1까지

int main(void)
{
 typedef void (*FUNC)(void);

 FUNC funcs[3];
 int i;

 funcs[0] = funcA;
 funcs[1] = funcB;
 funcs[2] = funcC;

 randomize();

 for (i=0; i < 10; i++)
  (funcs[random(3)])();

 return 0;
}

void funcA(void)
{
 puts("A");
}

void funcB(void)
{
 puts("B");
}

void funcC(void)
{
 puts("C");
}
실행 결과는 매번 달라집니다.

함수 포인터 배열을 만들어서 여기다가 함수들을 등록하고, 난수를 발생시켜서 렌덤하게 함수를 실행시키는 예제입니다.

특별히 더 설명할 것은 없겠죠?


6. Call Back Function

콜 백 함수라는 것은 일종의 이벤트와 비슷한 거라고 할 수 있습니다.
(C#의 delegate 역시 이것과 많이 닮아있습니다.)

함수 포인터도 결국 변수이기 때문에 당연히 함수의 인수로도 넘겨 줄 수 있습니다. 함수에게 함수를 인수로 넘겨준다? 기묘하게 보일 수도 있겠지만 C에서는 충분히 가능한 일입니다.

#include <stdio.h>

typedef void (*event)(int);

void doWhile1toN(int, event);
void printNum(int);

int main()
{
 doWhile1toN(30, printNum);
 printf("");

 return 0;
}

void doWhile1toN(int n, event onOdd)
{
 int i;

 for(i=1; i <= n; i++)
  if (i % 2) onOdd(i);
}

void printNum(int num)
{
 printf("%d ", num);
}

이처럼 함수 포인터도 쓰기에 쓰기에 따라서 다양하게 응용될 수가 있습니다.
반응형

댓글