함수 포인터란?
정의
- 함수도 코드 영역에 올라가며 주소를 가집니다.
- 이 주소를 저장하는 변수가 함수 포인터입니다.
- 핵심은 "호출을 데이터처럼 다룰 수 있다"는 점입니다.
포인터 기본 상기
int* ptr → ptr은 주소값을 담는 변수.
*ptr → 그 주소를 타고 가면 int가 있다.
- 함수도 마찬가지: 주소를 따라가면 함수 코드가 있다.
왜 중요한가?
- 지금 당장 호출하지 않고, 나중에 실행할 동작을 선택할 수 있습니다.
- 같은 함수 틀에서 동작만 갈아끼우는 전략(Strategy) 패턴 구현이 쉬워집니다.
- 콜백, 키 매핑, 필터 조건 전달 같은 패턴의 기반이 됩니다.
메모리 관점에서의 함수 포인터
┌─────────────────────────────────────────────────────────────────┐
│ 메모리 (코드 영역) │
├─────────────────────────────────────────────────────────────────┤
│ 주소 0x1000 │ Add() 함수 코드: mov, add, ret ... │
│ 주소 0x1050 │ Minus() 함수 코드: mov, sub, ret ... │
│ 주소 0x10A0 │ Print() 함수 코드: ... │
└─────────────────────────────────────────────────────────────────┘
▲
│ func = &Add (함수 포인터가 0x1000 저장)
┌─────────────────────────┴───────────────────────────────────────┐
│ 스택 (변수 영역) │
│ func: [0x1000] ← func(10, 20) 호출 시 0x1000으로 점프하여 실행 │
└─────────────────────────────────────────────────────────────────┘
기본 예제 코드
void Print() {
cout << "Hello World" << endl;
}
int Add(int a, int b) {
return a + b;
}
Print: void 반환, 인자 없음
Add: int 반환, int 두 개 인자
함수 포인터 문법
타입 만들기 (3단계)
- 함수 원형 그대로 적기:
void Print(void);
- 함수 이름 제거 후 괄호·별표 추가:
void (*)(void)
- 타입 이름 지정:
using FuncType = void (*)(void);
옛날 vs 현대 방식
| 방식 | 문법 |
|---|
| typedef | typedef void (*FuncType)(void); → 변수명이 중간에 끼어 들어감 |
| using (C++11) | using FuncType = void (*)(void); → 가독성 좋음 |
선언·대입·호출
using CalcFunc = int (*)(int, int);
int Add(int a, int b) { return a + b; }
int Minus(int a, int b) { return a - b; }
CalcFunc func = &Add;
int r1 = func(10, 20);
int r2 = (*func)(10, 20);
func = Minus;
int r3 = func(10, 20);
시그니처 일치(매우 중요)
- 함수 포인터는 시그니처가 정확히 일치하는 함수만 가리킬 수 있다.
int Add(int, int) → int (*)(int, int) 타입만 가능
void Print(void) → void (*)(void) 타입만 가능
- 서로 다른 시그니처면 저장 불가.
자주 헷갈리는 문법 포인트
- 함수명은 컨텍스트에서 자동으로 함수 포인터로 변환되므로
&는 생략 가능
- 오버로드 함수는 이름만으로 모호할 수 있어 명시적 캐스팅이 필요할 수 있음
- 안전하게 쓰려면
nullptr 초기화 + 호출 전 null 체크 습관 권장
활용 케이스
동작을 인자로 넘기기
using CalcFunc = int (*)(int, int);
int DoSomething(int a, int b, CalcFunc op) {
if (op == nullptr) return 0;
return op(a, b);
}
DoSomething(10, 20, Add);
DoSomething(10, 20, Minus);
- 동작 자체를 바꿔치기 하면 같은 함수가 다른 결과를 낸다.
콜백 함수
- UI 버튼 클릭 시 실행할 함수를 매핑해 둔다.
- 게임 엔진은 "어떤 함수가 실행될지" 예측할 수 없으므로, 함수 포인터로 전달받는다.
키보드 매핑
- Q키 → 휴스킬, 옵션에서 E키로 변경 가능
- OnKeyPressed 같은 함수 포인터 배열/맵으로 관리하면, 설정 변경이 쉬워진다.
인벤토리 검색
FindItemByRarity, FindItemByOwner 등 함수별로 검색하면 코드 중복이 많다.
- 조건을 함수로 넘기면 하나의
FindItem으로 통합 가능:
using ItemSelectorType = bool (*)(const Item* item);
Item* FindItem(Item items[], int itemCount, ItemSelectorType selector) {
for (int i = 0; i < itemCount; i++) {
if (selector(&items[i])) return &items[i];
}
return nullptr;
}
bool IsRare(const Item* item) { return item->_rarity == 1; }
Item* rareItem = FindItem(items, 10, IsRare);
함수 포인터의 한계
| 한계 | 설명 |
|---|
| 시그니처 불일치 | 다른 시그니처 함수는 사용 불가 |
| 데이터 바인딩 불가 | "20번 유저를 공격한다" 같은 행동+데이터를 함께 담을 수 없다 |
| 비정적 멤버 함수 직접 저장 불가 | 멤버 함수는 this가 필요해 일반 함수 포인터와 타입이 다름 |
| 표현력 제한 | 캡처/상태 저장이 필요한 고수준 콜백에는 불편 |
다음 파트와 연결
- 비정적 멤버 함수를 다루려면 멤버 함수 포인터(Part 3)가 필요합니다.
- 행동 + 데이터를 함께 묶으려면 함수 객체(Part 4)가 필요합니다.
체크 질문 (스스로 답해보기)
- 왜 함수 포인터는 "호출을 데이터처럼 다룬다"고 말할 수 있을까?
func = Add;와 func = &Add;가 모두 가능한 이유는?
- 함수 포인터에서 시그니처 일치가 왜 중요한가?
- "20번 유저 공격" 같은 요청이 함수 포인터만으로 불편한 이유는?