[C] 포인터 잔당

장세민·2022년 10월 23일
0

📝 TIL

목록 보기
28/40
post-thumbnail

Before Starting

지금까지 다룬 주소는 변수나 배열과 같이 데이터의 주소였다.
그러나 함수에도 주소가 있고, 포인터를 통해 함수의 기능을 사용할 수 있다.

프로그램 만들 때는 호출 함수를 알 수 없고
실행될 때 결정된다면 호출할 함수의 주소를 받기 위해 함수 포인터가 필요하다.

딥하게 배워보자.

📌 함수 포인터

📖 개념

함수 포인터를 사용하기 위해 가장 중요한 것은 함수명의 의미를 파악하는 것이다.

함수명은 함수 정의가 있는 메모리의 시작 위치이다.
즉, 함수명은 주소이므로 포인터에 저장하면 쉽게 호출할 수 있을 것이다.

함수 포인터를 사용하여 함수를 호출해보자.

  1. #include <stdio.h>
  2.  
  3. int sum(int, int);
  4.  
  5. int main(void)
  6. {
  7. int (*fp)(int, int);
  8. int res;
  9.  
  10. fp = sum;
  11. res = fp(10, 20);
  12. printf("result: %d\n", res);
  13.  
  14. return 0;
  15. }
  16.  
  17. int sum(int a, int b)
  18. {
  19. return (a + b);
  20. }

프로그램을 컴파일하면 함수도 실행 파일의 한 부분을 차지하므로 메모리에 올려진다.
메모리에 있는 함수를 실행시키기 위해서는 위치를 알아야 하고,

컴파일 후 함수명이 메모리 주소로 바뀌므로 호출 시 함수명을 사용하는 것!

주소에 간접 참조 연산자를 사용하여 가리키는 대상을 사용하듯이,
함수명에도 간접 참조 연산자를 사용하여 가리키는 함수의 기능을 사용할 수 있다.

res = (*sum)(10, 20);

11행을 다음과 같이 함수명에 괄호와 함께 간접 참조 연산자를 사용하여 호출해도 결과는 같다.



함수의 형태

함수 형태는 매개변수의 개수와 자료형, 그리고 반환값의 자료형으로 정의한다.

함수의 주소 sum을 저장할 함수 포인터는

int (*fp)(int, int);

다음과 같이 선언할 수 있고,

*fp: 포인터
int(int, int): 가리키는 함수

를 의미한다.

괄호가 없으면 주소를 반환하는 함수의 선언이 되므로 주의해야 한다.

함수 포인터를 선언 후에 함수명을 저장하면 간접 참조 연산자 없이 바로 함수 호출이 가능하다.



📖 활용

함수의 형태만 같으면 기능과 상관없이 모든 함수에 사용할 수 있다.
따라서 형태가 같은 다양한 기능의 함수를 선택적으로 호출할 때 사용한다.

함수 포인터로 원하는 함수를 호출하는 프로그램을 실행 시켜보자.

  1. #include <stdio.h>
  2.  
  3. void func(int (*fp)(int, int));
  4. int sum(int a, int b);
  5. int mul(int a, int b);
  6. int max(int a, int b);
  7.  
  8. int main(void)
  9. {
  10. int sel;
  11.  
  12. printf("01 두 정수의 합\n");
  13. printf("02 두 정수의 곱\n");
  14. printf("03 두 정수 중에서 큰 값 계산\n");
  15. printf("원하는 연산을 선택하세요.");
  16. scanf("%d", &sel);
  17.  
  18. switch(sel)
  19. {
  20. case 1: func(sum); break;
  21. case 2: func(mul); break;
  22. case 3: func(max); break;
  23. }
  24.  
  25. return 0;
  26. }
  27.  
  28. void func(int (*fp)(int, int))
  29. {
  30. int a, b;
  31. int res;
  32.  
  33. printf("두 정수의 값을 입력하세요: ");
  34. scanf("%d%d", &a, &b);
  35. res = fp(a, b);
  36. printf("결괏값은: %d\n", res);
  37. }
  38.  
  39. int sum(int a, int b)
  40. {
  41. return (a + b);
  42. }
  43.  
  44. int mul(int a, int b)
  45. {
  46. return (a * b);
  47. }
  48.  
  49. int max(int a, int b)
  50. {
  51. if (a > b) return a;
  52. else return b;
  53. }

함수가 쪼금 길어졌지만, 어렵지 않다.

이 프로그램은 함수를 정의할 때 일부를 구현하지 않고 호출될 때 그 기능을 결정한다.

28행에서 구현한 func 함수는 다음 기능을 수행한다.

1. 2개의 정수를 입력한다.
2. 두 정수로 연산을 수행한다.
3. 연산 결과를 출력한다.

함수의 기능에서 2번의 연산 종류를 함수를 호출할 때 결정하고 싶다면
매개변수에 함수 포인터를 선언한다.
그리고 func 함수를 호출할 때는 원하는 기능의 함수를 인수로 주고 호출한다.

즉, func 함수 안에서 함수 포인터인 매개변수 fp가 함수명을 저장하여
fp를 통해 해당 기능을 가진 함수를 호출하는 것이다.


🔔 정리

함수 포인터에 함수명을 대입하면 함수처럼 호출 가능!



📖 void 포인터

주소는 가리키는 자료형이 일치하는 포인터에만 가능하다.

만약, 가리키는 자료형이 다른 주소를 저장하는 경우라면?

void 포인터를 사용한다.

사용해보자.

  1. #include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int a = 10;
  6. double b = 3.5;
  7. void *vp;
  8.  
  9. vp = &a;
  10. printf("a: %d\n", *(int *)vp);
  11. printf("a의 주소 + 1의 값: %d\n", (int *)vp + 1);
  12. vp = &b;
  13. printf("b: %.1lf\n", *(double *)vp);
  14.  
  15. return 0;
  16. }

void 포인터의 선언은 7행에서의 형식과 같다.

void는 가리키는 자료형을 결정하지 않겠다는 뜻이다.

따라서 어떤 주소든 저장할 수 있다는 장점이 있지만,
같은 이유로 간접 참조 연산이나 포인터 연산이 불가능하다.

간접 참조 연산을 하려면 몇 바이트를 어떤 자료형의 형태로 읽어야 하는지 알아야 하는데,
어떤 주소가 올지 알 수 없으므로 사용할 때는 원하는 형태로 변환하여 사용해야 한다.

HOW?

11행

printf("a: %d\n", *(int *)vp);

(int *): int *로 형 변환

13행

printf("b: %.1lf\n", *(double *)vp);

(double *): double *로 형 변환

과 같이 원래의 자료형에 맞게 형 변환 할 수 있다.

형 변환을 했으니 자료형이 정해지고, 그에따라 바이트도 정해졌으니
간접 참조 연산을 수행할 수 있게된다.

형 변환 연산자간접 참조 연산자는 모두 단항 연산자로서 우선순위가 같다.
연산 순서는 오른쪽부터 왼쪽으로 차례로 연산한다.


void 포인터가 형 변환을 하지 않는 경우

대입 연산을 할 때는 형 변환 없이 다른 포인터에 대입한다.
그래도 항상 명시적으로 형 변환하여 사용하는 것이 좋다.

int *pi = (int *)vp
profile
분석하는 남자 💻

0개의 댓글