Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C++] 함수 포인터의 변형 (함수 포인터 배열 관련) 본문

Programming/C++

[Programming/C++] 함수 포인터의 변형 (함수 포인터 배열 관련)

scii 2023. 3. 2. 00:14

함수 포인터는 표기법이 난해하다는 문제를 가지고 있다.

const double* f1(const double arr[], int n);
const double* f2(const double [], int);
const double* f3(const double*, int);

// 함수 포인터 배열
const double* (*pa[3]) (const double*, int) = {f1, f2, f3};

pa는 리턴형이 const double*이고, 매개변수로 const double*와 int형을 받을 수 있는 함수 포인터 3개를 저장할 수 있는 함수 포인터 배열이다.

pa를 가리키는 포인터를 만드려면 어떻게 해야 할까? 명확하게, 선언은 pa를 선언하는 것과 유사하지만, *가 더 필요하다. 새로운 포인터 pd를 호출할 경우, 포인터를 가리키는 것이 필요하지 배열 이름이 필요한 것은 아니다. (*pd)[3]로 선언이 되어햐 하며, 괄호로 *와 pd를 묶어 준다.

*pd[3] 		// 3개의 포인터 배열
(*pd)[3] 	// 3개의 원소를 가지는 배열 포인터

다시 말해, pd는 포인터이고 세 개의 원소를 가지는 배열을 가리킨다. pa의 원래 선언에서 뒷부분과 같이 표현될 수 있는며 다음과 같다.

const double* (*(*pd)[3]) (const double *, int) = &pa;

pd가 배열을 가리키는 포인터이고, *pd가 배열이고, *(pd)[ i ] 가 배열의 원소일 때, 함수를 호출하기 위해서는 함수를 가리켜야 한다. 단순하게 함수를 호출하는 구문인 (*pd)[ i ](av, 3)과 *(*pd)[ i ](av, 3)은 함수를 가리키기 위해 리턴된 포인터이다. 다른 방법으로 함수를 호출하기 위해 (*(*pd)[ i ])(av, 3)을 사용하거나, double 값을 가리키기 위해 *(*(*pd)[ i ]) (av, 3) 을 사용할 수 있다.

pa와 &pa의 차이를 확실하게 알아야 한다. pa는 배열의 첫 번째 원소의 주소이다 (&pa[0]와 같다). 그러므로, 단일 포인터의 주소이다. 그러나, &pa는 전체 배열의 주소이다 (세 개 포인터 블록을 말함). 숫자상으로 보면 pa와 &pa는 같은 값을 가질 수도 있으나, 데이터 형이 다르다.
한 가지 큰 차이는 pa+1은 배열 내 다음 원소의 주소이고, &pa+1은 pa 배열 뒤로 12바이트 다음 블록의 주소라는 것이다 (각 주소를 4바이트로 가정). 
또 다른 차이는 pa는 첫 번째 원소의 값을 한 번에 얻을 수 있지만, &pa는 같은 값을 얻기 위해 두 단계를 거쳐야 한다.

**&pa == *pa == pa[0]

다음의 예제를 살펴보자.

#include <iostream>

using namespace std;


// 표현식은 다르지만 모두 동일한 함수이다.
const double* f1(const double arr[], int n);
const double* f2(const double[], int n);
const double* f3(const double*, int n);


int main() {
    double av[3] = { 123.123, 155.55, 2322.97 };

    // 함수를 가리킨다.
    const double* (*p1) (const double*, int) = f1;
    // C++11 자동 형 변환
    auto p2 = f2;
    // C++11 이전 버전일 경우 아래 구문으로 대체
    //const double* (*p2) (const double*, int) = f2;

    cout << "함수 포인터: " << endl;
    cout << "주소 값" << endl;
    cout << (*p1)(av, 3) << ": " << *(*p1)(av, 3) << endl;
    cout << p2(av, 3) << ": " << *p2(av, 3) << endl;

    // 포인터들의 배열 pa
    // auto는 리스트 초기화에 사용할 수 없다.
    const double* (*pa[3]) (const double*, int) = { f1, f2, f3 };
    // 그러나, 단일 값을 초기화할 때는 사용할 수 있다.
    // pb는 pa의 첫 번째 원소를 가리킨다.
    auto pb = pa;
    // C++11 이전 버전일 경우 아래 구문으로 대체
    //const double* (**pb) (const double*, int) = pa;

    cout << endl << "함수 포인터를 원소로 가지는 배열: " << endl;
    cout << "주소 값" << endl;

    for (int i = 0; i < 3; i++) {
        cout << pa[i](av, 3) << ": " << *pa[i](av, 3) << endl;
        cout << endl << "함수 포인터를 가리키는 포인터: " << endl;
        cout << "주소 값" << endl;
    }

    for (int i = 0; i < 3; i++) {
        cout << pb[i](av, 3) << ": " << *pb[i](av, 3) << endl;
    }

    // 함수 포인터를 원소로 가지는 배열을 가리키는 포인터
    cout << endl << "포인터를 원소로 가지는 배열을 가리키는 포인터: " << endl;
    cout << "주소 값" << endl;

    // pc를 선언하는 간단한 방법
    auto pc = &pa;
    // C++11 이전 버전일 경우 아래 구문으로 대체
    //const double* (*(*pc)[3]) (const double*, int) = &pa;

    cout << (*pc)[0](av, 3) << ": " << *(*pc)[0](av, 3) << endl;

    // pd를 선언하는 복잡한 방법
    const double* (*(*pd)[3]) (const double*, int) = &pa;
    // pdb에 리턴 값 저장
    const double* pdb = (*pd)[1](av, 3);

    cout << pdb << ": " << *pdb << endl;

    // 또 다른 방법
    cout << (*(*pd)[2](av, 3)) << ": " << *(*(*pd)[2])(av, 3) << endl;

    return 0;
}


const double* f1(const double* arr, int n) {
    return arr;
}

const double* f2(const double arr[], int n) {
    return arr + 1;
}

const double* f3(const double arr[], int n) {
    return arr + 2;
}


// 결과
함수 포인터:
주소 값
0000000BC4AFF5B8: 123.123
0000000BC4AFF5C0: 155.55

함수 포인터를 원소로 가지는 배열:
주소 값
0000000BC4AFF5B8: 123.123

함수 포인터를 가리키는 포인터:
주소 값
0000000BC4AFF5C0: 155.55

함수 포인터를 가리키는 포인터:
주소 값
0000000BC4AFF5C8: 2322.97

함수 포인터를 가리키는 포인터:
주소 값
0000000BC4AFF5B8: 123.123
0000000BC4AFF5C0: 155.55
0000000BC4AFF5C8: 2322.97

포인터를 원소로 가지는 배열을 가리키는 포인터:
주소 값
0000000BC4AFF5B8: 123.123
0000000BC4AFF5C0: 155.55
2322.97: 2322.97

auto의 진가

C++11의 목표 중의 하나가 C++을 보다 쉽게 사용하고, 프로그래머가 세부적인 것은 신경을 덜 쓰면서도 설계에 더 집중할 수 있도록 하는 것이다.

// C++11 자동 형 변환
auto pc = &pa;

// C++98, 직접 복잡하게 선언해야 한다.
const double *(*(pc)[3]) (const double*, int) = &pa;

자동 형 변환은 컴파일러의 임무에 대한 철학적 변화를 반영한 것이다. C++98에서 컴파일러는 프로그래머가 잘못을 하였을 때 알려 줘야 하는 지식에 중점을 두었다. C++11에서는 이것은 최소한으로 가져가면서, 프로그래머가 올바른 선언을 하는 데 도움을 줄 수 있는 지식에 중점을 두고 있다.

여기에 잠재적 결점이 있다. 자동 형 변환은 초기화하는 형과 변수의 형을 매치시키는 것을 보장한다. 그러나, 초기화 시에 잘못된 형을 제공할 가능성이 여전히 존재한다.

// *pa가 아니라 &pa를 사용해야 한다.
auto pc = *pa;

이 선언은 pc를 *pa의 형과 매치를 시키고 컴파일러는 에러를 발생시킨다.


typedef를 이용한 단순화

C++은 선언을 단순하게 하기 위해 auto 외에 다른 방법을 제공한다. typedef 키워드는 데이터 형에 가명을 붙일 수 있다.

// double에 real이라는 가명을 만든다.
typedef double real;

이 기법은 식별자로 가명을 선언하고 앞에 typedef 키워드를 삽입한다. 그래서 함수 포인터 형을 다른 이름으로 만들 수 있다.

// p_func는 형 이름이다.
typedef const double* (*p_func) (const double*, int);

// p1 함수 포인터는 f1을 가리키는 포인터이다.
p_func = p1 = f1;

정교하게 만들기 위해 아래와 같이 사용할 수 있다.

// pa는 3개의 함수 포인터를 저장할 수 있는 배열이다.
p_func pa[3] = {f1, f2, f3};

// pd는 3개의 함수 포인터를 저장하고 있는 배열을 가리킨다.
p_func (*pd)[3] = &pa;

typedef는 단지 입력한 가명을 저장하는 것뿐만 아니라, 코드를 작성하면서 오류를 줄여 주고 프로그램을 더 쉽게 이해할 수 있게 도와준다.

Comments