[C언어] 함수 포인터란?

Oxong·2022년 2월 28일
0

iOS 개발 관련 모음

목록 보기
6/8

공부한 것을 정리하는 용도의 글이므로 100% 정확하지 않을 수 있습니다.
참고용으로만 봐주시고, 내용이 부족하다고 느끼신다면 다른 글도 보시는 것이 좋습니다.
+ 틀린 부분, 수정해야 할 부분은 언제든지 피드백 주세요. 😊
                                            by. Oxong




나는 비전공자라 C에 대해서 사실 거의... 백지와도 같은 상태였다.

swift로 개발을 하더라도 objective-c 코드를 아예 사용 안할 수는 없고, (안할 수도 있지만 내 경우에는 그랬다.)
objective-c 로 개발은 못 하더라도, 보고 이해해서 어느정도 사용할 줄은 알아야한다고 생각했기 때문에
코드 분석을 하면서 이해가 안되는 부분에 대해 한 파트씩 정리를 해나가기로 했다.



포인터란?


C언어는 CallByValue. 즉, 값을 복사해 그 값을 넘기는 방식을 채택한 언어이다.
쉽게 말해, 함수를 호출하면서 넘긴 변수 자체를 공유하지 않는다.

이 방식은 여러 호출점에서 하나의 공간을 공유하지 않기 때문에 값의 오염이 발생하지 않는다.
그러나 여러 호출점에서 같은 공간을 사용하기 위해 특별한 장치를 사용해야 한다.


여기서 말하는 '특별한 장치'가 바로 '포인터'이다.

포인터는 변수의 주소(메모리의 주소)를 보관하는 새로운 형태의 자료형이라고 할 수 있다.

포인터의 핵심은 참조연산자(*) 이다. 우리는 * 을 통해 포인터를 구분할 수 있다.


하지만 포인터는 데이터(변수)의 주소만 저장할 수 있는 것은 아니다.

포인터는 함수의 주소도 저장했다가 해당 주소의 함수를 호출하는 데 사용할 수도 있다.

이를 함수 포인터(Function Pointer)라고 한다.




참조 연산자(*)와 주소 연산자(&)

int *p = NULL;
int* p == int * p == int *p

포인터 변수임을 선언하고 싶을 때, 참조 연산자(*)를 붙여서 선언한다.
참조 연산자는 자료형과 변수명 사이 어디에 넣어도 상관 없다.

하지만 포인터 변수 p 에는 주소 값이 할당 되어야 하고, 문자나 숫자 등이 할당되어서는 안 된다.

// 나쁜 예
int *p = 17;	//error
char *p = 'a';	//error

// 좋은 예
int num = 17;
int *p = #	//num의 주소값이 입력
int *p = NULL;	//의도적으로 비워두든지 (가장 추천!)
int *p;		//선언만 해두든지

일반 변수에 주소 연산자(&)을 붙이면, 주소값이 튀어나온다고 했다.
포인터 변수에 참조 연산자(*)을 붙이면, 변수값이 튀어나온다.

P = 주소 ⟹ *P = 값  * : 값 찾아줘!

Num = 17 ⟹ &Num = 주소   & : 주소 찾아줘!



포인터 변수 선언

int num;
int* p1 = # // int 포인터 타입 변수인 p1에 int 타입 num 변수의 주소를 저장하겠다. (p1은 num을 가리키고 있다.)

printf("p1이 가지고 있는 값 : %p\n" p1);
printf("p1 == &num : %d\n", p1 == &num);


포인터의 자료형

// 포인터의 기본 구조

[type]* [varName];
[type] *[varName];
[type] * [varName];

// 애스터리스크(*) 기호는 타입과 변수명 사이 어디에 들어가던 상관없다.

포인터의 자료형은 [type]* 이다.
( * 문자는 해당 변수가 포인터임을 알려주는 문자로, 연산자가 아니다. )

포인터의 자료형을 작성하라 하면 아래 예시 코드처럼 int *로 작성해야 한다.

// 자료형 예시
int* p_i; // p_i가 저장하고 있는 주소에 가서 int 크기로 값을 읽거나 쓰겠다.
char* p_c;
short* p_s;

float* p_f;
double* p_d;



함수 포인터


C 언어에서 '함수'는 컴파일되면 기계어로 변경되고, 프로그램이 실행되면 코드 세그먼트라는 메모리 영역에 위치하게 된다.

함수의 형태가 변경되더라도 메모리에 저장되었기 때문에 주소를 가지게 되는 것이다.

위에서 언급한 것과 같이 포인터의 자료형은 포인터가 사용할 대상의 형식을 의미한다.
따라서 함수 포인터는 자신이 사용할 함수의 원형을 자료형으로 사용한다.

// 예시
void PrintValue(int a_value)
{
  printf("value = %d\n", a_value);
}

예를 들어 함수 포인터가 위의 예시에서 정의된 PrintValue 함수의 주소를 사용하고 싶다면
PrintValue 함수의 원형인 void (int)를 사용하여 void(*)(int)를 함수 포인터 자료형으로 사용해야 한다.

즉, 함수 포인터는 함수의 반환형과 매개변수의 타입이 일치해야만 그 함수의 주소값을 저장할 수 있다.

따라서 PrintValue 함수의 주소를 저장할 함수 포인터는 아래와 같이 선언해야 한다.

void (*p_func)(int); // 포인터 p_func의 자료형은 void(*)(int)

또한, p_func 변수에 PrintValue 함수의 주소를 대입할 때는 아래와 같이 '&' 를 함수의 이름 앞에 사용해야 한다.

p_func = &PrintValue;

(※ 사실 데이터 포인터는 &가 필수적이지만, 함수는 함수의 이름 자체로 함수의 주소를 호출하기 때문에 &를 사용하지 않아도 동작한다.)


<예시>

void PrintValue(int a_value)
{
  printf("value = %d\n", a_value);
}


int main() {
	
    // 함수 포인터 p_func 변수를 선언하고, PrintValue 함수의 주소 대입
	void (*p_func)(int) = PrintValue; // 함수 포인터는 & 없이도 함수 주소값 할당 가능

	// 🔹 함수 포인터를 사용하여 PrintValue() 호출 
    (*p_func)(5); // 원칙적인 표현
    p_func(7); // 허용되는 표현

    return 0;
}

위 코드의 결과 값
value = 5
value = 7





포인터를 사용하는 이유는?


일반 개발자들이 모두 포인터를 사용하여 개발하지는 않는다고 한다.

하지만 주로 함수자체의 매개변수, 반환값으로 사용할 때나 콜백(callback)함수를 사용할 때 포인터를 사용하여 개발한다고 한다.

여러사람이 협업할때에는 누가 어떤 함수를 필요로 할지 모르기 때문에 함수포인터를 정의하고 매개변수로 전달하게 되면 그 함수 내부에서 호출을 하면 되기 때문이다.


일단 처음 시작할 때 언급했듯이, 포인터는 간접 참조이다.
간접 참조는 직접 코드에 나와있지 않고 입력받는 값 등을 통해 간접적으로 접근한다고 해서 간접 참조라 불린다.

간접 참조 연산자를 이용하면 호출할 공간이 일시적으로 현재 사용할 변수의 값을 주소로 갖는 공간으로 변경된다.
즉, 다른 공간에 잠시 접근할 수 있게 된다.

(간접 참조 연산자는 절대 일반 변수에는 사용할 수 없다. 다시 말하면, 일반 변수는 다른 공간에 접근할 수 없다.)


즉, 함수 포인터는 콜백(callback) 매커니즘을 구현할 때 함수를 또 다른 함수의 인자(argument)로 넘겨주기 위해 주로 사용된다. (가장 많이 사용하는 이유)

라이브러리에서 함수의 기능이 어떻게 바뀔지 예상할 수 없을 때, 여러 개의 함수를 배열로 관리하고자 할 때 사용할 수도 있으며,
코드 간결화를 통해 가독성 및 빠른 처리속도를 기대할 수 있다.




여러개의 함수 포인터 배열 관리

아래의 코드는 두 수의 덧셈과 뺄셈을 수행하는 함수 포인터들의 배열을 나타낸 것이다.

#include <stdio.h> 
 
void add(int num1, int num2) {
    printf("%d\n", num1+num2);
}
 
void sub(int num1, int num2) {
    printf("%d\n", num1-num2);
}
 
int main() {
    void (*fp[2])(int, int);
    fp[0] = &add;
    fp[1] = &sub;
 
    (*fp[0])(3, 5);
    (*fp[1])(3, 5);
 
    return 0;
}


함수 포인터를 매개변수로 보내기

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>  

#define loop(v, lo, hi) for((v)=(lo); (v)<(hi); (v)++)

int sum(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int multi(int a, int b) { return a * b; }
int divv(int a, int b) { return a / b; }
int mod(int a, int b) { return a % b; }


// 🔹🔸 3. int (*(calculator3(int (*fp[])(int, int), int arr_size)))(int, int)
// *(calculator3(int (*fp[])(int, int), int arr_size))은 함수포인터로 볼 수 있다.(ex. fp)
// int (*fp)(int, int) 형태를 호출한 곳으로 보내준다.
// 예를 들어 random_Num이 2인 경우 fp[2]는 multi로 호출한 곳에 multi를 보내준다.

int (*(calculator3(int (*fp[])(int, int), int arr_size)))(int, int) {
                 // 🔹🔸 1. calculator3에 함수포인터배열을 사용하여 매개변수로 받는다.
	int low = 0;
	int high = arr_size - 1;
	int random_Num = rand() % high + low; // 난수 발생범위를 low에서 high 범위내에 출력되는 코드
	return fp[random_Num]; // 🔹🔸 2. 임의의 수(0~4 사이)를 넣어 fp[]를 반환한다.
}


int main(void) {
	int (*fpa[])(int, int) = { sum, sub, multi, divv, mod };
	
	int dx;

	srand(time(NULL));

	loop(dx, 0, 5)
	{
		Sleep(300);
		printf("random RUN!! : %d\n", calculator3(fpa, sizeof(fpa) / sizeof(fpa[0]))(10, 3));
        // 🔹🔸 4. 받아온 함수에 매개변수로 (10,3) 을 넣어 %d를 출력한다.
	}
}






Reference


포인터 part1. default

11. 포인터란 무엇인가 (참조 연산자* / 주소 연산자& / C언어)

함수 포인터에 대하여

[C] 함수 포인터란 무엇인가?

함수 포인터(Function Pointer)란?

[C 기초] 함수포인터

0개의 댓글