그라운드 임베디드 EN 유튜브 구독

노트 / C 언어

포인터 심화와 함수 포인터

포인터 연산, 이중 포인터, 그리고 콜백과 상태 머신에 쓰는 함수 포인터를 정리했습니다.

GROUND EMBEDDED · 정리 노트C 언어 07포인터 심화와 함수 포인터 1typedef void (*handler_t)(uint8_t); 2handler_t on_rx = parse_command; 3on_rx(byte);// parse_command(byte) 실행

포인터 연산

p + 1은 주소에 1을 더하는 것이 아니라 가리키는 자료형 크기만큼 이동합니다. uint32_t *p라면 4바이트를 건너뜁니다. 배열 이름이 첫 요소의 주소처럼 동작하기 때문에 buf[i]와 *(buf + i)는 같습니다.

이중 포인터

포인터의 주소를 담는 포인터입니다. 함수 안에서 포인터 자체를 바꾸고 싶을 때(예: 버퍼를 할당해 돌려줄 때) 씁니다.

함수 포인터

on_rx함수 주소를 담은 변수호출 on_rx(byte)parse_command()저장된 주소로 점프
함수 포인터는 어느 함수를 부를지를 변수에 담아 둡니다.

함수도 주소를 가집니다. 그 주소를 변수에 담아 두면 실행할 함수를 나중에 고를 수 있습니다.

typedef void (*handler_t)(uint8_t);
handler_t on_rx = parse_command;
on_rx(byte);   // parse_command(byte) 실행

펌웨어에서 쓰는 곳은 이렇습니다.

  • 콜백: 드라이버가 "수신 완료" 같은 일을 위쪽 코드에 알릴 때
  • 상태 머신: 상태마다 처리 함수를 표에 넣어 두고 현재 상태의 함수를 호출
  • 벡터 테이블: 인터럽트 처리 함수 주소를 모아 둔 배열 자체가 함수 포인터 표입니다

함수도 주소가 있다

함수는 Flash의 어느 주소에 놓인 코드입니다. 함수 포인터는 그 주소를 담는 변수이고, 담긴 주소로 뛰어 실행할 수 있습니다.

void led_on(void)  { /* ... */ }
void led_off(void) { /* ... */ }

void (*action)(void) = led_on;     // 주소를 담고
action();                          // 그 주소의 함수를 실행
action = led_off;                  // 실행 중에 바꿀 수 있음

선언이 낯설면 typedef로 이름을 붙입니다.

typedef void (*handler_t)(uint8_t arg);

쓰임 1: 명령 테이블

받은 명령 0x02표에서 찾기0x01 → 상태 읽기0x02 → 속도 설정0x10 → 리셋
명령 번호로 표를 찾아 짝지어진 함수를 부릅니다. 명령을 늘릴 때는 표에 한 줄만 더합니다.

switch가 길어지는 대신, 명령 번호와 처리 함수를 표로 묶습니다.

typedef struct { uint8_t id; void (*fn)(const uint8_t *data, uint8_t len); } cmd_t;

static const cmd_t table[] = {
    { 0x01, cmd_read_status },
    { 0x02, cmd_set_speed   },
    { 0x10, cmd_reset       },
};

void dispatch(uint8_t id, const uint8_t *data, uint8_t len) {
    for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++)
        if (table[i].id == id) { table[i].fn(data, len); return; }
    send_nack(id);                 // 표에 없는 명령
}

명령을 추가할 때 표에 한 줄만 넣으면 됩니다. 표가 const이므로 Flash에 놓여 RAM도 쓰지 않습니다.

무료 전자책이 주제를 한 권으로 이어 읽기 (PDF 3권)→

쓰임 2: 콜백으로 계층 나누기

드라이버가 응용 코드를 직접 부르면 서로 얽힙니다. 대신 "일이 끝나면 이 함수를 불러 달라"고 등록합니다.

static void (*rx_cb)(uint8_t byte);
void uart_set_rx_callback(void (*cb)(uint8_t)) { rx_cb = cb; }

void USART1_IRQHandler(void) {
    uint8_t b = (uint8_t)USART1->RDR;
    if (rx_cb) rx_cb(b);           // NULL 확인 후 호출
}

UART 드라이버는 누가 데이터를 쓰는지 모릅니다. 같은 드라이버를 다른 프로젝트에 그대로 가져갈 수 있습니다.

쓰임 3: 상태 머신

state_idle시작 버튼?state_running고장?state_error
지금 상태가 곧 실행할 함수입니다. 조건이 맞으면 다음 상태의 함수로 바꿉니다.
typedef void (*state_fn)(void);
static state_fn state = state_idle;

void state_idle(void)    { if (start_pressed()) state = state_running; }
void state_running(void) { if (fault())         state = state_error;   }

while (1) { state(); }

지금 상태가 곧 실행할 함수입니다. 상태가 많아져도 메인 루프는 한 줄입니다.

벡터 테이블도 함수 포인터 배열이다

MCU의 인터럽트 벡터 테이블은 핸들러 주소를 순서대로 늘어놓은 함수 포인터 배열입니다. 인터럽트가 걸리면 CPU가 번호에 해당하는 칸의 주소를 읽어 그곳으로 뜁니다.

현업에서 자주 터지는 문제

  • NULL 호출: 등록하지 않은 콜백을 부르면 주소 0으로 뛰어 하드폴트가 납니다. 부르기 전에 확인합니다.
  • 형이 다른 함수 대입: 인자 개수나 종류가 다른 함수를 캐스팅해 넣으면 컴파일은 되지만 스택이 어긋납니다.
  • ISR에서 부르는 콜백이 길다: 콜백은 인터럽트 문맥에서 실행됩니다. 짧게 끝내도록 약속합니다.
  • 표의 순서와 번호가 어긋남: 인덱스로 바로 찾는 표는 번호와 위치가 맞아야 합니다. 범위 검사를 먼저 합니다.
영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSEC 문법을 실제 펌웨어 코드로 써 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →