함수 포인터란?

정의

  • 함수도 코드 영역에 올라가며 주소를 가집니다.
  • 이 주소를 저장하는 변수가 함수 포인터입니다.
  • 핵심은 "호출을 데이터처럼 다룰 수 있다"는 점입니다.

포인터 기본 상기

  • int* ptrptr주소값을 담는 변수.
  • *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단계)

  1. 함수 원형 그대로 적기: void Print(void);
  2. 함수 이름 제거 후 괄호·별표 추가: void (*)(void)
  3. 타입 이름 지정: using FuncType = void (*)(void);

옛날 vs 현대 방식

방식문법
typedeftypedef 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;        // & 생략 가능: func = Add;
int r1 = func(10, 20);       // 30
int r2 = (*func)(10, 20);    // 30 (동일 의미)

func = Minus;
int r3 = func(10, 20);       // -10

시그니처 일치(매우 중요)

  • 함수 포인터는 시그니처가 정확히 일치하는 함수만 가리킬 수 있다.
  • 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);   // 30
DoSomething(10, 20, Minus); // -10
  • 동작 자체를 바꿔치기 하면 같은 함수가 다른 결과를 낸다.

콜백 함수

  • 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번 유저 공격" 같은 요청이 함수 포인터만으로 불편한 이유는?

profile
李家네_공부방

0개의 댓글