본문으로 바로가기
homeimage
  1. Home
  2. 컴퓨터/프로그래밍
  3. C언어 포인터함수와 콜백함수 예제 설명

C언어 포인터함수와 콜백함수 예제 설명

· 댓글개 · 바다야크

C언어 포인터 이해하기 시리즈 4부입니다. 컴퓨터에 있는 프로그램을 실행하면 프로그램이 먼저 메모리에 올라옵니다. 즉, 변수뿐만 아니라 프로그램도 메모리를 차지하는 것이죠. 당연한 얘기를 입니다만, 함수도 시작 주소가 있을 것입니다. 포인터가 변수의 메모리 주소를 갖는다면, 함수 주소도 역시 포인터로 받을 수 있습니다. 변수의 주소가 아닌 함수의 시작 주소를 담은 포인터 변수를 통해 호출하는 함수를 포인터 함수라고 합니다.

함수 이름을 직접 호출하는 것이 아니라 함수의 주소를 가지고 있는 포인터 변수로 호출하는 것이며, 포인터 변수는 고정이 아니라 코딩에 따라 여러 함수의 주소로 바꿀 수 있습니다. 즉, 함수 이름을 직접 부르는 것을 정적 호출이라고 한다면, 포인터 변수를 통하는 방법을 동적 호출이라고 합니다. 함수 이름으로 실행하면 될 것을 굳이 포인터 변수를 이용하는 이유가 무엇일까요?

C언어 콜백함수 구현

포인터 함수로 콜백함수(Callback)를 구현할 수 있습니다. 콜백함수는 인수로 넘겨받은 함수를 말합니다. 일반적으로 함수는 숫자나 문자 같은 데이터를 받다 보니 인수로 함수를 받는다고 하면 낯설 텐데요, 변수가 숫자·문자 같은 데이터를 넘겨준다고 한다면, 콜백함수는 특정 기능을 넘기는 것입니다.

즉, 어떤 조건이 발생하면 인수로 넘긴 콜백함수를 실행해 달라는 것으로 함수를 호출하는 쪽에서 어떤 조건이 발생하면 처리해야 할 내용을 미리 작성해서 콜백함수로 넘겨주는 것입니다. 콜백함수를 사용하면 라이브러리와 같은 기존 코드를 수정하지 않으면서도 다양한 기능을 실행할 수 있습니다.

예를 들어보겠습니다. 정렬 함수를 만들어서 라이브러리로 제공하려고 합니다.

#include <stdio.h>
#include <string.h>
//////////////////////////////////////////// 라이브러리 함수
void sort( int *data, int sz_data){
   int *ptr;
   int  ndx;
   int  tmp;
   int  is_done;

   while( 1){
      ptr = data;
      is_done = 1;
      for ( ndx = 0; ndx < sz_data-1; ndx++){
         if ( *ptr > *( ptr+1)){
            tmp = *ptr;
            *ptr = *(ptr+1);
            *(ptr+1) = tmp;
            is_done = 0;
         }
         ptr++;
      }
      if (is_done)  break;
   }
}
//////////////////////////////////////////// 개발자 코드
int main( void)
{
   int   ary[10] = { 4, 5, 1, 2, 6, 7, 2, 0, 9, 8};
   int   ndx;

   sort( ary, 10);

   for ( ndx = 0; ndx < 10; ndx++){
      printf( "%d\n", ary[ndx]);
   }

   return 0;
}

정렬을 위해 두 개의 변수 값을 비교하고 결과에 따라 변수 값을 교환하는 코드를 넣어서 sort() 함수를 작성하고 라이브러리로 만들었습니다.

$ ./a.out
0
1
2
2
4
5
6
7
8
9
$

실행해 보니 잘 되네요. 그래서 일이 끝났다고 좋아하며 다른 개발자에게 제공했습니다. 그런데, 그 개발자가 올림차순이 아니라 내림차순이 필요하다며 수정하거나 아니면 내림차순으로 정렬하는 함수를 추가해 달라고 합니다.

이런 경우 내림차순 함수를 추가하기보다는 변수 값 비교와 치환하는 부분을 라이브러리를 사용하는 개발자가 작성할 수 있도록 제공하는 것이 좋습니다. 즉, 라이브러리를 쓸 사람이 마음대로 활용하라는 것이죠. 이렇게 하려면 라이브러리 안에서 개발자가 작성한 비교·치환하는 함수를 호출해야 하는데, 그 개발자가 무슨 이름으로 함수로 만들지 어떻게 알겠습니까?

어쩔 수 없이 개발자에게 sort_user() 이름으로 꼭 만들라고 부탁할까요? 이 방법이 가능하다고 해도 sort() 함수는 무조건 sort_user()만 호출하게 될 텐데, 제약이 너무 큽니다. 개발자가 만든 sort_user()는 내림차순 코드만 넣었는데, 나중에 다른 루틴에서 올림도 필요하다면? 어쩔 수 없이 새로 함수를 추가해야 합니다.

간단한 예를 들기 위해 정렬 함수를 꺼내서 경우의 수가 올림·내림 두 가지이지만, 정렬 함수가 아니라 더 다양한 처리를 해야 한다면 이런 방법으로는 코딩이 너무 지저분해집니다. 라이브러리 함수라고 얘기하기도 부끄럽습니다.

이럴 때 콜백함수를 사용합니다. 라이브러리에 있는 정렬 함수를 호출할 때 데이터만 보내는 것이 아니라 비교·치환하는 함수를 함께 인수로 던지는 것이죠. 이렇게 콜백함수로 라이브러리를 만들면 외부 요청에 의해 수정하는 일이 크게 줄어듭니다.

#include <stdio.h>
#include <string.h>

//////////////////////////////////////////// 라이브러리 함수
void sort( int *data, int sz_data, int (*call_back)( int *, int *) ){
   int *ptr;
   int  ndx;
   int  is_done;

   while( 1){
      ptr = data;
      is_done = 1;
      for ( ndx = 0; ndx < sz_data-1; ndx++){
         if ( !call_back( ptr, ptr+1))     // 교환했다면
            is_done = 0;                  // 계속 교환할 것이 있을 수 있으므로
         ptr++;
      }
      if (is_done)  break;
   }
}

//////////////////////////////////////////// 개발자 코드

int sort_descending( int *data1, int *data2){
   int  tmp;

   if ( *data1 < *data2){
      tmp = *data1;
      *data1 = *data2;
      *data2 = tmp;
      return 0;     // 교환 했음
   }
   return 1;  // 교환하지 않았음
}

int main( void)
{
   int   ary[10] = { 4, 5, 1, 2, 6, 7, 2, 0, 9, 8};
   int   ndx;

   sort( ary, 10, sort_descending);

   for ( ndx = 0; ndx < 10; ndx++){
      printf( "%d\n", ary[ndx]);
   }

   return 0;
}

라이브러리 제공자는 sort() 함수만 만들고 개발자는 정렬 함수까지 넣어서 호출합니다. 위 예제에서는 내림차순 함수를 만들어서 호출했습니다.

$ ./a.out
9
8
7
6
5
4
2
2
1
0
$

실행해보니 내림차순으로 잘 정렬되었네요.

 

데이터와 함수의 짝짓기

제목을 짧게 짓기 위해 적다 보니 데이터와 함수의 짝짓기라고 썼습니다. C++ 객체화의 가장 큰 특징은 변수와 함수를 한 몸으로 묶을 수 있다는 것입니다. 객체를 선언할 때 접근 순위를 두어서 그 객체의 변수(프로퍼티)를 그 객체에 포함된 함수로만 다룰 수 있습니다. C언어는 특정 변수를 특정 함수에서만 처리하는 것은 오로지 프로그래머의 재량에 달려 있습니다.

데이터에 따라 실행하려는 함수를 다르게 하려면 if 절을 이용해야 합니다. 예를 들어서 아래처럼요.

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

#define FUN_LENGTH   0
#define FUN_TEXT     1

void fun1(char *ptr){
   printf( "문자열 길이= %ld\n", strlen( ptr));
}

void fun2(char *ptr){
   printf( "문자열 내용= %s\n", ptr);
}

void fun( char *ptr, int fun_num){
   if (fun_num == FUN_LENGTH) fun1( ptr);
   else fun2( ptr);
}

int main( void)
{
   fun( "길이가 궁금해", FUN_LENGTH);
   fun( "내용이 궁금해", FUN_TEXT);

   return 0;
}

포인터 함수를 이용하면 아래와 같이 작성할 수 있습니다.

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

void fun1(char *ptr){
   printf( "문자열 길이= %ld\n", strlen( ptr));
}

void fun2(char *ptr){
   printf( "문자열 내용= %s\n", ptr);
}

int main( void)
{
   void (*fun)( char *ptr);

   fun = fun1; fun( "길이가 궁금해");
   fun = fun2; fun( "내용이 궁금해");

   return 0;
}

포인터 변수로 함수 주소를 받아서 실행했습니다. 함수를 동적으로 호출하는 모습이 확실이 보이지요? 그렇다면 굳이 이렇게 하는 이유가 무엇일까요? FUN_LENGTH와 FUN_TEXT를 제거하고 if 절을 없애기 위해서일까요? 이유가 될 수 있지만, 반드시는 아닙니다. 오히려 if 절을 사용하는 것이 프로그램을 이해하기 쉽습니다.

C언어에서 포인터 함수에 스트럭쳐를 함께 사용하면 변수와 실행 함수를 묶어서 인수로 넘길 수 있습니다. C++처럼 강력한 접근 제한자가 없지만, 데이터에 따라 처리하는 함수를 묶을 수 있고 다른 함수에 인수로 넘길 수 있습니다.

#include <stdio.h>
#include <string.h>

typedef struct {
   void (*fun)( char *ptr);
   char *data;
} object_t;

void fun1(char *ptr){
   printf( "문자열 길이= %ld\n", strlen( ptr));
}

void fun2(char *ptr){
   printf( "문자열 내용= %s\n", ptr);
}

void call_fun( object_t *ptr){

   ptr->fun( ptr->data);
}

int main( void)
{
   object_t obj1 = { fun1, "길이가 궁금해"};
   object_t obj2 = { fun2, "내용이 궁금해"};

   call_fun( &obj1);
   call_fun( &obj2);
   return 0;
}

실행하면 이렇게 출력됩니다.

$ ./a.out
문자열 길이= 19
문자열 내용= 내용이 궁금해
$

이외에도 필요에 따라 포인터 함수를 다양하게 활용할 수 있습니다.

SNS 공유하기
💬 댓글 개
최근글
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.