함수의 시작 주소를 담는 포인터. 어떤 함수를 부를지 런타임에 결정할 수 있게 한다.
문법 — 읽는 법
void (*fp)(int); // int를 받고 void를 반환하는 함수의 포인터
int (*cmp)(const void*, const void*);
fp = my_handler; // & 생략 가능 (함수 이름이 곧 주소)
fp(10); // (*fp)(10) 과 동일
괄호가 없으면 뜻이 달라진다.
void (*fp)(int); // 함수 포인터
void *fp(int); // void* 를 반환하는 함수 선언!
typedef로 가독성을 확보하는 것이 관례다.
typedef void (*Handler)(void);
Handler table[16];
임베디드에서 어디에 쓰이나
① 벡터 테이블 — 가장 근본적인 사용처
__attribute__((section(".isr_vector")))
void (* const vector_table[])(void) = {
(void (*)(void))&_estack, // 0: 초기 스택 포인터
Reset_Handler, // 1: 리셋
NMI_Handler, // 2
HardFault_Handler, // 3
…
SysTick_Handler,
};
벡터 테이블 자체가 함수 포인터 배열이다. CPU가 인터럽트 번호로 이 배열을 인덱싱해 핸들러 주소를 읽어 점프한다.
② 콜백 등록
static void (*uart_rx_cb)(uint8_t);
void uart_set_callback(void (*cb)(uint8_t)) { uart_rx_cb = cb; }
void USART1_IRQHandler(void) {
uint8_t c = USART1->DR;
if (uart_rx_cb) uart_rx_cb(c); // ★ NULL 검사 필수
}
★ NULL 검사를 빠뜨리면 콜백 등록 전에 인터럽트가 들어왔을 때 주소 0으로 점프해 HardFault가 난다. 실제로 매우 흔한 버그다.
③ 상태 머신
typedef State (*StateFn)(Event);
StateFn current = state_idle;
current = current(event); // 다음 상태 함수를 반환받아 전이
switch 중첩보다 깔끔하고, 상태가 늘어도 코드가 선형으로만 늘어난다.
④ HAL 추상화 — C로 다형성 흉내내기
typedef struct {
int (*init)(void);
int (*write)(const uint8_t *, size_t);
void (*close)(void);
} Driver;
const Driver uart_drv = { uart_init, uart_write, uart_close };
const Driver spi_drv = { spi_init, spi_write, spi_close };
const Driver *dev = &uart_drv;
dev->write(buf, len); // 어느 드라이버인지 몰라도 된다
C++의 vtable이 바로 이것을 컴파일러가 자동으로 만들어 주는 것이다. 이 구조를 이해하면 vtable도 자연스럽게 이해된다.
주의점
① ROM에 두어야 RAM을 아낀다
const Driver uart_drv = { … }; // const → .rodata (플래시)
Driver uart_drv = { … }; // .data → RAM 소모 + 덮어쓰기 위험
const가 없으면 폭주한 코드가 함수 포인터를 덮어써
임의 코드 실행으로 이어질 수 있다. 안전성 측면에서도 중요하다.
② 오버헤드가 있다
- 직접 호출 — : 컴파일 시 주소가 정해짐. 인라인·최적화 가능
- 간접 호출 — : 런타임에 주소를 읽어 점프. 인라인 불가
- 파이프라인 분기 예측 실패 가능
인터럽트 지연이 중요한 코드나 아주 짧은 함수에는 부담이 될 수 있다.
③ 멤버 함수는 그대로 못 넘긴다 (C++)
class Sensor { void on_data(uint8_t); };
uart_set_callback(&Sensor::on_data); // ✗ this 포인터가 없다
static 멤버 함수를 쓰거나, void* 컨텍스트를 함께 받는 형태로 설계한다.
typedef void (*Callback)(void *ctx, uint8_t data); // 실무 표준 형태
표준 라이브러리의 예
qsort(arr, n, sizeof(int), compare); // 비교 방법을 함수 포인터로 주입
qsort는 무엇을 정렬하는지 모르지만, 비교 함수를 받아 동작한다.
전략 패턴의 C 버전이다.