"함수 포인터는 '실행할 코드의 주소'를 담는 변수입니다."
지금까지 본 int *ptr은 데이터(Data Segment, Stack, Heap)를 가리켰습니다.
반면, void (*func_ptr)()은 코드(Text Segment)를 가리킵니다.
하드웨어 입장에서 함수 호출(Call)은 "Program Counter(PC)를 특정 주소로 점프시키는 행위"일 뿐입니다. 함수 포인터는 그 점프할 목적지 주소를 변수에 담아두고, 런타임에 목적지를 갈아끼울 수 있게 해줍니다.
메모리 상에서 함수 포인터가 어떻게 연결되는지 엑스레이로 찍어봅시다.
[ 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이 있음)
func_ptr 변수(0x5000)에는 0x1000이라는 값이 들어있습니다.func_ptr()): CPU는 0x5000번지에서 값을 읽어옵니다 -> 0x1000. 그리고 PC(Program Counter)를 0x1000으로 강제 이동시킵니다. -> Function A 실행func_ptr = Function_B): func_ptr의 값을 0x2000으로 바꿉니다.func_ptr()): 이제 CPU는 0x2000으로 점프합니다. -> Function B 실행즉, 똑같은 코드를 실행했는데, 전혀 다른 함수가 수행됩니다. 이것이 다형성(Polymorphism)의 기초입니다.
Q: 그냥 함수 이름을 직접 부르면 되지, 왜 복잡하게 포인터를 쓰나요?
A: "설계자가 사용자를 미리 알 수 없기 때문입니다 (Callback)."
여러분이 TV 제조사(하드웨어/라이브러리 설계자)입니다. 리모컨에 '바로가기 버튼'을 하나 만들었습니다.
이 버튼을 눌렀을 때 넷플릭스가 켜질지, 유튜브가 켜질지 여러분은 알 수 없습니다. 그건 TV를 산 사람(사용자/응용 프로그래머)이 결정할 일이니까요.
하드웨어 인터럽트도 똑같습니다. CPU 설계자는 "인터럽트가 오면 0x0000번지를 실행해라"라고만 설계합니다. 그 0x0000번지에 무엇을 넣어둘지(어떤 함수로 연결할지)는 OS 개발자가 함수 포인터 배열(Interrupt Vector Table)을 채워 넣음으로써 결정됩니다.
이벤트 기반 프로그래밍의 핵심입니다.
#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;
}
리눅스 디바이스 드라이버나 임베디드 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;
}
**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;
}