C++ 함수 포인터 완벽 이해: 콜백 구조로 배우는 함수 포인터

📚 델리게이트 시리즈

1편: 함수 포인터
2편: std::function
3편: Delegate 구현
4편: 엔진 스타일 Delegate

🚀 들어가며

게임에서 키를 눌렀을 때 특정 함수가 실행되는 구조는 어떻게 만들어질까요?

“나중에 실행할 함수”를 저장할 수 있다면,
이벤트 시스템, 콜백, 델리게이트 구조까지 자연스럽게 이어집니다.

이번 글에서는 그 출발점인 함수 포인터를 다룹니다.

게임이나 프로그램에서 자주 등장하는 상황을 생각해보겠습니다.

키 입력 발생
→ 특정 함수 실행

버튼 클릭
→ 등록된 함수 실행

충돌 발생
→ 이벤트 처리 함수 호출

이때 자연스럽게 이런 질문이 생깁니다.

❓ “어떤 함수를 실행할지 나중에 결정할 수는 없을까?”

예를 들어:

  • 점프 버튼 → Jump() 실행
  • 공격 버튼 → Attack() 실행

즉, 실행할 함수를 미리 정하지 않고, 나중에 바꾸고 싶다는 요구가 발생할 수 있습니다.

👉 이 문제를 해결하는 가장 기본적인 방법이 바로 함수 포인터(Function Pointer)입니다.


🎯 함수 포인터란 무엇인가?

함수 포인터는 말 그대로:

함수의 주소를 저장하는 변수입니다.

일반 변수는 값을 저장하지만,
👉 함수 포인터는 “실행할 함수”를 저장합니다.


🧠 기본 개념

다음과 같은 함수가 있다고 가정해봅시다.

#include <iostream>

void Jump()
{
    std::cout << "Jump!\n";
}

이 함수의 주소를 저장하려면 다음과 같이 작성합니다.

void (*funcPtr)() = &Jump;

구조를 보면:

void (*funcPtr)()
  • void → 반환 타입
  • () → 매개변수
  • (*funcPtr) → 함수 포인터

🎮 함수 포인터로 함수 실행하기

funcPtr();

또는

(*funcPtr)();

👉 두 방식 모두 동일하게 동작합니다.


🔥 핵심 아이디어

함수 = 메모리 주소를 가진다
↓
그 주소를 변수에 저장할 수 있다
↓
나중에 실행할 수 있다

👉 이게 함수 포인터의 본질입니다.


🎯 콜백 구조 만들기

함수 포인터의 진짜 의미는 “콜백 구조”에서 드러납니다.

✔ 콜백 함수 예제

void OnJump()
{
    std::cout << "Jump!\n";
}

void OnFire()
{
    std::cout << "Fire!\n";
}
void Execute(void (*callback)())
{
    callback();
}

사용:

Execute(OnJump);
Execute(OnFire);

👉 Execute 함수는 어떤 동작이 실행되는지 모릅니다.

즉, Execute를 실행하기 전에 “로직”을 전달하고 필요할 때 원하는 로직을 실행하도록 만들 수 있습니다.


🎯 이게 왜 중요한가?

이 구조의 핵심은 다음입니다.

함수를 전달한다
↓
나중에 실행한다
↓
동작을 외부에서 결정한다

👉 이게 바로 이벤트 시스템의 시작입니다.


🎮 입력 시스템 예제

조금 더 현실적인 예제로 보면:

void Jump()
{
    std::cout << "Jump!\n";
}

void Fire()
{
    std::cout << "Fire!\n";
}
void (*inputHandler)();

inputHandler = Jump;
inputHandler();

inputHandler = Fire;
inputHandler();

👉 출력:

Jump!
Fire!

👉 이 코드는 단순한 문법이 아니라:

“어떤 동작을 할지 런타임에 바꿀 수 있다”는 의미가 됩니다.


🧩 이벤트 흐름으로 보면

입력 발생
↓
콜백 함수 선택
↓
해당 함수 실행

👉 이 구조는 다음으로 확장됩니다:

  • UI 이벤트 시스템
  • 충돌 이벤트
  • 게임 로직 분리
  • 엔진 입력 시스템

⚠️ 함수 포인터의 한계

함수 포인터는 제한 사항이 있습니다.

❌ 상태를 저장할 수 없다

void PrintValue(int value)
{
    std::cout << value << "\n";
}
void (*func)(int) = PrintValue;
func(10);

👉 호출은 가능하지만 “value를 미리 저장”할 수는 없습니다.

❌ 객체와 함께 쓰기 어렵다

class Player
{
public:
    void Attack()
    {
        std::cout << "Player Attack\n";
    }
};
void (*func)() = &Player::Attack; // ❌ 오류

👉 멤버 함수는 일반 함수 포인터로 사용할 수 없습니다.

👉 멤버 함수와 일반 함수는 반환형과 파라미터 형태가 같더라도 서로 다른 타입으로 취급되기 때문입니다.


🔥 핵심 정리

함수 포인터는:

  • 함수 실행 가능 ✔
  • 런타임 선택 가능 ✔
  • 콜백 구조 가능 ✔

하지만:

  • 상태 저장 불가 ❌
  • 객체 결합 어려움 ❌

🎯 그래서 등장한 것

이 문제를 해결하기 위해 등장한 것이:

  • std::function
  • 람다(lambda)
  • 델리게이트 시스템

🎮 게임 엔진에서의 의미

게임에서는 이런 일이 계속 발생합니다.

키 입력 발생
→ 함수 실행

충돌 발생
→ 이벤트 처리

UI 클릭
→ 리스너 호출

👉 이 모든 구조의 시작이 함수 포인터입니다.


🔥 핵심 정리

  • 함수 포인터는 함수의 주소를 저장한다
  • 함수를 나중에 실행할 수 있다
  • 콜백 구조의 기반이 된다
  • 이벤트 시스템의 출발점이다
  • 하지만 상태와 객체를 다루기에는 한계가 있다

🎯 다음 글 예고

다음 글에서는:

👉 std::function과 lambda로 콜백 확장하기

  • 상태를 가지는 함수
  • 객체와 함께 사용하는 구조
  • 함수 포인터의 한계 해결

🎮 마무리

함수 포인터는 단순한 문법이 아니라
👉 “함수를 값처럼 다루는 첫 번째 단계”입니다.

이 개념을 이해하면 이후의 델리게이트 구조까지 자연스럽게 이어집니다.


🚀 한 단계 더 나아가고 싶다면

콜백 구조와 이벤트 시스템은 게임 엔진의 핵심 구성 요소입니다.

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다