프로그래밍

c언어/c++ 함수포인터

업글 2020. 11. 26. 23:18

안녕하세요 업글입니다! 함수포인터에 대해서 포스팅 해보겠습니다.

 

함수포인터는 이름에서부터 알 수 있듯이 함수의 주소를 가르키는 포인터입니다.

여기서 함수도 주소가 있을까하는 의문이 드시는분도 있으실 겁니다.

함수는 메모리의 코드영역에 위치하기 때문에 변수와 같이 주소를 가지게 됩니다.

 

저의 경우 대체적으로 함수포인터가 가르키는 함수를 호출하는 간단한 예제(특히 더하기, 빼기, 곱하기, 나누기 예제)를 통해서 함수포인터의 문법에 대해서만 설명하는 책들을 많이 봤었습니다. 

반면에 함수포인터를 어떤 경우에 유용하게 사용할 수 있는지에 관한 내용은 본 적이 크게 없는 것 같습니다.

개인적인 생각으로는 문법에 대해서 이해해도 사용하지 않으면 문법에 대한 이해는 쓸모없다고 생각합니다.

 

이번 포스팅에서는 함수포인터가 유용하게 사용되는 경우에 대해서 중점적으로 설명드리겠습니다.

 

본론을 설명 드리기 전에 문법에 대해서 간단히 알아보겠습니다.

함수포인터의 문법

함수포인터는 아래의 형태로 선언할 수 있습니다.

반환 자료형 (*함수 포인터의 이름)(함수 인자);

 

선언에 대해서 4가지의 경우를 예시로 들어서 설명드리겠습니다.

 

첫번째, 반환 자료형과 함수 인자가 없는 경우

void (*pFunc)(void);

 

두번째, 반환 자료형은 있고 함수 인자가 없는 경우

int (*pFunc)(void);

 

세번째, 반환 자료형은 없고 함수 인자가 있는 경우

void (*pFunc)(int);

 

네번째, 반환 자료형과 함수 인자가 있는 경우

int (*pFunc)(int);

함수포인터 선언 후 함수포인터가 특정 함수를 가르키도록 하기위해서는 아래와 같이 구현하면 됩니다.

 pFunc = InputFunction; // 방법1
 pFunc = &InputFunction; // 방법2

 

함수 이름이 주소를 나타내기 때문에 함수포인터에 이름을 대입할 수 있습니다.

 

여기서 pFunc = InputFunction() 이러한 형태로 실수하지 않도록 주의하셔야 합니다.

 

함수포인터가 가르키는 함수 호출 시에는 아래와 같이 구현하면 됩니다.

함수인자가 없는 경우,

 1) 암시적 역참조 : pFunc();
 2) 명시적 역참조 : (*pFunc)();

 

함수인자가 있는 경우,

 1) 암시적 역참조 : pFunc(인자 값);
 2) 명시적 역참조 : (*pFunc)(인자 값);

 

주로 암시적 역참조를 통해서 함수 호출을 하지만 편안한 방법으로 사용하시면 됩니다.

 

가장 중요하게 알고 가야될 점은 함수포인터는 동일한 반환 자료형과 함수 인자를 가지는

함수만을 가르킬 수 있다는 점입니다.

 

위의 간단한 문법에 대한 설명에서 보셨듯이 함수포인터를 통해서 함수포인터가 가르키는 함수를 호출할 수 있습니다.

이 특징을 보면 함수포인터의 역참조하는 부분의 코드에 대한 수정없이 함수포인터가 가르키는 함수 변경을 통해서

원하는대로 다른 함수를 호출할 수 있다는 점입니다.

 

본론으로 함수포인터를 유용하게 사용하는 경우에 대해서 설명드리겠습니다.

 

함수포인터가 유용하게 사용되는 경우

첫번째, 상황에 따라 부분적인 로직이 달라져야 하는 경우

예를 들어서 어떠한 알고리즘을 진행한 후 마지막 로직이 상황에 따라 다른 경우

마지막 로직 부분을 함수포인터로 실행하게 하고 상황에 따른 로직을 함수로 각각 만들어서

함수포인터에 입력해서 마지막 로직을 다르게 실행 시킬 수 있습니다.

 

설명으로는 이해가 힘드니 간단한 코드 예시로 설명드리겠습니다.

int main()
{
    int result;

    result = function(Square);
    result = function(Add);

    return 0;
}

int function(int(*pFunc)(int value))
{
    int calcResult, ret;

    calcResult = algorithm(); // 어떠한 알고리즘 실행
    ret = pFunc(calcResult); // 상황에 따라서 다른 로직을 수행

    return ret;
}

int Square(int value)
{
    return (value * value);
}

int Add(int value)
{
    return (value + value);
}

function함수에서 algorithm함수 실행까지 동일하게 동작하고 그 후의 로직만 상황에 따라 변경되기 때문에 그 후의 로직을 함수포인터로 처리하여 상황에 따라서 function의 인자에 로직을 수행하는 함수를 입력하여 마지막 로직을 다르게 실행할 수 있습니다.

 

여기서 함수포인터를 인자로 받지 않고 아래의 코드와 같이 function함수를 복사하여 마지막 로직만 수정하면 되지라는 생각을 하실 수 있을 것 같습니다.

int functionSquare()
{
    int calcResult, ret;

    calcResult = algorithm(); // 어떠한 알고리즘 실행
    ret = Square(calcResult);

    return ret;
}

int functionAdd()
{
    int calcResult, ret;

    calcResult = algorithm(); // 어떠한 알고리즘 실행
    ret = Add(calcResult);

    return ret;
}

int Square(int value)
{
    return (value * value);
}

int Add(int value)
{
    return (value + value);
}

이러한 경우 마지막 로직이 변경될 때 마다 함수를 추가하여 중복된 코드를 추가하므로 코드가 길어지고 복잡해지게 됩니다. 반면에 함수포인터를 사용하게 되면 function함수의 코드를 수정하지 않으므로 검증된 function함수를 그대로 사용할 수 있으며 마지막 로직이 변경되는 경우 중복되는 함수를 여러 개 만들지 않고 마지막 로직에 관한 함수만 새로 만들면 되기 때문에 코드가 깔끔해지고 코드 추가 및 수정이 용이해집니다.

또한 신뢰성시험을 진행한다고 가정했을 때 위와 같이 함수를 추가하는 경우 기존 함수와 복사한 함수 모두 신뢰성 시험을 진행해야하지만 함수포인터를 사용하면 함수포인터의 인자로 입력되는 함수에 대해서만 신뢰성 시험을 진행하면 됩니다.

 

두번째, 여러 자료형을 적용해야하는 경우

이 경우를 가장 잘 나타낼 수 있는 예시는 stdlib.h의 qsort함수입니다.

void qsort (void *base, size_t number, size_t width, int (*compare)(const void *, const void *);

qsort의 함수의 구조는 위와 같습니다. 여기서 마지막 함수의 인자가 함수포인터임을 확인할 수 있습니다.

각 인자에 대해서 간단히 설명드리겠습니다.

 1. base : 정렬 하고자 하는 배열의 시작 주소를 입력받는 포인터

 2. number : 배열 요소의 개수(ex. int arr[10]일 때, 10 = sizeof(arr)/sizeof(int))

 3. width : 배열 요소의 크기(ex. 32bit 시스템에서 int arr[10]일 때, 4 = sizeof(int))

 4. compare : 비교함수를 입력받는 함수포인터 / compare에 입력되는 함수의 반환 값이 1일 때 swap

 

아래의 int형 배열과 ST_Score라는 구조체 배열을 qsort를 사용하여 오름차순으로 정렬하는 예제를 통해 설명 드리겠습니다.

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

typedef struct {
    unsigned int mathScore;
    float englishScore;
}ST_Score;

int intCompare(const void* element1, const void* element2)
{
    int* element1Ptr = (int*)element1;
    int* element2Ptr = (int*)element2;

    if (*element1Ptr > * element2Ptr) {
        return 1;
    }
    else if (*element1Ptr < *element2Ptr) {
        return -1;
    }
    else {
        return 0;
    }
}

int mathScoreCompare(const void* element1, const void* element2)
{
    ST_Score* element1Ptr = (ST_Score*)element1;
    ST_Score* element2Ptr = (ST_Score*)element2;

    if (element1Ptr->mathScore > element2Ptr->mathScore) {
        return 1;
    }
    else if (element1Ptr->mathScore < element2Ptr->mathScore) {
        return -1;
    }
    else{
        return 0;
    }
}

int englishScoreCompare(const void* element1, const void* element2)
{
    ST_Score* element1Ptr = (ST_Score*)element1;
    ST_Score* element2Ptr = (ST_Score*)element2;

    if (element1Ptr->englishScore > element2Ptr->englishScore) {
        return 1;
    }
    else if (element1Ptr->englishScore < element2Ptr->englishScore){
        return -1;
    }
    else{
        return 0;
    }
}

int main(void)
{
    int i;

    int arr[] = { 2,5,4,1,6,8,9,7,3,10 };
    ST_Score scoreArr[] = {
        {3, 5.0},
        {2, 4.0},
        {4, 3.0},
        {1, 2.0},
        {5, 1.0},
    };

    qsort(arr, _countof(arr), sizeof(arr[0]), intCompare);
    for (i = 0; i < _countof(arr); i++) {
        printf("%d\n", arr[i]);
    }

    qsort(scoreArr, _countof(scoreArr), sizeof(scoreArr[0]), mathScoreCompare);
    for (i = 0; i < _countof(scoreArr); i++) {
        printf("%d, %f\n", scoreArr[i].mathScore, scoreArr[i].englishScore);
    }

    qsort(scoreArr, _countof(scoreArr), sizeof(scoreArr[0]), englishScoreCompare);
    for (i = 0; i < _countof(scoreArr); i++) {
        printf("%d, %f\n", scoreArr[i].mathScore, scoreArr[i].englishScore);
    }

    return 0;
}

예제의 코드가 생각보다 길지만 이해하시는데 큰 어려움은 없을 것으로 생각됩니다. intCompare, mathScoreCompare, englishScoreCompare함수를 통해서 자료형에 무관하게 qsort함수를 그대로 사용할 수 있다는 것을 확인하실 수 있습니다. 여러 자료형을 입력 받기 위해서는 함수의 인자로 꼭 void 포인터를 사용해야한다는점 기억해주시길 바랍니다.

 

또한, qsort의 경우 상황에 따라 비교하는 함수의 로직이 달라져야 하므로 앞에서 설명드린 첫번째 상황의 예시로도 설명할 수 있겠습니다.

 

세번째, 콜백

코드를 사용하는 사람이 원하는 함수를 실행시킬 수 있다.

이 내용은 첫번째, 두번째의 예제로도 설명가능한 내용이지만 조금 다른 예제로 설명드리겠습니다.

 

어떠한 팀에서 프로젝트를 진행한다고 했을 때 한 팀원이 통신에 대한 부분의 코드를 작성하는 일을 맡았다고 가정합니다. 하지만 아직 정확히 어떠한 인터페이스를 사용하여 통신을 할지 정해지지 않았거나 인터페이스가 변하는 경우에도 코드 유지보수를 효율적으로 하기위해서 어떻게 코드를 작성해야할까요? 이 때 콜백을 사용할 수 있다고 생각합니다.

 

main.c

#include "comm.h"

int main()
{
    int data[] = { 1,2,3,4,5 };

    CommInit();

    Send((unsigned char*)data, sizeof(data));

    return 0;
}

comm.h

#ifndef _COMM_H
#define _COMM_H

extern int (*Send)(unsigned char* data, unsigned int size);

void CommInit();

#endif

comm.c

#include "comm.h"
#include <stdio.h>

#define COMM (TCP_IP)

#define TCP_IP (0)
#define UART   (1)

int (*Send)(unsigned char* data, unsigned int size);

int TcpIpSend(unsigned char* data, unsigned int size)
{
    int result=0;

    printf("TcpIpSend\n");

    /* TcpIp Send 코드 */
    

    return result;
}

int UartSend(unsigned char* data, unsigned int size)
{
    int result=0;

    printf("UartSend\n");

    /* Uart Send 코드 */
    

    return result;
}

void CommInit()
{
#if (COMM == TCP_IP)
    Send = TcpIpSend;
#elif (COMM == UART)
    Send = UartSend;
#endif
}

 

Send 함수포인터에 사용하고자 하는 인터페이스의 Send함수를 입력하여 사용하고자 하는 인터페이스를 사용할 수 있습니다. 또한, 다른 부분의 코드를 작성하는 팀원은 데이터를 송신 해야하는경우 Send함수에 전달하고자 하는 데이터와 데이터 사이즈 입력하고 Send함수를 호출하여 사용할 수 있습니다. 인터페이스가 변경되는 경우에도 comm.c 파일에서 통신관련부분의 코드만 수정하면 되기 때문에 유지보수에 용이하게 됩니다.

 

네번째, 분기 삭제

함수포인터 배열을 통해 분기문을 삭제하여 코드를 간결하게 표현할 수 있다.

 

예를 들어서 통신을 통해서 데이터를 수신할 때 메시지에 따라서 다른 로직을 수행해야하는 경우 메시지에 따른 분기로 처리해줘야합니다. 아래의 코드로 설명드리겠습니다.

 

 

 

switch를 통해서 메시지에 따른 분기 처리

typedef struct {
    int msgId;
    int data;
}ST_ReceiveData;

void recv(ST_ReceiveData* recvData)
{
    /*통신으로 수신 받은 데이터를 recvData에 입력하는코드 */
}

void FunctionMsgId0(ST_ReceiveData * recvData)
{
}

void FunctionMsgId1(ST_ReceiveData* recvData)
{
}

void FunctionMsgId2(ST_ReceiveData* recvData)
{
}

int main()
{
    ST_ReceiveData recvData;

    recv(&recvData);

    switch (recvData.msgId) {
        case 0:
            FunctionMsgId1(&recvData);
            break;
        case 1:
            FunctionMsgId2(&recvData);
            break;
        case 2:
            FunctionMsgId3(&recvData);
            break;
        default:
            break;
    }
}

함수 포인터 배열을 사용하여 분기 삭제

typedef struct {
    int msgId;
    int data;
}ST_ReceiveData;

void recv(ST_ReceiveData* recvData)
{
    /*통신으로 수신 받은 데이터를 recvData에 입력하는코드 */
}

void FunctionMsgId0(ST_ReceiveData * recvData)
{
}

void FunctionMsgId1(ST_ReceiveData* recvData)
{
}

void FunctionMsgId2(ST_ReceiveData* recvData)
{
}

int main()
{
    ST_ReceiveData recvData;
    void (*MsgProcess[])(ST_ReceiveData * recvData) = {
        FunctionMsgId0, // msg0
        FunctionMsgId1, // msg1
        FunctionMsgId2, // msg2
        // msg추가 시 해당 함수 추가
    };

    recv(&recvData);

    MsgProcess[recvData.msgId](&recvData);
}

메시지가 추가되면 switch문 분기로 인해서 코드가 길어지게 됩니다. 반면에 함수 포인터 배열을 사용하는 경우 메시지가 추가될 때 함수포인터 배열에 해당 함수를 추가해주면 되기 때문에 유지보수가 용이해지며 분기문이 없기 때문에 가독성이 좋아지게 됩니다.

 

이상 함수포인터를 유용하게 사용하는 경우에 대해서 마무리 하겠습니다.

'프로그래밍' 카테고리의 다른 글

c언어/c++ 메디안필터  (0) 2020.12.14
c언어/c++ 이동평균필터  (0) 2020.12.13
c언어/c++ sizeof  (0) 2020.11.12
쓰레드(Thread)에 대한 생각  (0) 2020.11.12
c언어/c++ int 크기  (0) 2020.11.11