관리 메뉴

Kim's Programming

C언어 - 포인터 고급(2/4) 본문

Programming/C

C언어 - 포인터 고급(2/4)

Programmer. 2015. 9. 5. 21:53

함수 포인터


함수 포인터의 정의


함수포인터(Pointer To Function)란 함수를 가리키는 포인터를 말합니다. 포인터란 본래 메모리상의 번지를 저장하는 변수인데 함수도 메모리에 존재하며 시작 번지가 있으므로 포인터 변수로 가리킬 수 있습니다. 일반적인 포인터는 변수가 저장되어 있는 번지를 가리키지만 함수 포인터는 함수의 시작 번지를 가리킨다는 점에서 다릅니다. 함수 포인터와 구분하기 위해서는 변수를 가리키는 일반적인 포인터를 특별히 데이터 포인트라고 부르기도 합니다. 정수형을 가리키는 int *pi는 정수형 변수의 번지를 가지며 실수형을 가지는 double *pd는 실수형 변수의 번지를 가집니다. 데이터 포인터는 단순히 가리키는 대상체의 타입만 밝히면 되므로 선언 형식이 간단합니다. 반면 함수 포인터는 대상체인 함수의 형식이 좀 더 복잡하기 떄문에 선언형식도 다소 복잡하게 됩니다. 대상체가 되는 함수의 리턴 타입과 인수들의 목록까지도 같이 밝혀야 합니다. 함수 포인터를 선언하는 형식은 함수의 원형 선언 형식과 유사합니다. 다음은 함수 포인터의 기본형식입니다.


리턴타입(*변수명)(인수의 목록):


함수의 원형을 써 놓고 함수명을 변수명으로 바꾸고 앞에 *을 붙인후 *변수명을 괄호로 싸면 됩니다. 함수 포인터도 변수이므로 고유한 이름을 가져야 하며 명칭 규칙에 맞게 적당히 이름을 붙여야 합니다. 리턴값과 인수 목록은 그대로 유지하되 단 형식 인수의 이름은 생략해도 상관이 없습니다. 함수 원형 선언에서 형식 인수의 이름이 별 의미가 없듯이 함수 포인터를 선언할 때도 인수의 타입만 의미가 있습니다. 다음 함수를 가리키는 함수포인터를 선언한다고 하겠습니다.


int function(int a)


정수형 인수를 하나 취하며 정수형을 리턴하는 function이라는 함수의 원형입니다. 이런 함수를 가리킬 수 있는 함수 포인터 pf를 선언하는 절차는 다음과 같습니다.

  1. int pf(int a);        //함수명을 변수명으로 바꾼다.
  2. int *pf(int a);      //변수명 앞에 *을 붙인다
  3. int (*pf)(int);      //변수를 괄호로 싼다. 형식인수의 이름은 생략가능
func가 (*pf)로 바뀌었는데 이름 바꾸고 *를 붙인 후 괄호를 싸기만 하면 됩니다. 이 선언문에서 괄호를 빼 버리면 정수형 포인터를 리턴하는 함수가 되기 떄문에 괄호를 꼭 붙여야 합니다. 이렇게 선언한 함수 포인터는 자신과 원형이 같은 함수의 시작 번지를 가리킬 수 있는데 단순히 함수의 이름을 대입하면 됩니다. 다음 대입식은 int function(int a)함수를 가리킬 수 있는 함수 포인터 pf에 function의 번지를 대입합니다.

pf=dunc;

이런 대입이 가능한 이유는 괄호없이 단독으로 사용된 함수명은 함수의 시작 번지를 나타내는 포인터 상수이기 떄문입니다. 마치 배열명이 첨자없이 사용되면 배열의 시작 번지를 나타내는 포인터 상수가 되는 것 처럼인 겁니다.function이라는 표현식 자체가 함수 포인터 상수이므로 함수 포인터 변수 pf가 그 번지를 대입받을 수 있습니다. 함수 이름 자체가 포인터 타입이므로 pf=&function와 같이 &연산자를 사용할 필요가 없습니다. pf 자체는 변수이므로 원형만 일치하면 다른 함수를 가리킬 수도 있습니다. 함수 포인터에 함수의 시작 번지를 저장했으면 이제 함수 대신 포인터로 함수를 호출 할 수 있습니다. 변수의 번지를 가리키는 데이터 포인터로 변수값을 읽을 수 있듯이 함수 포인터로는 이 포인터가 가리키는 번지의 함수를 호출할 수 있는 것입니다. 함수 포인터로 함수를 호출하는 형식은 다음과 같이 2가지이고 function함수를 가리키는 pf를 이용 function함수를 호출하는 방법입니다. 호출 방법은 다음과 같습니다.

(*pf)(2);
pf(2);

함수 포인터 다음에 인수의 목록을 나열하는데 func 함수는 정수형 인수 하나를 취하므로 상수 2를 넘겨주었습니다. 물론 정수와 호환되는 변수를 인수로 넘기는 것도 가능합니다. pf가 function의 번지를 가지고 있으니 *pf는 곧 function과 동일하며 그래서 (*pf)(2)는 function(2)와 같습니다. 이떄 *pf를 감싸는 괄호를 생략할 수 없는데 *pf(2)와 같이 쓰게되면 *연산자보다 ()연산자가 순위가 높아 pf(2) 호출문을 리턴하는 포인터로부터 대상체를 읽는 문장이 되어 버리는 결과가 나옵니다. * 연산자가 먼저 실행되어 이 포인터가 가리키는 함수를 찾은 후 이 함수로 인수를 넘겨야 합니다.

(*pf)(2) 호출 구문이 문법상 원칙적으로 맞는 방법이지만 괄호와 * 연산자를 쓰는 것이 번거롭기 떄문에 C컴파일러들은 좀 더 간략화된 호출 방법을 지원하는데 그것이 아래쪽의 pf(2)호출형식입니다.




(*pf)(2);
pf(2);

함수 포인터 다음에 인수의 목록을 나열하는데 function함수는 정수형 인수 하나를 취하므로 상수 2를 넘겨주었습니다. pf가 function의 번지를 가지고 있으니 *pf는 곧 function과 동일하고 그래서 (*pf)(2)는 function(2)와 같습니다. 이떄 *pf를 감싸는 괄호는 생략할 수 없는데 *pf(2)와 같이 쓰면 *연산자 보다 ()연산자가 더 순위가 높아서 pf(2) 호출문을 리턴하는 포인터로부터 대상체를 읽는 형태가 되어 버립니다. *연산자가 먼저 실행되어 이 포인터가 가리키는 함수를 찾은 후 이 함수로 인수를 넘겨야 합니다. (*pf)(2)호출 구문이 문법상 원칙적으로 맞지만 광호와 *연산자를 쓰는 것이 번거롭기 때문에 간략한 호출방법을 지원하는 것이 바로 pf(2)입니다. 함수 포인터를 마치 함수인 것처럼 쓰고 괄호안에 인수를 넘깁니다. 문법적으로 엄격하게 따진다면야 pf(2)는 잘못된 것이지만 컴파일러가 예외 인정합니다. pf가 함수포인터라는 것을 컴파일러가 알고 있으므로 굳이 *연산자와 ()괄호를 쓸지 않아도 pf가 가리키는 함수를 호출하는 문장이라는 것을 알 수 있으며 모호하지 않습니다. 그래서 함수 포인터를 함수와 동일한 방법으로 사용하는 것을 허용합니다. 다음소스는 함수 포인터를 사용한 예시입니다.
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
function 함수는 정수값 하나를 인수로 받아 이 값의 2배값을 리턴하는 함수입니다. main에서는 이런 형식의 함수를 가리킬 수 있는 함수 포인터 pf를 선언하고 function의 시작번지를 대입했습니다. 그리고(*pf)(3)호출문으로 function함수를 호출하여 그 리턴값을 지역변수 i에 대입한 후 출력했습니다. 인수로 3을 전달해서 리턴값은 6이되며 출력값도 6입니다. (*pf)(2)를 pf(2)로 바꾸어도 동일한 결과가 나옵니다.

pf가 가리키는 함수는 정수형 인수 하나만 취하도록 되어 있으므로 pf를 통해 함수를 호출할 때도 원형에 맞게 호출해야합니다.(*pf)(2.5)호출은 결고로 처리되며 (*pf)("string")호출은 컴파일 에러로 처리되는데 function()에 대입했을떄 에러가 나는것과 같은 이유입니다.

함수 포인터의 타입

함수 포인터 타입도 일종의 고유한 타입입니다. 따라서 원형이 다른 함수포인터 끼리는 곧바로 대입할 수 없으며 함수의 인수로도 넘길 수 없습니다. 정수형 포인터 변수(int *)에 실수형 포인터 변수(double *)의 값을 대입할 수 없는거와 마찬가지입니다. 다음 소스를 보겠습니다.
1
2
3
int (*pf)(char *);
void (*pf2)(double);
pf1=pf2;        //에러
cs
pf1형은 문자형 포인터를 인수로 취하고 정수형을 리턴하는 함수를 가리키는 함수 포인터이며 pf2는 실수를 인수로 취하고 리턴값이 없는 함수를 가리키는 함수 포인터입니다. 두 변수가 가리킬 수 있는 함수의 원형이 다르기 떄문에 pf2가 가리키는 번지를 곧바로 pf1에 대입할 수 없습니다. 만약 이것이 가능하다면 pf1로 함수를 호출할 떄 컴파일러는 char * 형의 인수를 찾미나 pf1이 가리키는 함수는 double형의 인수를 받아들이기 떄문에 함수가 제대로 호출되지 않을 것입니다. 함수 포인터가 가리킬 수 있는 원형과 같지 않은 함수의 번지를 대입하는 것도 똑같은 에러로 처리가 됩니다. 다음 코드에서 pf3은 문자형 포인터와 실수를 인수로 취하고 리턴값이 없는 함수를 가리키도록 선언했는데 function함수는 pf3와 원형이 다르므로 pf3에 function 함수의 번지를 대입할 수 없습니다.
1
2
3
4
int function(int a);
 
void (*pf3)(char *,double);
pf3 = function; //에러
cs
그러나 타입이 다른 함수 포인터끼리도 강제로 대입은 할 수 있는데 이것이 일단은 가능해야합니다. void 포인터에 저장된 함수의 번지를 대입받는다거나 자료 구조 설계시 미리 알 수 없는 함수에 대한 포인터를 다루고자 할 때입니다. 이럴 때 사용하는 것이 바로 캐스트 연산자이며 여러 가지 이유로 강제 캐스팅의 필요성은 누구나 인정하고 있습니다. int *pi와 double *pd가 있을떄 pi=pd대입은 금지가 되지만 pi=(int *) pd는 가능한 것처럼 함수 포인터도 타입에 맞게 캐스팅하면 강제로 대입할 수 있습니다. 데이터 포인터에서와 마찬가지로 함수 포인터에도 캐스트 연산자를 쓸 수 있는데 문제는 함수 포인터의 캐스트 연산자가 모양이 생소해서 조금 어렵다는 것입니다. 함수 포인터는 타입 자체가 길기 떄문에 캐스트 연산자의 모양도 상당히 복잡해 보입니다. 다음 코드는 pf2를 pf1에 강제로 대입하기 위해 캐스트 연산자를 사용한 것입니다.
1
2
3
int (*pf1)(char *);
void (*pf2)(double);
pf1=(int (*)(char *))pf2;
cs
이 식에서 (int(*)(char*))가 캐스트 연산자입니다. 함수 포인터형의 캐스트 연산자를 만드는 방법은 함수 포인터 선언식에서 변수명을 빼고 전체를 괄호로 한번 더 싸주면 됩니다. 공식같은거라 외우거나 찾아서 해야합니다.
1
2
3
4
5
int (*pf1)(char *)
//변수명 삭제
int (*)(char *)
//전체를 괄호로 감쌈
(int (*)(char *))
cs
pf1=(int(*)(char *))pf2 대입문은 pf2가 가리키는 번지를 문자형 포인트를 인수로 취하고 정수를 리턴하는 함수 포인터 타입으로 잠시 바꾼 후 pf1에 대입합니다. pf3에 function 함수의 번지를 강제로 대입할 떄도 마찬가지로 캐스트 연산자를 사용할 수 있습니다. 물론 이렇게 강제로 대입했을 떄의 부작용은 프로그래머가 책임져야합니다.
1
pf3=(void(*)(char *,double))function;
cs

캐스트 연산자가 길어보이지만 그 원리는 알면 어렵지는 않습니다. T형에 대해 T형 배열과 T형 포인터를 항상 선언할 수 있으므로 함수 포인터에 대해서도 배열과 포인터를 선언할 수 있습니다. function타입의 함수를 가리킬 수 있는 함수 포인터를 요소로 가지는 크기 5의 arpf배열은 다음과 같이 선언합니다.

1
int(*arpf[5])(int);
cs
함수 포인터 배열을 선언할 때는 변수명 다음에 첨자 크기를 밝혀 주면 됩니다. 잘못 생각하면 int(*arpf)(int)[5};가 맞을 것 같기도 하지만 첨자 크기는 반드시 변수명 다음에 써줘야 합니다. 이 선언에 의해 int(*)형의 함수의 번지를 가리킬 수 있는 함수 포인터 arpf[0]~arpf[4]까지 4개의 변수가 생성되며 각 변수는 int function(int)와 같은 원형의 함수를 가리키는 번지를 가질 수 있습니다. 동일한 타입의 변수들을 배열에 모아두면 루프를 돌면서 함수들을 순서대로 호출한다거나 하는 처리도 가능해집니다. 다음은 함수 포인터의 포인터를 선언해 보겠습니다. function 타입의 함수를 가리키는 함수 포인터를 가리키는 포인터 ppf는 다음과 같이 선언하는데 *만 하나 더 적어주면 됩니다.
1
int (**ppf)(int);
cs
이렇게 선언된 ppf는 int (*)(int)타입으로 선언된 함수 포인터 변수나 함수 포인터 배열을 가리킬 수 있는 이차 함수 포인터 변수입니다. ppf=&pf 또는 ppf=arpf 식으로 함수 포인터 변수의 번지를 대입받을 수 있으며 ppf로부터 함수를 호출할 때는 (**ppf)(2)형식을 사용합니다. 함수 포인터 변수의 번지를 대입받을 수 있으며 ppf로 부터 함수를 호출할 떄는 (**ppf)(2)형식을 사용합니다.

함수 포인터의 타입은 함수가 취하는 인수들의 타입과 리턴값까지 정확하게 밝혀야하기 떄문에 타입의 형식이 너무 길어서 쓰기가 번거롭습니다. 또한 함수 포인터로부터 파생된 타입을 만드는 것도 헷갈리고 생소한 면이 있습니다. 그래서 함수 포인터 타입을 자주 사용하거나 자신이 없으면 직접 타입을 기술하는 것보다 typedef로 함수 포인터 타입을 따로 정의한 후 사용하는 것이 편리합니다. int function(int)형의 함수를 가리키는 타입을 다음과 같이 정의합니다.
1
2
3
typedef int (*PFTYPE)(int);
 
PFTYPE pf;
cs

함수 포인터를 선언하는 문장에서 변수명을 원하는 타입 이름으로 바꾸고 앞에 typedef만 붙이면 됩니다. 이후 컴파일러는 PFTYPE이라는 명칭을 int (*)(int)타입으로 인식하므로 PFTYPE으로 함수 포인터 변수를 쉰게 선언할 수 있으며 캐스트 연산자로 사용할 수 있습니다. 또한 함수 포인터로부터 배열이나 포인터 같은 파생 변수를 선언하는 것도 훨씬 더 간편합니다. 또한 함수 포이터로부터 배열이나 포인터 같은 파생 변수를 선언하는 거도 훨씬 더 간단합니다.

1
2
PFTYPE arpf[5];
PFTYPE *ppf;
cs
마치 int 형으로부터 배열이나 포인터를 선언하듯이 PFTYPE을 사용할 수 있으므로 직관적이고 읽기도 좋습니다.

포인터로 함수 호출하기

그렇다면 함수 포인터는 왜 사용할까요? 앞에서는 함수 포인터를 통해 간접적으로 function 함수를 호출했는데 그냥 function(2)을 호출하는 것이 훨씬 더 간단하고 직관적인데 왜 굳이 함수 포인터가 필요한 걸 까요? 함수 포인터는 변수이기 떄문에 조건에 따라 언제든지 다른 함수를 가리킬 수 있습니다. 물론 하나의 함수 포인터가 가리킬 수 있는 함수의 원형은 일치해야 합니다. 함수 포인터를 인수로 사용하면 함수를 다른 함수로 전달하는 것도 가능해지며 함수 포인터 배열이나 구조체를 통해 여러 개의 함수군을 통째로 바꿔치기 할 수 있습니다.
다음 소스는 조건에 따라 함수를 바꿔가며 호출하는 것입니다.
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
function2는 입력된 값의 2배를 리턴하며 function3은 입력 값의 3배를 리턴하는데 두 함수는 원형이 동일합니다. main에서는 이 두 함수를 가리킬 수 있는 함수 포인터 pf를 선언하고 사용자의 입력에 따라 pf에 function2, function3중 하나를 대입해 둡니다. 호출할 함수의 번지를 pf가 가지고 있으므로 (*pf)로 호출하기만 하면 선택된 함수가 호출될 것입니다. 이 소스는 어디까지나 사용예를 보여주며 실용성은 없지만 다음과 같이 코드를 작성해도 결과는 동일합니다.
1
2
3
4
5
6
7
8
if(ch=='2')
{
    printf("결과값 = %d\n",function(2));
}
else
{
    printf("결과값 = %d\n",function(2));
}
cs
이렇지만 다음과 같은 경우는 함수 포인터를 사용하는것이 좋습니다.

    1. 선택해야 할 함수가 두개 이상인 경우, 예를 들어 수십개의 함수 중 하나를 호출해야 한다면 함수포인터 배열을 선언하고 그 첨자를 선택하는 것이 더 쉽습니다.

    2. 함수를 선택하는 시점과 실제로 호출하는 시점이 완전히 분리 되어 있는 경우도 함수 포인터를 쓰는 것이 유리합니다. 호출할 함수에 대한 조건 점검은 필요할 때 한번만 하고 선택된 함수는 별다른 조건 점검없이 함수 포인터로 바로 호출할 수 있습니다.

    3. 호출할 함수가 DLL 같은 외부 모듈에 있고 이 함수를 동적으로 연결할 경우는 컴파일할 때 함수의 존재가 알려지지 않으므로 반드시 함수 포인터를 사용해야합니다. 함수 포인터를 사용하면 이름으로부터 원하는 함수의 번지를 찾아 호출할 수 있습니다.
함수 포인터를 반드시 사용해야 하는 경우는 아주 많습니다. 대표적으로 개방 데이터 베이스 환경인 ODBC를 들 수 있는데 ODBC는 다양한 데이터 베이스 서버를 일관된 방법으로 다룰 수 있는 표준 인터페이스를 제공합니다. 언제든지 드라이버를 바꾸기만 하면 ODBC 표준을 따라는 모든 데이터 베이스 서버를 엑세스 할 수 있습니다. 각 드라이버는 ODBC 규약에 따라 미리 정해진 함수의 집합을 구현하며 ODBC 관리자는 응용 프로그램의 요구에 따라 드라이버의 함수를 찾아 호출하는데 이때 드라이버내의 함수 번지를 저장하기 위해 함수 포인터 배열이 사용됩니다.

함수 포인터 인수
 
함수 포인터는 함수를 가리키고는 있지만 어쨋거나 변수이기 떄문에 함수의 인수로 전달 될 수 있습니다. 함수를 함수의 인수로 전달한다는 것이 이상하긴 하지만 이렇게 하면  함수 내부에서 어떤 함수를 호출할 것인지를 호출측에서 지정할 수 있습니다. 함수 포인터가 아니라면 이것은 불가능합니다. 함수 포인터를 인수로 받아들이는 함수의 예는 많지만 대표적인 퀵 소트 함수인 qsort 함수의 원형을 보겠습니다.
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[] = { 123412312547612843561231523 };
 
    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))(intdouble)
{
    if (ch == 'a')
    {
        return f1;
    }
    else
    {
        return f2;
    }
}
 
void main()
{
    int (*fp)(intdouble);
 
    fp = SelectFunction('a');
    printf("리턴값 = %d\n", fp(12.3));
}
cs

SelectFunction 한수의 원형을 말로 풀어본다면 "나는 char형을 인수로 취하며 정수형과 실수형을 인수로 취하고 정수형을 리턴하는 함수 포인터를 리턴한다." 라고 할 수 있습니다. 이 함수가 리턴하는 포인터의 타입은 int(*)(int, double)형이며 위 소스는 이런 함수가 2개 있습니다. SelectFunction 함수는 인수로 전달된 char형 인수가 'a'이면 f1을 리턴하고 그렇지 않으면 f2를 리턴합니다. 결국 SelectFunction 함수는 주어진 조건을 계싼하여 적절한 함수를 선택한다고 할 수 있습니다. 선언문이 생소하지만 typedef를 거치면 볼만 합니다.

1
2
typedef int (*PF)(intdouble);
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(212));
    printf("3+4+5+6=%d\n", GetSum(43456));
    printf("10~15=%d\n", GetSum(6101112131415));
 
}
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