임베디드 면접 용어 사전
C 언어

함수 포인터

함수의 주소를 담는 포인터. 콜백·상태 머신·벡터 테이블 구현의 기본 도구다.

함수의 시작 주소를 담는 포인터. 어떤 함수를 부를지 런타임에 결정할 수 있게 한다.

문법 — 읽는 법

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 버전이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — C 언어 핵심 — 포인터·메모리클래스·const/volatile·비트·struct 패딩