본문 바로가기

무언가 만들기 위한 지식/C,C++,Embedded C

[C,C++,Embedded C] Function Pointer(함수 포인터)

함수 포인터란 말그대로 함수를 가리키는 포인터이다.
그동안 필자가 어설픈 프로그래밍을 하면서 사실상 함수포인터를 쓸일은 거의 없었다.
(System Programming이나 Network Programming 에서 아주 살짝 나온정도?)
하지만 최근, Embedded C를 배우면서 함수의 포인터를 이용해 함수를 인자로 두거나 리턴 타입을 함수포인터로 하는 실례를 많이 접하게 되었다.

처음 코드를 접하게 되면, 이게 뭐지...하고 당황할 수 밖에 없는데, 그런것들은 차차 학습하여야 한다.
실제로 어플리케이션만 개발할 사람들은 이 함수포인터에 대해 몰라도 되지만, 적어도 디바이스나 OS Kernel에서 작업하는 사람들은 반드시 알아두어야 한다고 한다.

배열에서 배열요소값이 함수일 수는 없다.
또한 함수에서 인자 값이 함수일 수도 없고, 리턴값이 함수일 수도 없다.(물론 리턴값이 배열일 수도 없다.)
이경우에 사용되는 것이 바로 함수의 포인터 이다.
포인터라 함은 포인터로 해당 배열명이나 함수명을 자기 값처럼 제어한다는 의미이다.
이 포인터는 함수에서 인자로 함수를 전달 받았을때 사용된다.

[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0   사용언어 : C]


1~7 Line은 그냥 2개의 함수를 정의한 부분이다.
재미있는 점은 9~10 Line에서 발견된다. 이부분에서는 함수명을 이용하여 출력을 해보았다.
출력결과 그 함수가 위치해 있는 주소값이 나오는 사실을 알 수 있다. 또한 함수명과 함수의 주소를 "&"을 이용했을 때 모두 같은 값을 나타낸다는 사실을 결과값으로부터 확인이 가능하다.
이는 마치 배열에서 처럼 배열명이 첫요소의 첫번째 값을 나타내는 것과 마찬가지로 쓰이고 있는 것이다.
또한 13, 14 Line에서 함수 포인터의 선언하는 부분을 유심히 볼 필요가 있다.
void (*p1)();   
처음보면 당연히 생소할수 밖에 없지만, 이 선언이 함수포인터 선언 이다.
우선순위에 의해 포인터(*)를 먼저 해석하고 다음에 나오는 (())를 함수로 해석하면 된다.
그러면
 "변수 p1은 포인터인데 이 포인터가 가리키는 곳에는 void를 리턴하고 인자가 없는 함수가 존재한다. "
라고 해석하면 된다.
이는 물론 void *p1() 와는 아주 의미가 다르다.

15와 17, 16과 18은 하나의 쌍인데, 이 두가지 표현이 모두 가능하다는 것을 표현하기 위해 나타낸 것이다.
배열의 경우 15 Line은 양쪽이 가리키는 Type이 다르기 때문에 에러가 발생하지만, 함수의 경우 배열명과 첫번째 주소를 같게 생각하기 떄문에 이상이 없다.

함수 포인터를 다룰때 또 고려해야할 부분은 함수의 주소값, 즉 함수명에서 연산은 불가능하다는 것이다.
배열명에서는 + 및 - 연산을 통해 파일 크기만큼 증감이 더해졌지만, 함수명에서는 아에 연산이 안된다.
그 이유는 &func+1에 대해 생각해보면 된다.
&func+1은 func 함수의 총 크기의 다음 위치를 가리키게 된다. 하지만 그 다음위치는 func함수내의 stack 및 기타 이용에 따라서 변한다. 즉 함수가 끝나야 크기가 나타나기 때문에 그 이전에 다음 위치를 잡아낼 수 없는것이다.
(요건 필자의 예측이다. 아무튼 함수명에는 주소연산을 이용할 수 없다.!!!!!!)

17~18 Line은 사용법에 있어서의 차이를 보여주기 위함이다.
(*p2)(7); 
어떻게 보면 위 방식으로 사용하는 것이 의미상 직관적이고 이해하기 편하지만, 
아주 간편하게  p2(7)로도 사용이 가능하다.


위에서 기본적인 함수 포인터의 사용법을 익혔으니,
이제 본격적으로 함수 포인터가 사용되는 예를 보자.
실제 필자가 본 OS Kernel 부분이나, Device Driver의 코드들에서는 Switch 문 대신에 바로 배열에 함수를 저장하여 인덱스가 나오면 바로 함수 Table에 인덱스를 이용해 한번에 접근 하는 방식을 취한다.
마치 자료구조에 나오는 Dictionary나 Map구조처럼 말이다.
Switch문의 경우 if문의 연속이라 속도면에서 가장 뒤쪽에 있는 코드는 가장 늦게 실행되게 된다.
그래서 특히 디바이스 드라이버의 경우 각 회사마다 동등한 속도를 보장해주기 위해서 이런 구조가 사용된다.
아래 코드는 함수(포인터)를 인자로 받아 함수(포인터)를 반환하는 구조이다. 그리하여 일정 테이블로부터 인덱스를 통해 함수를 읽어온다.

[작성환경 : Window XP  작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0   사용언어 : C]


4~12 Line은 보통의 함수를 3개를 선언한 부분이다.
13, 15 Line에서는 함수를 인자로 받아서, 함수내에서 그 함수를 사용하는 부분이다.
func함수를 호출하고 인자로 주면, func내부에서 변수 p를 보통의 함수처럼 사용이 가능해 진다. 이부분이 함수의 포인터이다. 포인터를 함수처럼 사용 가능하다.
17 Line에서는 pa를 변수를 선언하였다.
pa라는 3칸짜리배열에는 포인터가 있는데, 그 포인터가 가리키는 값은 int를 반환하는 함수이다.
이 배열을 통해 함수를 가져와서 후에 사용하게 된다.

여기서 18~20 Line에는 배열에서 값을 꺼내오는 함수가 존재한다.
이 compute라는 함수는 인덱스를 입력받은면 해당하는 위치의 함수를 리턴하는 함수이다.
26~30 Line은 테스팅을 위한 출력들이 존재한다.

결과 값은 모두 같은 값인 7을 출력하게 된다.
27 Line의 경우 pa[0](3,4)로 나타났지만, 
구체적으로 사용한다면 (*pa[0])(3,4) 로 표현이 가능하다. 가로의 위치에 대해서 주의해야 하는데, 이 경우 pa[0]에는 함수의 주소가 들어 있고 그 값을 가져오기 때문에 *pa에 가로를 써주어야 한다. ( *을 사용할 경우)
귀찮게 compute라는 함수를 만든 이유는 인자에 구애받지 않고 배열에서 함수를 불러오기 위해서이다.
인자는 compute밖의 함수에서 ( )를 이용하여 넣어주면 된다.

21 Line은 typedef를 이용하여 함수선언을 단순화해 준 것이다.
변수를 함수로 선언할 경우 리턴값과 인자의 위치 때문에 많은 사람들에게 혼란을 주게 된다.
이를 간소화 하기 위해 보통 typedef를 통해서 미리 함수 type을 선언해 놓는다.
위 소스에서는 3 Line에서 typedef를 통해 pf를 함수로 선언하였다. 그리고 21 Line에서는 간단히 pf를 넣어줌으로써 리턴값이 함수인 것을 명시하였다. (typedef 사용법은 후에 포스팅으로)

※ 참고사항 : 함수의 경우 *가 포인터 선언시만 쓰이고 사용할떄는 의미가 없다고 보면 된다.
실제로 함수 포인터의 사용에 있어서 *p, **p, ************p 는 모두 같다. 컴파일 에러도 안나고 잘못된 값을 출력하지도 않는다.