함수 포인터 1, 2

CJB_ny·2022년 8월 22일
0

C++ 정리

목록 보기
62/95
post-thumbnail

언리얼 엔진 사용하다보면은 STL내용을 배워야한다.

언리얼엔진은 C++ STL사용하지 않고 자체 라이브러리 사용한다.

STL을 공부하기 위해서는 사전 지식필요

  • 함수 포인터

  • 함수 객체

  • 템플릿

함수 포인터 #1

일단 포인터라는 것은

이렇게 사용이 가능하고 이러이러한 개념이다~

함수 라는 것 자체는

이런 형태이다.

typedef int DATA; 이렇게 타입을 만들어 주었는데

함수를 이런식으로 타입을 만든거 보여준 적없다.

일단 함수도 "타입"으로 만들어서 DATA*라는 포인터를 타고 갔을 대

"함수"가 있다라고 어떻게 표현 해줄 수 있을까?


함수는 인자를 안받을 수도있고 하나, 두개 이렇게 받을 수도있다.

이것에 따라서 달라지는데

즉, 함수의 모양이 중요하다.

이것은 함수의 이름인데 "타입"을 다룰 것이라

이 함수 자체가 함수의 시그니처 라고 볼 수 있다.

그래서 아까 typedef int DATA; 처럼

이렇게 해줄 수 있을 거같은데

함수의 경우 문법이 조금 다르다. 이렇게 끝에 붙여주는 것이 아니라

함수 타입

여기 사이에 붙여 주어야한다.

그냥 이거는 C++문법이라 어쩔 수 없고

해석을 하면 함수를 typedef해줄 것인데

함수의 타입의 이름을 FUNC_TYPE이라 부를 것이고

함수의 타입은 int (int a, int b) 인자를 두개를 받고 int를 반환 하는 타입이다.

모던 C++

에서는 'using'을 사용해서

using FUNC_TYPE = int(int a, int b)이렇게 정의 가능.

함수 포인터

일단 포인터라는 것을 알 수 있다.

변수 이름은 fn이다. 이 포인터를 타고가면 무엇이 있냐라고하면은

이제 '함수'가 있는 것이다.

그 '함수'가 시그니처가 인자는 int, int 두개를 받고 반환역시 int로 하는 시그니처가 있을 것이다. 라고 하는 것이다.

(int* ptr = &a의 경우 ptr타고 가면 정수형 데이터가 있을 것이다. 라고한 것처럼)

포인터 함수 호출 의미

이렇게 디버그 하고 디스어셈블리 보면은

call을 할 대 저 밑줄 친 주소로 jmp하게 된다.

이렇게 Add의 실제 구현부가 있는곳으로 타고 갈 수 있다.

결국 중요한 것은

Add라는 것 자체가 '주소'를 들고있다는 것이다.

함수의 이름은 함수의 시작 주소를 가지고있다. (배열과 유사)
(함수의 이름도 함수가 시작하는 그 위치를 가지고있다)

그렇게 때문에 함수도 역시

typedef int(FUNC_TYPE)(int, int); 

FUNC_TYPE* fn;

fn = Add();

// #########################

typedef int DATA;

int a = 10;

int* ptr = &a;

// 둘이 똑같지 않노?

시그니처가 같은 함수를 fn이라는 함수 포인터에다가 담아 줄 수 있다.

그래서 이제는 Add를 호출 안하고

이렇게 호출이 가능함.

이렇게 '간접적'으로 호출이 가능함.

특이사항?

함수 포인터는 *(접근 연산자) 붙여도 함수 주소를 나타낸다.

위, 아래 두개의 의미가 완전 같다.

왜 사용을 하나?

시그니처가 유사한 함수를 만들 경우

이렇게 있을 경우

함수 포인터가 없다고 가정할경우

42번째줄 처럼 Sub함수 호출할려면 싹다 바꿔주어야 하지만

그게 아니라 함수 포인터가 있을 경우

fn = Add가 아니라 fn = Sub로만 바꾸어 주면 된다.

(나머지 코드 안 붙여 주어도된다)

실 사용 예

이런 클래스 있다고 가정 ㄱㄱ. 또한

아이템을 찾아주는 함수도 있다고 가정을 하도록 하자.

지금 // TODO 에 들어갈 부분이 반복 될 것인데

이런 반복되는 부분들을 계속 복붙하면 '유지보수'에 안좋다.

그래서

'동작' 자체를 인자로 넣어주고

'동작'을 만족을 하면은 item을 뱉어주는 식으로 만들어 놓으면 아름답게 동작할 거같다.

동작을 인자로 ❓

레어 아이템인지 아닌지를 bool로 뱉어주는 이런 모양의 함수가 있다고 가정을 하자.

그러면 이제 FindItem이라는 함수의 인자로

bool ()(Item item)
이런 모양(시그니처)으로 함수를 받을 것인데
그것은 포인터 이고 -> bool (
)(Item item)
그 함수 포인터의 이름을 bool (
selsector)(Item* item)
이라고할 것이다.

이게 원래는

bool ()(Item* item)  // 이런 시그니처를 가지는 함수를

FUNC라는 이름으로 재정의 한다.

=> typedef bool(ITEM_SELECTOR)(Item* item);

그러면 이제 bool을 반환하고 인자를 Item* item을 받는 함수의 시그니처를

ITEM_SELECTOR라는 이름으로 정의 해준 것이다.

그러면 세번째 인자로 bool (selector)(Item item)

부분을 이제는

ITEM_SELECTOR라는 타입에다가 포인터를 붙이고 이름을

selector라고 붙인 것을 이제는

ITEM_SELECTOR* selector라고 할 수 있겟다.

이것을 한번에 하면은

Item* FindItem(Item items[], int itemCount, bool ( *selector )(int, int))

이렇게 해줄 수 있는 것이다.

이렇게 조건문의 조건식으로 selector라는 포인터를 넘겨줄 수 있다.

사용을 할 때는 이렇게

함수의 인자에다가 함수를 넘겨주는 식으로 사용이 가능하다.

호출하는 부분 디버그 잡기

여기 딱 호출해서 FindItem 부분의 매개변수 주소값을 보면은

IsRareItem이라는 함수의 주소가 들어온 것을 확인이 가능하다.

다양한 '동작' 자체를 함수의 인자로 넘겨주고 싶을 경우

'함수 포인터' 사용이 가능하다.

다른 사용 예시

그러면 지금 ITEM_SELECTOR의 시그니처를 int를 하나 더 받게 만든다면

지금 사진처럼 수정을 해주어야 하고

그 시그니처에 맞게 함수들의 모양도 같이 수정을 해야하는 '단점' 도 존재를 하기는 한다.

나중에 람다나 함수객체 등등 사용하면 보완 가능하다.

오늘 결론

다른거 다 필요없고

포인터이기는 포인터인데

그 포인터를 타고 가니까 함수가 있네? 이것이다.

이런식으로 사용할 수 있고

또한

이런식으로 함수의 매개변수로 '동작'을 넣어 줄 수 있다는 것이다.

함수 포인터 #2

#1 에서 누락된 내용이랑.

멤버 함수와 관련된 멤버 함수 포인터 알아 보고 관리하는 방법 알아 볼 것이다.

잠깐 정리

int (*fn) (int, int); 라는 형태(며양 == 시그니처)의 함수 포인터를 만들 수 있고

이것을 타고 갔을 때 뭐가 있나요?

=> int (int, int) int두개를 받고 int를 반환하는 모양의 함수가 있습니다.

사용을 할 때는

fn(); 또는 (*fn)(); 접근연산자를 사용하여 호출 할 수도있다.

참고로 함수의 이름은 함수의 주소를 나타낸다고 햇었는데

fn = Test; 를 해주어도 가능하지만

'&'를 용해서 fn = &Test;를 해주어도 된다.

(완전 동일함)

'&'가 생략이 가능한 이유는 C언어와의 '호환성' 때문이고

또한 '주소'라는 것을 표현 하기 위해서

fn = &Test라고 표현 해주는 것을 추천한다.

이게 멤버함수와의 융통성도 있다.

그런데

int (*fn) (int, int); 

// 이런식으로 정의 해서 사용하는게 귀찮을 경우

typedef int(FUNC_TYPE)(int, int);

이렇게 int ()(int, int); 이런 시그니처를 

FUNC_TYPE으로 정의를 한다음

FUNC_TYPE* fn; 이렇게 만들어 줄 수 있다.

그런데 이 방법보다는 한방에 처리를 하는

int (*fn) (int, int);  이 방법을 많이 쓴다.

typedef 의 진실

이렇게 정의를 하면 NUMBER를 int처럼 사용이 가능함.

근데 이렇게 새롭게 정의하는 타입이 간단하면

이렇게 동작을 하는데

배열이나 함수 포인터가 오게되면은 이게 성립을 하지 않는다.

왼쪽 / 오른쪽 기준이 아니라 '선언 문법'에서 typedef를 앞에다 붙이쪽이다.

??

=>

전역으로 이렇게 변수. 포인터, 함수선언, 배열을 선언 해줄 수 있는데

typedef자체가

이렇게 앞에다가 붙여주면 끝나는 것이다.

그런데

typedef int Func();

int Func();는 의미가 다른데

typedef int Func()는 Func라는 '함수'가 등장할 것인데

인자로는 아무것도 안받고 반환형이 int인 것을 typedef로 정의 해준 것이고

int Func()는 함수를 그냥 선언 해준 것이다.

이게 가독성 측면서에서 '햇가리는 문제'와

함수는 그자체로 메모리를 가지지 않고

함수를 호출하였을 때 그곳에 코드가 있는것일 뿐이다.

그래서 이제는


함수 포인터 10~15분 잘 이해가 안감.


이녀석 자체를 포인터로 만들어서 사용하는게 조금더 유리하다.

typedef int FUNC(int, int); 로 만들어서 사용하지 않고

한번에 함수 포인터로 만들려면?

함수 포인터 정의 하는방법

이 방법을 많으 사용할 것이고 이 방법을 써라

이전꺼는 잊고

typedef int FUNC(int, int); 이제 이거 말고 함수 포인터라는 것을 명확히 하기 위해서

typedef int (*PFUNC)(int, int);

이버젼으로 함수의 포인터를 만드는 것을 추천을 한다.

그래서 아까는

FUNC* fn; => PFUNC fn; 이렇게 사용이 가능.

반쪽짜리

typedef int(*PFUNC)(int, int);

얘는 '반쪽짜리' 함수 포인터 이다.

이 문법으로 만든 함수 포인터는

전역함수 / 정적 함수 만을 담을 수 있다.
(호출 규약이 동일한 애들끼리만들 담을 수 있다)

전역함수랑 정적 함수가 아닌것은 뭐가 있을까?

  • 전역함수

이렇게 전역으로 정의한거고

  • 정적함수


    이게 '멤버 함수'이고

    HelloKnight같은게 '정적 함수'이다.

그래서 typedef int(*PFUNC)(int. int);와 같은 것은

정적함수 / 전역함수 만 담을 수 있는데

멤버 함수의 경우에는?

시그니처를 맞춰주고

이렇게 담을려고해도 안됨.

일반함수 호출

일반함수 (정적 / 전역 함수) 같은 경우에는

Test(1, 2)라는 함수를 호출할 경우는

스택 프레임에 1, 2를 인자로 넣어준 다음에 Test를 호출해가지고

EAX레지스터에다가 값을 반환하는 형식이라면은...

Knight::GetHP(1, 2)호출할 때랑은 완전히 다르다.

Knight::GetHP(1, 2)애만 덩그러니 호출 할 수는 없었다.

멤버함수이 이기 때문에 객체가 있어야한다.

이렇게.

그래서 둘의 차이가 뭐냐하면은 특정객체를 기반으로 호출하냐 아니냐 이다.

태생이 다르다.

디스 어셈블리 보면은 객체의 주소를 먼저 멀이주게된다.

그래서 특정 객체를 기반으로 호출 하냐 아니냐가 차이점이다.

멤버 함수 포인터 선언 방법

일반 함수 포인터와 유사한데

*MEMFUNC 인데 특정 클래스의 멤버 함수 포인터라는것을 알려주기 위해서

typedef int (Knight::*PMEMFUNC)int, int);

이렇게 정의해줄 수 있다.

그래서

typedef int(*PFUNC)(int, int);
typedef int(Knight::*PMEMFUNC)(int. int);

항상 이 두가지 버젼이 있다고 생각을 해야한다.

사용

PMEMFUNC의 이름을 그냥 mfm이라고 하고

여기다가 mfm = Knight::GetHp;

라고 해주도록 하자.

근데

이렇게 뜨면서 컴파일 안된다.

그래서 멤버함수의 경우

PMEMFUNC mfm;

mfm = &Knight::GetHp; // 이렇게 '&'붙여 주어야한다.

그래서 일반함수(전역 / 정적)의 경우이건

멤버함수이건 '&'를 함수이름앞에 붙여주도록 통일하는게 좋다.

그런데 또

mfm();이렇게 바로 사용할 수 없다. 

=> 특정 객체를 기반으로 호출하는 것이기 때문에(멤버함수)

그래서 '반드시' 특정 객체가 존재해야한다.

Knight k1; 

PMEMFUNC mfm;

mfm = &Knight::GetHP;

mfm 안에 있는 주소값을 사용해서 호출하겠다라는 의미로

k1.* 해주고

(k1.*mfm)(1, 2); // 이렇게 호출 해주면 된다.

근데

(k1.*mfm)(1, 2); 조금 복잡하더라도 이렇게 해주어야하는 ㅣ유가

k1.mfm(1, 2) 일경우 컴파일러 입장에서

k1이라는 객체에 mfm라는 멤버함수를 말하는 것인지,

k1이라는 객체에 mfm라는 멤버가 들어가있는 것인지 분간이 어렵다.

혹시

이런식으로 같은 이름의 멤버 변수가 있을 수 있기 때문이다.

그래서 멤버 함수의 포인터라는 것을 명시해주기 위해서

(k1.*mfm)(); 이렇게 해주어야한다.

응용

동적할당한 객체의 경우

이렇게 가능한데 햇갈리니까

(k2->*mfm)(1, 2); 이렇게 활용 ㄱㄱ.

거의 활용이 안되는 문법이기는 하지만

알아야하고 쓰일 때도 있다.

이런 다른 클래스 안에 같은 이름의 함수가 있다고 하더라도

이렇게 먼저 함수 포인터를 Knight의 GetHp라고 정의를 해주었기 때문에

이렇게 담을 수 없다.

사용

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글