C & C++/디딤돌 C언어

Part 33. 함수 포인터와 콜백

언휴 2024. 1. 23. 14:08

Part 33. 함수 포인터와 콜백

115. 함수 포인터와 콜백 개요

C언어 함수 포인터

컴퓨터 프로그램은 수행할 작업들을 논리적으로 표현한 코드의 집합이라고 말할 수 있어요.
그리고 단위 작업의 논리 전개를 한 것이 함수죠.
컴퓨터 CPU내에는 현재 수행할 코드 메모리 주소를 기억하는 프로그램 카운터(PC) 레지스터가 있어요.
대부분의 코드는 수행한 후에 프로그래머 카운터를 1 증가하여 다음 위치에 있는 코드를 수행하죠.
하지만 함수를 호출하면 피호출 함수의 코드로 분기하고 끝나면 호출한 코드 다음 위치에 있는 코드가 동작해요.

이렇게 동작할 수 있는 이유는 함수 이름은 해당 함수가 수행할 코드의 시작 메모리 주소를 값으로 갖고 있기 때문이예요.
배열명이 원소 형식의 포인터라고 말하는 것처럼 함수명은 해당 함수의 시작 메모리 주소를 의미하여 함수 포인터라 부르죠.

함수 포인터는 피호출 함수에서 대부분의 논리를 구현할 수 있지만 일부 논리를 호출하는 곳에서 결정할 필요가 있을 때 입력 매개 변수로 사용하는 등의 작업에서 사용할 수 있어요.
응용 프로그램은 시스템을 사용하기 위해 일반적으로 시스템을 호출하죠.
하지만 특정 사건이 발생하는 것을 운영체제에 위임하게 하고 특정 사건이 발생하는 것을 통보해 주게 설정할 수 있어요.
그리고 사건이 발생하면 운영체제는 응용에서 설정한 함수를 호출하게 할 수 있어요.
이 때 운영체제에 설정한 함수를 운영체제가 호출하는 것은 일반적인 호출 방향의 역방향이어서 콜백이라 불러요.
응용 프로그램 내부에서도 함수 포인터를 사용하면 이러한 콜백 원리를 이용할 수 있어요.

이번 장에서는 이러한 함수 포인터와 콜백이 무엇인지 기본적인 내용을 다룰게요.

116. 함수 포인터

함수 포인터는 시그니쳐를 원소 형식으로 취급해요.
두 개의 정수 형식을 입력 인자로 받고 반환 형식이 정수인 함수 포인터 변수는 int (*fun)(int ,int); 처럼 선언할 수 있어요.

그리고 fun 포인터 변수에 함수 원형이 같은 함수 이름을 대입할 수 있죠.
함수 포인터 변수는 함수 호출과 같은 방법으로 사용할 수 있어요.
또한 typedef int (*Fun)(int ,int); 처럼 함수 포인터 형식명을 정의하여 Fun fun; 처럼 변수 선언에 사용할 수도 있죠.

예를 들어 두 개의 정수를 입력 인자로 받아 두 수의 합과 차, 곱, 나누기를 하여 결과를 반환하는 함수들이 있다고 가정해요.
그리고 typedef int (*Fun)(int,int); 처럼 함수 포인터 형식명을 정의하기로 해요.
Fun 형식을 원소로 하는 배열을 선언할 수도 있죠.
그리고 각 원소에 두 수를 입력 인자로 받아 사칙 연산을 수행하여 결과를 반환하는 함수명으로 대입할 수 있어요.
그리고 배열의 원소를 함수처럼 사용할 수 있답니다.

typedef int(*Fun)(int,int); //함수 포인터 형식 Fun 정의
…중략…
Fun arr[4] = {Add,Sub,Mul,Div};
…중략…
printf(“%d\n”, arr[i](9,2));
◈ 함수 포인터 변수를 사용하는 예

#include <stdio.h>
typedef int(*Fun)(int,int); //함수 포인터 형식 Fun 정의
int Add(int a,int b);
int Sub(int a,int b);
int Mul(int a,int b);
int Div(int a,int b);
 
int main()
{
    Fun arr[4] = {Add,Sub,Mul,Div};
    int i = 0;
 
    for(i=0; i<4; i++)
    {
        printf("%d\n", arr[i](9,2));
    }
    return 0;
}  
 
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)
{
    if(b)
    {
        return a/b;
    }
    return 0;
}

◈ 실행 결과

11
7
18
4

117. 콜백

C언어 콜백

이번에는 함수 포인터를 콜백으로 이용하는 간단한 예를 살펴보아요.

정렬 알고리즘은 원소 형식이 무엇인지에 상관없이 대부분 같아요.
하지만 비교하는 것은 차이가 있죠.
만약 번호와 이름을 멤버로 갖는 학생 구조체 배열을 번호순으로 정렬한다면 멤버 번호로 비교해야 할 거예요.
이름순으로 정렬한다면 멤버 이름으로 비교해야겠죠.

이럴 때 비교하는 논리를 전달받아 형식에 관계없이 정렬하는 함수를 구현할 수 있어요.
이 때 비교하는 논리를 전달 받기 위해 함수 포인터를 사용해요.
호출하는 곳에서는 비교 함수를 정의하여 정렬 함수에 이를 입력 인자로 전달해요.
그리고 정렬 함수에서는 전달받은 비교 함수를 이용하여 정렬하는 것죠.
결국 피호출 함수인 정렬 함수에서 호출한 곳에서 정의한 비교 함수를 호출하는 것이므로 콜백이라 할 수 있어요.

typedef int (*Compare)(void *,void *); //비교한 결과를 반환하는 함수 포인터 정의

void Sort(void **base,size_t asize,Compare compare)
{
    ... 중략...
    if(compare(base[i-1], base[i])>0) // 앞의 요소가 더 클 때
    ... 중략 ...
}

int ComareByNum(Stu *stu1,Stu *stu2)
{
    return stu1->num - stu2->num;
}
int CompareByName(Stu *stu1,Stu *stu2)
{
    return strcmp(stu1->name,stu2->name);
}

◈ 콜백을 이용한 정렬 함수 구현 및 사용 예

#pragma warning(disable:4996)
#include <stdio.h>
#include <malloc.h>
#include <memory.h>
#include <string.h>
typedef int (*Compare)(void *,void *); //비교한 결과를 반환하는 함수 포인터 정의
typedef struct _Stu Stu;
#define MAX_NAME_LEN  20
struct _Stu
{
    int num;
    char name[MAX_NAME_LEN+1];
};
void StuSut(Stu *stu, int num,const char *name);
Stu *NewStu(int num,const char *name)
{
    Stu *stu = (Stu *)malloc(sizeof(Stu));
    StuSut(stu,num,name);
    return stu;
}
void StuSut(Stu *stu, int num,const char *name)
{
    stu->num = num;
    memset(stu->name,0,sizeof(stu->name));
    strncpy(stu->name,name,MAX_NAME_LEN);
}
void DeleteStu(Stu *stu)
{
    free(stu);
}
void StuView(Stu *stu)
{
    printf("번호:%d 이름:%s\n",stu->num,stu->name);
}
void BubbleSort(void **base,int asize,Compare compare)
{
   void *temp;
    int i = 0;
    for( ; asize>1 ;asize--) //정렬해야 할 사이즈가 1개 이상이라면 반복
    {
        for( i=1; i<asize ;i++)//비교해야 할 인덱스가 asize보다 작다면
        {
            if(compare(base[i-1], base[i])>0) // 앞의 요소가 더 클 때
            {
                //두 개의 요소를 교환
                temp = base[i-1];
                base[i-1] = base[i];
                base[i] = temp;
            }
        }
    }
}
int CompareByNum(Stu *stu1,Stu *stu2)
{
    return stu1->num - stu2->num;
}
int CompareByName(Stu *stu1,Stu *stu2)
{
    return strcmp(stu1->name,stu2->name);
}
 

int main()
{
    Stu *arr[4]={0,};
    int i = 0;
    arr[0] = NewStu(3,"홍길동");
    arr[1] = NewStu(11,"강감찬");
    arr[2] = NewStu(6,"김구");
    arr[3] = NewStu(8,"을지문덕");
    BubbleSort(arr,4,CompareByNum);
    printf("번호순\n");
    for(i=0;i<4;i++)
    {
        StuView(arr[i]);
    }
    printf("이름순\n");
    BubbleSort(arr,4,CompareByName);
    for(i=0;i<4;i++)
    {
        StuView(arr[i]);
    }
    for(i=0;i<4;i++)
    {
        DeleteStu(arr[i]);
    }
    return 0;
}

이 외에도 많은 곳에서 함수 포인터를 활용할 수 있어요.
여기서는 이 정도로 함수 포인터를 소개하고 마무리를 할게요.

결국 프로그래밍 실력은 문제 해결을 위한 자신의 고민과 스트레스 속에서 조금씩 성장하는 것 같아요.
작은 문제라도 자신이 직접 작성하는 것은 쉬운 일이 아니죠.
그리고 작은 문제라도 자신이 직접 해결하면 무언가 뿌듯함을 느낄 수 있을 거예요.
여러분 모두 훌륭한 개발자가 되길 기원할게요.
그 동안 수고했어요.