📚 델리게이트 시리즈
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로 콜백 확장하기
- 상태를 가지는 함수
- 객체와 함께 사용하는 구조
- 함수 포인터의 한계 해결
🎮 마무리
함수 포인터는 단순한 문법이 아니라
👉 “함수를 값처럼 다루는 첫 번째 단계”입니다.
이 개념을 이해하면 이후의 델리게이트 구조까지 자연스럽게 이어집니다.
🚀 한 단계 더 나아가고 싶다면
콜백 구조와 이벤트 시스템은 게임 엔진의 핵심 구성 요소입니다.
단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.