정신과 시간의 방
작성일
2023. 2. 14. 20:06
작성자
risehyun

01. 함수 포인터란

 - 변수란 값을 저장하는 메모리 공간의 이름입니다.

 - 포인터란 주소를 저장하는 메모리 공간의 이름입니다.

 - 아래는 정수형 변수와 포인터 변수의 예제입니다.

#include <iostream>
using namespace std;

int main()
{
    int n = 10;
    int *pn = &n;
    
    return 0;
}

 

 - 함수 포인터란 함수의 시작 주소를 저장하는 변수입니다.

 - 본래 함수의 이름 == 함수가 시작되는 주소이므로, 함수 포인터는 이 함수의 주소를 저장하는 포인터가 되는 것입니다.

 - 함수 포인터는 함수 시그니처, 즉 함수의 반환 타입과 매개변수 리스트와 같에 선언됩니다.

 - 예를 들어서 int 시그니처를 가지는 int func(int a, int b)인 함수의 함수 포인터는 int(*pf)(int, int)와 같이 선언됩니다.

 

// 함수 포인터의 선언과 사용
#include <iostream>
using namespace std;

void Print(int n)
{
	cout << "정수: " << n >> endl;
}

int main()
{
	// void Print(int n)의 함수 포인터 선언
	void (*pf)(int);
	// 함수의 이름은 함수의 시작 주소
	pf = Print;
	
	Print(10);  // 1. 함수 호출 
	pf(10);     // 2. 포인터를 이용한 함수 호출, 첫 번째 방법 
	(*pf)(10);  // 3. 포인터를 이용한 함수 호출, 두 번째 방법 
	
	cout << endl;
	cout << Print << endl;
	cout << pf << endl;
	cout << *pf << endl;
	
	return 0;
}

 

 

02. 함수 포인터의 종류

 - C++에서 함수는 정적 함수와 멤버 함수로 나눌 수 있습니다.
 - 정적 함수에는 전역 함수, namespace 내의 전역 함수, static 멤버 함수가 해당됩니다.
 - 멤버 함수는 다시 객체와 주소로 각각 호출할 수 있습니다.
 - 즉, 정적 함수 호출 방법은 1가지, 멤버 함수 호출 방법은 2가지가 있으므로 함수 호출 방식은 아래와 같이 3가지로 정리됩니다.

1) 정적 함수 호출(정적 함수)
2) 객체로 멤버 함수 호출(멤버 함수)
3) 객체의 주소로 멤버 함수 호출(멤버 함수)

 

// C++에서의 함수 호출 방식 3가지 예제

#include <iostream>
using namespace std;

void Print()
{
	cout << "정적 함수 Print()" << endl;	
}

class Point
{
	public:
		void Print()
		{
			cout << "멤버 함수 Print()" << endl;
		}
};

int main()
{
	Point pt;
	Point *p = &pt;
	
	Print();       // 첫째, 정적 함수 호출 
	pt.Print();    // 둘째, 객체로 멤버 함수 호출 
	p->Print();    // 셋째, 주소로 멤버 함수 호출 
	
	return 0;
}

 

 

  • 정적 함수 호출
    - 정적 함수 포인터는 앞에서 배운 것처럼 함수 시그니처만 알면 쉽게 선언이 가능합니다.
    - 정적 함수인 전역 함수, namespace 내 전역 함수, static 멤버 함수는 모두 함수 호출 규약이 같아 함수 포인터도 같습니다.
    - 즉, 이들 함수는 this 포인터가 필요하지 않아 호출 방식이 일반적인 전역 함수와 동일합니다.
    - 따라서, 함수 포인터도 동일한 형식 (ex. void(*)(int))으로 선언해서 사용할 수 있습니다.
    - 반환 타입이 void이고 int형 인자를 갖는 정적 함수의 포인터는 다음과 같이 선언할 수 있습니다.
void (*pf)(int);

 

 

- 실제 사용에 대한 예시 코드는 다음과 같습니다.

void GlobalFunc(int x) { }
namespace A {
    void NSFunc(int x) { }
}
class My {
public:
    static void StaticMemberFunc(int x) { }
};

int main() {
    void (*pf)(int);

    pf = GlobalFunc;          // namespace 없는 전역 함수 호출 OK
    pf = A::NSFunc;           // namespace A의 전역 함수 호출 OK
    pf = My::StaticMemberFunc; // My 클래스의 정적 멤버 함수 호출 OK

    pf(42);                   // 위 3가지 경우에 대해 전부 같은 방식으로 호출 가능
}

 

 

  • 객체와 주소로 멤버 함수 호출
    - 멤버 함수 포인터는 함수 포인터 선언에 어떤 클래스의 멤버 함수를 가리킬 것인지 클래스 이름을 지정해야 합니다.
    - 예를 들어 시그니처가 void Point()::Print(int n)인 멤버 함수의 포인터는 void (Point::*pf)(int)처럼 선언합니다.
    - 함수 호출은 다음과 같이 멤버 함수 호출 방법에 따라 다릅니다.

* 객체로 멤버 함수 호출 시에는, .* 연산자를 이용합니다. 예를 들어서 (객체.*pf)(10))처럼 사용합니다.

* 주소로 멤버 함수 호출 시에는 ->* 연산자를 이용합니다. 예를 들면 (주소->*pf)(10))처럼 사용합니다.

 

또한, 함수 호출 시 연산자 우선순위로 인해 객체와 .*, ->* 연산자 사이에 () 연산자를 사용해야 합니다.

// Point 클래스의 멤버 함수 포인터

#include <iostream>
using namespace std;

class Point
{
	int x;
	int y;
	
	public:
		explicit Point(int _x = 0, int _y = 0) : x(_x), y(_y) { }
		void Print() const { cout << x << ',' << y << endl; }
		void PrintInt(int n) { cout << "테스트 정수 : " << n << endl; }
};

int main()
{
	Point pt(2, 3);
	Point *p = &pt;
	
	void (Point::*pf1)() const;  // 멤버 함수 포인터선언 
	pf1 = &Point::Print;
	
	void (Point::*pf2)(int);     // 멤버 함수 포인터 선언 
	pf2 = &Point::PrintInt;
	
	pt.Print();                  // 객체로 멤버 함수 포인터를 이용한 호출 
	pt.PrintInt(10);             // 객체로 멤버 함수 포인터를 이용한 호출 
	cout << endl;
	
	(pt.*pf1)();                 // 주소로 멤버 함수 포인터를 이용한 호출 
	(pt.*pf2)(10);               // 주소로 멤버 함수 포인터를 이용한 호출 
	cout << endl;
	
	return 0;
}

 

 

03. 클라이언트 코드와 서버 코드

 - 서버 코드란? 어떤 기능이나 서비스를 제공하는 코드 측으로, 일반적으로 서버는 하나

 - 클라이언트 코드란? 서버로부터 기능을 제공받는 코드 측, 일반적으로 서버 코드를 사용하는 클라이언트는 여러 개

 - 기능 제공이라고 해서 대단한 뭔가가 있는 건 아니고, 그냥 단순한 함수를 정의하기만 해도 그것을 서버 코드라고 할 수 있습니다.

 

// 서버 코드와 클라이언트 코드

#include <iostream>
using namespace std;

////// 서버 //////
void PrintHello()
{
	cout << "Hello!" << endl;
}


////// 클라이언트 ///////
int main()
{
    printHello();
	
	return 0;	
}

 

 - 일반적으로 클라이언트 코드 측에서 서버를 호출하고 기능을 사용하지만, 서버가 클라이언트를 호출해야 하는 경우도 있습니다.

 - 클라이언트가 서버를 호출하는 걸 콜(call)이라고 하고, 서버가 클라이언트를 호출하는 걸 콜백(callback)이라고 합니다.

 - 이 콜백 메커니즘을 활용해 알고리즘 정책을 클라이언트에서 유연히 바꿀 수 있게 서버를 더욱 추상화할 수 있습니다.

 - 예시로 GUI의 이벤트 기능은 콜백 메커니즘으로 구현되며 윈도의 모든 프로시저는 시스템이 호출하는 콜백 함수 입니다.

 

// 콜백 함수

#include <iostream>
using namespace std;

////// 서버 //////
void PrintHello()
{
	cout << "Hello!" << endl;
	Client();  // 서버에서 클라이언트 코드 호출 
}

void Client()
{
    cout << "난 client" << endl;	
}

////// 클라이언트 ///////
int main()
{
    printHello(); // 서버 코드 호출 
	
	return 0;	
}

 

 - 위 예제는 간단한 콜백 메커니즘을 보여주기 위한 예제로, 실제 서버 코드는 이렇게 구현할 수 없습니다.

 - 서버는 여러 클라이언트에 의해 호출되며 클라이언트의 존재를 알지 못합니다. 즉, 위의 예제처럼 서버가 미리 Client() 함수를 알고 호출하는 것은 불가합니다.

 - 따라서, 콜백 메커니즘을 구현하려면 클라이언트가 서버를 호출할 때 서버에 클라이언트의 정보를 제공해야 합니다. (내가 누구인지_ID등)

 - 서버에 클라이언트 정보를 제공하는 방법 중 대표적인 방법이 함수 포인터 매개변수를 이용해 콜백 함수의 주소를 전달하는 것입니다. (그 외에 함수 객체, 대리자, 전략 패턴을 사용하는 방법이 있습니다.)

 - 아래 예제는 함수 포인터를 이용한 콜백 메커니즘을 구현한 것입니다.

 

// 함수 포인터를 이용한 콜백 매커니즘 구현 

#include <iostream>
using namespace std;

////// 서버 //////
// 배열의 모든 원소에 반복적인 작업을 수행하게 추상화됨 (구체적인 작업은 없음)
void For_each(int *begin, int *end, void (*pf)(int))
{
	while(begin != end)
	{
		pf(*begin++); // 클라이언트 함수 호출 (콜백) 
	}
} 

////// 클라이언트 ///////
void Print1(int n)
{
	cout << n << ' ';
}

void Print2(int n)
{
	cout << n*n << " ";
}

void Print3(int n)
{
	cout << "정수: " << n << endl;
}

int main()
{
    int arr[5] = { 10, 20, 30, 40, 50 };
	
	for_each(arr, arr+5, Print1);
	cout << endl << endl;   
	
        for_each(arr, arr+5, Print2);
	cout << endl << endl;   
	
	for_each(arr, arr+5, Print3);
	cout << endl << endl;
	
	   
	return 0;	
}

 

'C,C++ > [서적] 뇌를 자극하는 C++ STL' 카테고리의 다른 글

Chapter06-01 시퀀스 컨테이너 : vector  (0) 2023.02.20
Chapter05. STL 소개  (0) 2023.02.16
Chapter04. 템플릿  (0) 2023.02.15
Chapter03. 함수 객체  (0) 2023.02.15
Chapter01. 연산자 오버로딩  (0) 2023.02.13