함수 포인터와 다중 포인터: 실행 흐름의 제어권

hyunahn·2025년 12월 4일

Function Pointers: 코드를 가리키는 손가락

1. The Core Concept (본질 정의)

"함수 포인터는 '실행할 코드의 주소'를 담는 변수입니다."

지금까지 본 int *ptr은 데이터(Data Segment, Stack, Heap)를 가리켰습니다.
반면, void (*func_ptr)()은 코드(Text Segment)를 가리킵니다.

하드웨어 입장에서 함수 호출(Call)은 "Program Counter(PC)를 특정 주소로 점프시키는 행위"일 뿐입니다. 함수 포인터는 그 점프할 목적지 주소를 변수에 담아두고, 런타임에 목적지를 갈아끼울 수 있게 해줍니다.


2. Under the Hood (하드웨어 투시)

메모리 상에서 함수 포인터가 어떻게 연결되는지 엑스레이로 찍어봅시다.

[ Memory Map ]

   Area      | Address  |      Content
-------------+----------+-----------------------
 Text Segment| 0x1000   | [ Function A Code ... ] <---+
 (Read-Only) | 0x2000   | [ Function B Code ... ]     |
             |          |                             |
-------------+----------+-----------------------      |
 Data Segment| 0x5000   | func_ptr = 0x1000   --------+
 (Read-Write)|          | (변수 안에 0x1000이 있음)
  1. 초기 상태: func_ptr 변수(0x5000)에는 0x1000이라는 값이 들어있습니다.
  2. 호출 (func_ptr()): CPU는 0x5000번지에서 값을 읽어옵니다 -> 0x1000. 그리고 PC(Program Counter)를 0x1000으로 강제 이동시킵니다. -> Function A 실행
  3. 변경 (func_ptr = Function_B): func_ptr의 값을 0x2000으로 바꿉니다.
  4. 호출 (func_ptr()): 이제 CPU는 0x2000으로 점프합니다. -> Function B 실행

즉, 똑같은 코드를 실행했는데, 전혀 다른 함수가 수행됩니다. 이것이 다형성(Polymorphism)의 기초입니다.


3. The "Why" & Real-world Analogy (이유와 비유)

Q: 그냥 함수 이름을 직접 부르면 되지, 왜 복잡하게 포인터를 쓰나요?
A: "설계자가 사용자를 미리 알 수 없기 때문입니다 (Callback)."

📱 만능 리모컨 비유

여러분이 TV 제조사(하드웨어/라이브러리 설계자)입니다. 리모컨에 '바로가기 버튼'을 하나 만들었습니다.
이 버튼을 눌렀을 때 넷플릭스가 켜질지, 유튜브가 켜질지 여러분은 알 수 없습니다. 그건 TV를 산 사람(사용자/응용 프로그래머)이 결정할 일이니까요.

  • 함수 포인터: 리모컨의 '빈 버튼'입니다.
  • 등록(Register): 사용자가 "이 버튼 누르면 유튜브 켜줘"라고 설정하는 과정입니다.
  • 콜백(Callback): 실제 버튼을 눌렀을 때, 설정된 기능(유튜브)이 실행되는 것입니다.

하드웨어 인터럽트도 똑같습니다. CPU 설계자는 "인터럽트가 오면 0x0000번지를 실행해라"라고만 설계합니다. 그 0x0000번지에 무엇을 넣어둘지(어떤 함수로 연결할지)는 OS 개발자가 함수 포인터 배열(Interrupt Vector Table)을 채워 넣음으로써 결정됩니다.


4. Code & Best Practice

1) 콜백 함수 (Callback Mechanism)

이벤트 기반 프로그래밍의 핵심입니다.

#include <stdio.h>

// 1. 함수 포인터 타입 정의 (가독성을 위해 typedef 필수!)
// "int를 하나 받고 void를 리턴하는 함수"의 포인터 타입 이름이 Handler
typedef void (*Handler)(int);

void alarm_sound(int code) {
    printf("[HardWare] Beep! Error Code: %d\n", code);
}

void led_blink(int code) {
    printf("[HardWare] LED Blink! Error Code: %d\n", code);
}

// 라이브러리 함수 (사용자가 뭘 등록할지 모름)
void error_detector(int error_code, Handler callback) {
    // 에러 감지 로직...
    if (error_code > 0) {
        callback(error_code); // 등록된 함수를 "역으로 호출(Call-back)"
    }
}

int main() {
    // 상황에 따라 동작을 갈아끼움
    error_detector(404, alarm_sound); 
    error_detector(500, led_blink);
    return 0;
}

2) 구조체와 함수 포인터: C언어로 객체지향 흉내내기 (HAL) ⭐

리눅스 디바이스 드라이버나 임베디드 HAL(Hardware Abstraction Layer)의 예시 입니다.

/* LCD 드라이버 인터페이스 정의 */
struct LcdDriver {
    void (*init)(void);
    void (*write_pixel)(int x, int y, int color);
    void (*clear)(void);
};

/* 삼성 LCD용 코드 */
void samsung_init() { /* ... */ }
void samsung_write(int x, int y, int c) { /* ... */ }
// ...

/* LG LCD용 코드 */
void lg_init() { /* ... */ }
void lg_write(int x, int y, int c) { /* ... */ }
// ...

int main() {
    // 하드웨어가 바뀌어도 main 로직은 수정할 필요가 없음 (드라이버만 교체)
    struct LcdDriver driver;
    
    // 설정 파일에서 읽어서 동적으로 할당한다고 상상해보세요
    int model = 1; 
    
    if (model == 1) {
        driver.init = samsung_init;
        driver.write_pixel = samsung_write;
    } else {
        driver.init = lg_init;
        driver.write_pixel = lg_write;
    }

    // 실제 사용: 어느 회사 LCD인지 몰라도 됨
    driver.init(); 
    driver.write_pixel(10, 10, 0xFF);
    
    return 0;
}

3) 다중 포인터 (Pointer to Pointer, **pp)

"포인터 변수의 값을 함수 안에서 바꾸고 싶을 때" 씁니다.

void allocate_memory(char **ptr) {
    // *ptr은 main의 my_str을 가리킴
    // 내용을 바꾸면 main의 my_str이 가리키는 곳이 변함
    *ptr = malloc(100); 
}

int main() {
    char *my_str = NULL;
    allocate_memory(&my_str); // 포인터의 주소를 넘김 (Double Pointer)
    // 이제 my_str은 힙 메모리를 가리킴
    free(my_str);
    return 0;
}

0개의 댓글