실체를 가리키는 포인터 말고 클래스의 멤버를 가리키는 포인터가 필요한 상황이 있다. 멤버를 가리키는 포인터는 클래스 실체의 행위를 재단하는데 유용하게 사용할 수 있다. 포인터가 어떤 멤버를 가리키는가에 따라 객체마다 다른 행위를 보여줄 것이다.
멤버를 가리키는 포인터는 나름의 사용법이 있지만 비슷한 행위를 실현하기 위해 다형성이 더 자주 사용된다. process
라는 멤버가 있는 클래스를 생각해 보자. 이 멤버는 일련의 대안 행위 중에 하나를 수행한다. 실체 생성 시간에 행위를 선택하는 대신에 클래스는 (추상) 바탕 클래스의 인터페이스를 사용할 수 있다. 파생 클래스의 실체를 그의 생성자에게 건네고 그리하여 그의 행위를 구성할 수 있다. 이렇게 하면 유연하고 확장성 있는 환경을 손쉽게 구성할 수 있다. 그러나 클래스 데이터 멤버에 접근하는 것은 유연성이 떨어지며 `friend' 선언을 사용할 필요성이 있을 수 있다. 그런 경우 멤버를 가리키는 포인터가 실제로 더 좋을 수 있다. (약간 유연성은 떨어지지만) 클래스의 데이터 멤버에 직접 접근할 수 있고 쉽게 환경을 구성할 수 있기 때문이다.
그래서 클래스의 데이터 멤버에 쉽게 접근할 것인가 아니면 쉽게 환경을 구성할 것인가에 따라 선택하면 된다. 이 장은 멤버를 가리키는 포인터에 집중해서 이 포인터들이 무엇을 제공해야 하는지 조사해 본다.
class String { char const *(*d_sp)() const; public: char const *get() const; };이 클래스에 대하여
char const *(*d_sp)() const
가 String::get
멤버 함수를 가리키도록 만드는 것은 불가능하다. d_sp
에 get
멤버 함수의 주소를 줄 수 없기 때문이다.
그 한 가지 이유는 d_sp
변수는 전역 영역에 있는데 반해, get
멤버 함수는 String
클래스 안에 정의되어 있고 그리하여 클래스 영역에 있기 때문이다 (전역 함수를 가리키는 포인터이지 String
안에 있는 함수를 가리키는 포인터가 아니다). d_sp
변수가 String
클래스의 데이터 멤버라는 사실은 여기에 전혀 관련이 없다. d_sp
의 정의에 따르면 클래스 밖 어디엔가 존재하는 함수를 가리킨다.
결론적으로 클래스의 멤버를 (즉, 클래스의 데이터나 함수를가리키는 그러나 보통은 클래스의 함수를) 포인터로 정의하기 위해 포인터의 영역은 반드시 클래스 영역을 나타내야 한다. 그리하여 String::get
멤버를 가리키는 포인터는 다음과 같이 정의된다.
char const *(String::*d_sp)() const;그래서
*d_sp
포인터 데이터 멤버 앞에 String::
을 배치함으로써 String
클래스 문맥 안에서 포인터로 정의된다. 그의 정의에 따라 String
클래스 안의 함수를 가리키는 포인터이지만 인자를 기대하지 않으며 자신의 객체 데이터를 변경하지 않고 그리고 불변 문자를 포인터로 돌려준다.
char const * (String::*d_sp)() const
를 사용해 d_sp
가 다음과 같다는 사실을 나타냈다.
*d_sp
);
String
안에 있는 무엇인가를 가리킨다 (String::*d_sp
);
char const *
를 돌려주는 const
함수를 가리키는 포인터이다 (char const * (String::*d_sp)() const
).
그러므로 부합하는 함수의 원형은 다음과 같다.
char const *String::somefun() const;
String
클래스 안의 const
함수로서 매개변수가 없고 char const *
를 돌려준다.
멤버를 포인터로 정의할 때 함수를 가리키는 포인터를 생성하기 위한 표준 절차를 똑같이 적용할 수 있다.
char const * ( String::somefun ) () const
*
)를) 함수 이름 바로 앞에 배치한다.
char const * ( String:: * somefun ) () const
char const * (String::*d_sp)() const
다음 예제도 데이터 멤버를 가리키는 포인터를 보여준다. String
클래스에 string d_text
멤버가 있다고 간주하자. 이 멤버를 가리키는 포인터를 어떻게 생성할 것인가? 또다시 표준 절차를 따르면 된다.
std::string (String::d_text)
*
)를 변수 이름 바로 앞에 배치한다.
std::string (String::*d_text)
std::string (String::*tp)이 경우는 괄호가 없어도 된다.
string String::*tp
대안으로, 지켜야 할 아주 간단한 규칙은 다음과 같다.
char const * (*sp)() const;클래스 영역을 앞에 붙이면 멤버 함수를 가리키는 포인터가 된다.
char const * (String::*sp)() const;멤버를 가리키는 포인터를 목표 클래스 (
String
) 안에 반드시 정의해야 하는 것은 아니다. 멤버를 가리키는 포인터는 목표 클래스나 (그래서 데이터 멤버가 된다) 또다른 클래스에 또는 지역 변수나 전역 변수로 정의할 수 있다. 이 모든 상황에 멤버 변수를 가리키는 포인터에 포인터가 가리키는 멤버의 주소를 줄 수 있다. 요점은 목표 클래스의 실체가 없어도 멤버를 가리키는 포인터를 초기화하거나 할당할 수 있다는 것이다.
그런 포인터에 대하여 주소를 할당하거나 초기화하는 것은 그저 그 포인터가 어느 멤버를 가리키는지 나타낼 뿐이다. 이것을 함수가 호출되는 실체에 상대적인 일종의 상대 주소로 간주해도 좋다. 멤버를 가리키는 포인터를 초기화하거나 할당할 때 실체가 전혀 필요없다. 멤버를 가리키는 포인터를 할당하거나 초기화해도 허용한다. 그렇지만 (물론) 올바른 유형의 실체를 지정하지 않고서 그런 멤버를 호출할 수는 없다.
다음 예제에서 멤버를 가리키는 포인터의 초기화와 할당 방법을 보여준다 (예시를 위해 PointerDemo
클래스의 멤버는 모조리 public
으로 정의했다). 예제에서는 멤버의 주소를 결정하기 위해 &
연산자가 사용되었다. 이런 연산자와 더불어 클래스 영역이 요구된다. 멤버 구현 안에 사용되었음에도 불구하고 말이다.
#include <cstddef> class PointerDemo { public: size_t d_value; size_t get() const; }; inline size_t PointerDemo::get() const { return d_value; } int main() { // 초기화 size_t (PointerDemo::*getPtr)() const = &PointerDemo::get; size_t PointerDemo::*valuePtr = &PointerDemo::d_value; getPtr = &PointerDemo::get; // 할당 valuePtr = &PointerDemo::d_value; }
별로 특별한 것은 없다. 전역 포인터와의 차이는 이제 영역이 PointerDemo
클래스 안으로 제한된다는 것이다. 이 제한 때문에 주소에 사용되는 모든 포인터 정의와 모든 변수는 PointerDemo
클래스 영역이 주어져야 한다.
멤버를 가리키는 포인터는 또 가상(virtual
) 멤버 함수와 함께 사용할 수 있다. 가상 멤버를 가리킬 때 특별한 구문이 요구되지 않는다. 포인터 생성과 초기화 그리고 할당은 비-가상 멤버와 똑 같은 방식으로 수행된다.
*
가 사용된다. 포인터 선택자 (->
) 또는 객체 선택자 (.
)를 사용하면 실체를 가리키는 포인터로 적절한 멤버를 선택할 수 있다.
멤버를 가리키는 포인터를 실체와 함께 조합하여 사용하려면 .*
필드 선택자를 지정해야 한다. 실체를 가리키는 포인터를 통하여 멤버를 사용하려면 ->*
필드 선택자를 지정해야 한다. 이 두 연산자는 필드 선택자의 표기법(.
그리고->
)을 조합하여 역참조 객체에 있는 적절한 필드에 도달한다. 포인터가 가리키는 멤버 변수나 멤버 함수에 도달하기 위하여 역참조 연산을 사용한다.
이전 절의 예제를 이용하여 멤버 함수와 데이터 멤버를 포인터로 어떻게 사용하는지 살펴보자:
#include <iostream> class PointerDemo { public: size_t d_value; size_t get() const; }; inline size_t PointerDemo::get() const { return d_value; } using namespace std; int main() { // 초기화 size_t (PointerDemo::*getPtr)() const = &PointerDemo::get; size_t PointerDemo::*valuePtr = &PointerDemo::d_value; PointerDemo object; // (1) (본문 참조) PointerDemo *ptr = &object; object.*valuePtr = 12345; // (2) cout << object.*valuePtr << '\n' << object.d_value << '\n'; ptr->*valuePtr = 54321; // (3) cout << object.d_value << '\n' << (object.*getPtr)() << '\n' << // (4) (ptr->*getPtr)() << '\n'; } /* 출력: 12345 12345 54321 54321 54321 */
다음을 눈여겨보라:
PointerDemo
실체와 그 실체를 가리키는 포인터를 정의했다.
.*
연산자를 통하여) valuePtr
가 가리키는 멤버에 도달한다. 이 멤버는 값이 주어져 있다.
PointerDemo
객체를 가리키는 포인터를 사용한다. 그러므로 ->*
연산자를 사용한다.
.*
와 ->*
를 사용하여 이 번에는 멤버를 가리키는 포인터를 통하여 함수를 호출한다. 멤버 필드 선택 연산자를 가리키는 포인터보다 함수 인자 리스트가 우선 순위가 더 높기 때문에 후자는 괄호로 보호해야 한다.
Person
클래스를 다시 한 번 연구해 보자. Person
에는 이름과 주소 전화번호를 보유한 데이터 멤버가 정의되어 있다. Person
사원 데이터베이스를 구성하고 싶다고 해보자. 사원 데이터베이스에 질의할 수는 있지만, 사원의 형태에 따라 데이터베이스에 질의할 때 이름과 전화 번호 또는 저장된 모든 개인 정보를 볼 수 있다. address
같은 멤버 함수는 열람자가 개인의 주소를 보도록 허용되어 있지 않을 경우에 `<열람 불가>
' 메시지를 돌려주고 그렇지 않으면 실제 주소를 돌려 주어야 한다는 뜻이다.
사원 데이터베이스는 질의하고자 하는 사원의 직위를 반영한 인자를 지정해 연다. 직위는 예를 들어 BOARD
, SUPERVISOR
, SALESPERSON
, 또는 CLERK
와 같이 조직에서의 계급을 반영할 수 있다. 앞의 두 범주는 사원에 관하여 모든 정보를 볼 수 있고 SALESPERSON
은 사원의 전화 번호를 볼 수 있는 반면에 CLERK
는 사원인지 아닌지만 볼 수 있다.
이제 데이터베이스 클래스에 string personInfo(char const *name)
멤버를 생성한다. 이 클래스의 표준 구현은 다음과 같을 것이다.
string PersonData::personInfo(char const *name) { Person *p = lookup(name); // `name'이 존재하는지 확인한다. if (!p) return "not found"; switch (d_category) { case BOARD: case SUPERVISOR: return allInfo(p); case SALESPERSON: return noPhone(p); case CLERK: return nameOnly(p); } }크게 시간이 걸리는 것은 아니지만 그럼에도
switch
는 personInfo
가 호출될 때마다 평가되어야 한다. switch
를 사용하는 대신에 d_infoPtr
멤버를 PersonData
의 멤버 함수를 가리키는 포인터로 정의할 수 있다. string
문자열을 돌려주고 Person
을 가리키는 포인터를 그의 인자로 기대한다.
switch
를 평가하는 대신에 이 포인터를 사용하여 allInfo
나 noPhone
또는 nameOnly
를 가리킬 수 있다. 그리고 포인터가 가리키는 멤버 함수는 PersonData
객체가 생성될 쯤이면 알려진다. 그래서 그의 값은 (PersonData
객체가 생성될 때) 한 번만 결정하면 된다.
d_infoPtr
를 초기화했기 때문에 personInfo
멤버 함수는 이제 간단하게 다음과 같이 구현된다.
string PersonData::personInfo(char const *name) { Person *p = lookup(name); // `name'이 존재하는지 확인한다. return p ? (this->*d_infoPtr)(p) : "not found"; }
d_infoPtr
멤버는 다음과 같이 정의된다 (PersonData
클래스 안에 정의되고 다른 멤버는 모두 생략함):
class PersonData { string (PersonData::*d_infoPtr)(Person *p); };마지막으로, 생성자는
d_infoPtr
를 초기화한다. switch 구문을 사용하여 간단하게 구현할 수 있다.
PersonData::PersonData(PersonData::EmployeeCategory cat) : switch (cat) { case BOARD: case SUPERVISOR: d_infoPtr = &PersonData::allInfo; break; case SALESPERSON: d_infoPtr = &PersonData::noPhone; break; case CLERK: d_infoPtr = &PersonData::nameOnly; break; } }멤버 함수의 주소가 어떻게 결정되는지 눈여겨보라. 이미
PersonData
클래스의 멤버 함수 안에 있음에도 불구하고 PersonData
클래스의 영역을 지정해야 한다.
stable_sort
총칭 알고리즘의 문맥에서 데이터 멤버를 가리키는 포인터의 사용법은
19.1.60항에 있다.
String
클래스에 공개 정적 count
멤버 함수가 있어서 지금까지 만든 문자열 객체의 갯수를 돌려준다고 해 보자. 그러면 String
실체가 전혀 없어도 String::count
함수를 호출할 수 있다.
void fun() { cout << String::count() << '\n'; }공개 정적 멤버는 자유 함수처럼 호출이 가능하다 (그러나 8.2.1항 참고). 비밀 정적 멤버는 오직 클래스 문맥 안에서만 호출할 수 있다. 클래스 멤버나 친구 함수로만 호출이 가능하다.
정적 멤버는 연관된 실체가 없기 때문에 주소를 평범한 함수 포인터 변수에 저장할 수 있으므로 전역 수준에서 작동한다. 멤버를 가리키는 포인터는 정적 멤버의 주소를 저장하지 못한다. 예를 들어:
void fun() { size_t (*pf)() = String::count; // 정적 멤버 함수의 주소로 pf를 초기화 cout << (*pf)() << '\n'; // String::count() 멤버가 돌려준 값을 보여줌 }
#include <string> #include <iostream> class X { public: void fun(); std::string d_str; }; inline void X::fun() { std::cout << "hello\n"; } using namespace std; int main() { cout << "size of pointer to data-member: " << sizeof(&X::d_str) << "\n" "size of pointer to member function: " << sizeof(&X::fun) << "\n" "size of pointer to non-member data: " << sizeof(char *) << "\n" "size of pointer to free function: " << sizeof(&printf) << '\n'; } /* (32-비트 골격구조에서의) 출력: size of pointer to data-member: 4 size of pointer to member function: 8 size of pointer to non-member data: 4 size of pointer to free function: 4 */
32-비트 골격구조에서 멤버 함수를 가리키는 포인터는 8 바이트를 요구한다. 반면에 다른 종류의 포인터는 4 바이트를 요구한다 (Gnu g++ 컴파일러 사용).
포인터 크기가 명시적으로 사용되는 경우는 거의 없지만 크기 때문에 다음과 같은 서술문에서 혼란을 일으킬 가능성이 있다.
printf("%p", &X::fun);물론
printf
는 이렇게 C++에 종속적인 포인터의 값을 출력하기에 올바른 도구는 아니다. 공용체(union
)를 사용하여 8-바이트 크기의 포인터를 일련의 size_t char
값으로 재해석하면 이 포인터의 값을 스트림에 삽입할 수 있다.
#include <string> #include <iostream> #include <iomanip> class X { public: void fun(); std::string d_str; }; inline void X::fun() { std::cout << "hello\n"; } using namespace std; int main() { union { void (X::*f)(); unsigned char *cp; } u = { &X::fun }; cout.fill('0'); cout << hex; for (unsigned idx = sizeof(void (X::*)()); idx-- > 0; ) cout << setw(2) << static_cast<unsigned>(u.cp[idx]); cout << '\n'; } /* 출력: 401057bef87d894810ec8348e5894855 */
그러나 왜 평범한 포인터와 크기가 다른가? 이 질문에 대답하기 위하여 먼저 익숙한 std::fstream
을 살펴 보자. std::ifstream
과 std::ofstream
으로부터 파생되었으므로 fstream
에는 ifstream
과 ofstream
이 모두 들어있다. fstream
을 그림 21과 같이 조직해 보겠다.
fstream (a)
에서 첫번째 바탕 클래스는 std::istream
이고 두 번째 바탕 클래스는 std::ofstream
이다. 그러나 순서를 뒤바꾸는 것도 역시 문제가 없다. fstream (b)
에 보여주는 바와 같이 말이다. 먼저 std::ofstream
을 다음에 std::ifstream
을 두어도 된다. 그것이 바로 핵심이다.
fstream fstr{"myfile"}
객체가 있고 fstr.seekg(0)
를 실행하면 ifstream
의 seekg
함수를 호출하는 셈이다. 그러나 fstr.seekp(0)
를 수행하면 ofstream
의 seekp
함수를 호출하는 것이다. 이 함수들은 &seekg와 &seekp로 각자 주소가 따로 있다 그러나 (
fstr.seekp(0)
과 같이) 멤버 함수를 호출하면 실제로는 seekp(&fstr, 0)
를 수행하고 있는 것이다.
그러나 여기에서 문제는 &fstr
이 객체의 주소를 올바르게 나타내지 못한다는 것이다. seekp
는 ofstream
에 작동하고, 그 객체는 &fstr
에서 시작하지 않는다. (fstream (a)
이므로) &(fstr + sizeof(ifstream))
에서 시작한다.
그래서 컴파일러는 상속을 사용하는 클래스의 멤버 함수를 반드시 그 객체의 상대 위치로 교정해 호출해야 한다.
그렇지만 다음과 같이 정의하고
ostream &(fstream::*ptr)(ios::off_type step, ios::seekdir org) = &seekp;
(fstr->*)ptr(0)
를 수행하면 컴파일러는 어느 함수가 실제로 호출되는지 더 이상 알지 못한다. 컴파일러는 그저 그 함수의 주소를 받을 뿐이다. 컴파일러의 문제를 해결하기 위해 (ofstream 객체의 위치를 찾기 위하여) 이제 shift에 멤버 포인터 자체가 저장된다. 그 때문에 바로 함수 포인터를 사용할 때 데이터 필드가 더 필요하다.
다음은 구체적인 예제이다. 먼저 구조체 2개를 정의한다. 각자 멤버 함수가 있다 (지면을 아끼기 위해 모두 한 줄짜리 인라인으로 구현한다.):
struct A { int a; }; struct B { int b; void bfun() {} };다음 C를 정의한다. A (first)와 B (next)를 상속받는다 (
ifstream
과 ofstream
을 내장한 fstream
과 비슷하다.):
struct C: public A, public B {};다음,
main
에서 C 객체와 공용체를 하나 정의한다. 공용체의 실체마다 같은 멤버 함수의 주소 &B::bfun
을 받는다. 그러나 BPTR.ptr
은 그것을 구조체 B의 세계에 있는 멤버로 보는 반면에, CPTR.ptr
은 그것을 구조체 C의 세계에 있는 멤버로 간주한다.
공용체에 값들을 주면 value[] 배열을 사용하여 ptr 필드에 있는 값을 보여준다 (아래 참고):
int main() { union BPTR { void (B::*ptr)(); unsigned long value[2]; }; BPTR bp; bp.ptr = &B::bfun; cout << hex << bp.value[0] << ' ' << bp.value[1] << dec << '\n'; union CPTR { void (C::*ptr)(); unsigned long value[2]; }; CPTR cp; cp.ptr = &C::bfun; cout << hex << cp.value[0] << ' ' << cp.value[1] << dec << '\n'; }이 프로그램을 실행하면 다음과 같이 출력한다.
400b0c 0 400b0c 4(두 번째 줄에 있는 첫 번째) 주소 값은 여러분과 다를 수 있다. 이 함수의 주소는 같다는 것을 눈여겨보라. 그러나 C 세계에서 B 객체는 A 객체를 넘어 존재하고 A 객체는 4 바이트이므로 4를 `
this
' 포인터의 값에 더해 그 함수를 C 객체로부터 호출해야 한다. 그것이 바로 정확하게 포인터의 두 번째 필드에 있는 shift 값이 컴파일러에게 알려주는 것이다.