개인적으로 C언어의 꽃이라고 생각되는 함수 포인터 개념을 이번 포스팅에서 다뤄보려고 합니다.
1. 함수 포인터란?
일반적으로 포인터란 자기 자신 이외의 메모리 주소를 담는 변수라고 표현됩니다.
이와 비슷하게 함수 포인터는 함수의 주소를 담는 변수라고 표현됩니다.
함수 포인터 또한 결국 포인터이기에 변수뿐만 아니라 함수 인자로도 사용할 수 있죠
우선 생김새를 배워봅시다.
[함수 반환형*] (*변수명)(인자)
ex) void* (*print)(char*);
일반 변수처럼 똑같이 자료형이 있고 변수명이 있습니다.
일반적으로 인자 형태는 변수명 뒤에 표시합니다.
※ 인자 부분을 비워둘 수도 있습니다. (비-프로토타입)
이는 C언어에서만 적용되고 C++은 반드시 인자갯수, 타입을 명시해줘야합니다.
2. 함수 포인터의 장단점
장점
- 런타임에 호출할 함수를 동적으로 결정할 수 있습니다. 즉, 코드가 실행 중에 어떤 함수를 호출할지 유연하게 선택할 수 있습니다.
- 이벤트 기반, 비동기 프로그래밍에서 콜백 함수를 구현하는데 용이합니다.
- 코드의 모듈화와 재사용성을 높이는 데 기여합니다. 특정 작업을 수행하는 코드를 여러 함수로 분리하여 모듈화할 수 있습니다.
- 객체 지향 언어의 다형성을 흉내낼 수 있습니다.
- 중복된 함수 호출 로직을 줄여 코드 크기를 절감할 수 있습니다.
단점
- 함수 포인터 문법에 익숙하지 않은 사람, 프로젝트 참여자가 아닌 이상 코드가 복잡해질수록 가독성이 떨어집니다.
- 인자 부분을 비워둘 수 있기에 타입 안전성이 떨어지고, 심하면 segment fault 오류를 야기할 수 있습니다.
- 메모리를 직접 다루기에 보안 위험에 더 노출되게 됩니다.
- C++의 다형성을 따라하기엔 역부족이며 코드 규모가 커질수록 복잡해집니다.
3. 함수 포인터 활용
예제 1) 콜백 함수 구현
#include <stdio.h>
// 콜백 함수
void onComplete() {
printf("Operation completed!\n");
}
// 작업을 수행하고 콜백 함수를 호출하는 함수
void performTask(void (*callback)()) {
// 작업 수행
printf("Performing task...\n");
// 작업이 완료된 후 콜백 함수 호출
callback();
}
int main() {
performTask(onComplete); // onComplete를 콜백으로 전달
return 0;
}
예제 2) 사용자 입력에 따라 동적으로 함수 호출
#include <stdio.h>
#include <stdlib.h>
void Create() { printf("Create!\n"); }
void Read() { printf("Read!\n"); }
void Update() { printf("Update!\n"); }
void Delete() { printf("Delete!\n"); }
void Exit() { exit(0); }
int numOfCmd = 5;
char** cmdHandler;
void (*cmd[6])();
typedef enum {
CREATE,
READ,
UPDATE,
DELETE,
EXIT,
NONE
} CmdType;
void Init() {
cmdHandler = (char**)malloc(sizeof(char*) * numOfCmd);
cmdHandler[0] = _strdup("Create");
cmdHandler[1] = _strdup("Read");
cmdHandler[2] = _strdup("Update");
cmdHandler[3] = _strdup("Delete");
cmdHandler[4] = _strdup("Exit");
cmd[0] = &Create;
cmd[1] = &Read;
cmd[2] = &Update;
cmd[3] = &Delete;
cmd[4] = &Exit;
}
int main() {
Init();
while (1) {
char input[10];
scanf_s("%s", input, 10); // 사용자 입력
CmdType type = NONE;
for (int i = 0; i < numOfCmd; i++) {
if (strcmp(input, cmdHandler[i]) == 0) // 함수명 일치시 명령 타입 변경
type = (CmdType)i;
}
if (type == NONE)
printf("Error : Command Syntax\n"); // 일치하는 함수명이 아닌 경우 에러 처리
else
cmd[type](); // 일치하는 함수명 자동 호출
}
return 0;
}
예제3) 큐에 함수 포인터를 담아 호출 시점 분리(커맨드 패턴)
만약 멀티 스레드 환경이라면 push 부분과 execute 부분에 임계 구역 정해주면 됩니다.
#include <stdio.h>
#include <stdlib.h>
#define QUEUE_MAX_SIZE 1024
typedef struct {
void (*execute)(void*);
} CMD;
typedef struct {
CMD* cmd;
} Node;
typedef struct q {
int cur;
int last;
Node nodes[QUEUE_MAX_SIZE];
int (*execute)(struct q*);
void (*flush)(struct q*);
int (*push)(struct q*, CMD*);
} Queue;
int Push(struct q* queue, CMD* cmd) {
if (queue->last >= QUEUE_MAX_SIZE) // 더 이상 넣을 공간이 없을 때
return -1;
queue->nodes[queue->last++].cmd = cmd;
return 0;
}
int Execute(struct q* queue) {
if (queue->cur >= QUEUE_MAX_SIZE || queue->cur >= queue->last) // 큐가 비어있는 경우
return -1;
CMD* cmd = queue->nodes[queue->cur++].cmd;
cmd->execute(cmd);
free(cmd);
return 0;
}
void Flush(struct q* queue) {
while (queue->execute(queue) == 0); // 큐가 빌때까지 함수 실행 후 입출력 커서 초기화
queue->cur = 0;
queue->last = 0;
}
void QueueInit(struct q* queue) {
queue->cur = 0;
queue->last = 0;
queue->push = &Push;
queue->execute = &Execute;
queue->flush = &Flush;
}
// 임시 상태 enum
typedef enum {
IDLE,
JUMP,
WALK,
RUN,
ATTACK,
NONE
} StateType;
typedef struct {
CMD base; // 반드시 멤버 변수 0번째에 넣어주어야함
int a;
int b;
} CMD1;
typedef struct {
CMD base;
StateType state;
} CMD2;
void SumAndPrint(void* _cmd) {
CMD1* cmd = (CMD1*)_cmd;
int a = cmd->a;
int b = cmd->b;
printf("SumAndPrint : %d + %d = %d\n", a, b, a + b);
}
StateType type = NONE;
void ChangeState(void* _cmd) {
CMD2* cmd = (CMD2*)_cmd;
printf("ChangeState before : %d\n", type);
type = cmd->state;
printf("ChangeState After : %d\n", type);
}
int main() {
Queue queue;
QueueInit(&queue);
{
CMD1* cmd = (CMD1*)malloc(sizeof(CMD1));
cmd->base.execute = &SumAndPrint; // 구조체의 0번째 주소에 함수를 넣기에 자료형이 변환되어도 함수 위치는 변하지 않음
cmd->a = 5;
cmd->b = 35;
queue.push(&queue, cmd);
}
// 처리 중 .
{
CMD2* cmd = (CMD2*)malloc(sizeof(CMD2));
cmd->base.execute = &ChangeState;
cmd->state = RUN;
queue.push(&queue, cmd);
}
// 처리 중 ..
{
CMD1* cmd = (CMD1*)malloc(sizeof(CMD1));
cmd->base.execute = &SumAndPrint;
cmd->a = -10;
cmd->b = 2;
queue.push(&queue, cmd);
}
// 처리 중 ...
{
queue.execute(&queue);
}
// 처리 중 ...
{
queue.flush(&queue);
}
return 0;
}
참고 자료
https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Declaring-Function-Pointers.html
Declaring Function Pointers (GNU C Language Manual)
22.5.1 Declaring Function Pointers The declaration of a function pointer variable (or structure field) looks almost like a function declaration, except it has an additional ‘*’ just before the variable name. Proper nesting requires a pair of parenthese
www.gnu.org
'C' 카테고리의 다른 글
[C언어] 비트 연산자를 활용하여 계산기 만들기 (0) | 2024.08.04 |
---|---|
[C] strdup, strjoin, split 구현 (0) | 2023.12.05 |
[C] strcmp / strncmp / memcmp 사용법과 구현 및 strncmp와 memcmp의 차이 (0) | 2023.02.11 |
[C] strcat / strncat / strlcat 사용법과 구현 (0) | 2023.02.04 |
[C] strlen / strcpy / strncpy / strlcpy 사용법과 구현 (0) | 2023.02.02 |