책정리/혼자 연구하는 C,C++ 2

33장 타입 정보

GONII 2015. 3. 9. 17:22

33-1 RTTI

가. 실시간 타입 정보

RTTI는 RunTime Type Information의 약자이며 실시간 타입 정보라는 뜻이다. 일반적으로 변수의 이름이나 구조체, 클래스의 타입은 컴파일러가 컴파일을 하는 동안에만 필요할 뿐이며 이진 파일로 번역되고 나면 이 정보들은 필요가 없다. 변수의 타입은 읽어들일 길이와 비트를 해석하는 정보로만 사용되며 기계어 수준에서는 길이와 비트해석 방법에 따라 생성되는 기계어 코드가 달라진다.

클래스도 마찬가지로 기계어로 바뀌면 구조체와 똑같되 다만 가상 함수가 있을 때 vtable을 가리키는 포인터를 하나 더 가진다는 정도만 다르다. 다만 첫 번째 인수가 this로 고정되어 있는 호출 규약을 사용하므로 이 함수를 호출 할 때는 항상 호출 객체의 포인터가 같이 전달되도록 컴파일 될 뿐이다.

기계(CPU)는 어차피 타입이라는 것을 인식하지 않으며 메모리에 있는 값을 지정한 길이만큼 읽고 쓰고 할 뿐이다. 그러나 상속된 클래스의 계층을 다루는 C++에서는 가끔 이런 타입에 대한 정보가 실행 중에 필요한 경우가 있다.

  • 예제 RTTI

#include <iostream>

   

class parent

{

public:

virtual void printMe() { printf("parent\n"); }

};

   

class child : public parent

{

private:

int num;

public :

child(int anum = 1234) : num(anum) {}

virtual void printMe() { printf("child\n"); }

void printNum() { printf("child = %d\n", num); }

};

   

void func( parent *p )

{

p->printMe();

((child*)p)->printNum();

}

   

void main()

{

parent p;

child c(5);

   

func(&c);

func(&p);

}

func(&c) 호출로 차일드의 번지를 전달할 때는 printMe나 printNum 두 호출 모두 성공적이다. printMe는 가상함수이므로 객체의 타입에 맞는 함수가 호출될 것이고 printNum은 비가상 함수지만 child의 멤버 함수인 것은 분명하므로 p를 child* 타입으로 캐스팅하면 호출할 수도 있고 동작도 한다. printNum에서 참조하는 this->num이 존재한다.

반면 func(&p)로 p의 번지를 전달할 때는 그렇지 않다. printMe는 vtable에서 실제 번지를 찾으므로 제대로 동작하지만 비가상 함수인 printNum 호출은 엉뚱하게 동작한다. 왜냐하면 실인수 p가 가리키는 객체는 num이라는 멤버를 가지고 있지 않은데 printNum이 읽는 num 멤버는 p객체에 존재하지 않는다.

printNum을 가상 함수로 바꾸어도 컴파일은 되지만 실행해보면 프로그램이 다운된다. 왜냐하면 객체 p가 가리키는 vtable(곧 parent 클래스의 vtable)에는 printNum이라는 함수의 번지가 없기 때문이다.

그런데 func함수에서는 p를 child*로 캐스팅했으므로 컴파일러는 이 번지에 printNum이 있을 것으로 판단하고 아무 메러를 내지 않는다.

객체가 child타입인 경우만 printNum을 호출하도록 하면 다음과 같다.

void func(parent *p)

{

p->printMe();

if( p가 child 객체를 가리키면)

{

((child*)p)->printNum();

}

}

그런데 포인터만 가지고 있는 상황에서 이 포인터가 parent 객체를 가리키는지 child 객체를 가리키는지를 어떻게 알 수 있을까? 이것은 일반적으로 불가능하다. 포인터는 객체의 번지를 가리키고 있을 뿐이며 이 번지에는 객체의 실제 데이터가 들어 있을 뿐 내가 누구라는 정보는 없다.

그래서 실행 중에 타입을 판별할 수 있는 기능이 필요해진 것이다. 사실 이 기능은 아주 오래 전부터 필요성이 제기되어 왔지만 C++의 초기 스펙에는 포함되지 않았고, 컴파일러 제작사나 라이브러리 제작사들은 나름대로 실행 중에 객체의 타입을 판별할 수 있는 기능을 작성해서 사용했다.

그러나 이렇게 각자가 만든 방법은 당연히 서로 호환되지 않으며 화환성이 결여된 기능은 아무리 좋아도 마음 놓고 사용할 수 없다. 그래서 최신 C++ 표준은 언어 차원에서 이 기능을 포함시켰으며 이것이 RTTI이다. 언어가 제공하는 표준이므로 호환성, 이식성이 확보된다.

RTTI는 가상함수가 있는 클래스에 대해서만 동작하는데 그 이유는 클래스의 타입 관련 정보가 vtable에 같이 저장되기 때문이다. 사실 가상 함수가 없는 클래스는 단독 클래스이거나 정적으로만 호출되므로 실행 중에 타입 정보를 알아야 할 필요가 전혀 없다고 할 수 있다.

RTTI가 제대로 동작하려면 모든 클래스에 타입과 관련된 정보를 작성해야 하며 그러자면 필시 프로그램이 느려지고 용량이 커지는 반대 급부가 있다. 그래서 대부분의 컴파일러들은 RTTI 기능을 사용할 것인지 아닌지를 옵션으로 조정할 수 있도록 되어 있다.

RTTI가 아니더라도 이 문제를 풀 수 있는 여러 가지 대체 방법이 있는데 예를 들어 가상 함수로도 문제를 풀 수 있고 대개의 경우 가상 함수가 훨씬 더 합리적인 선택이다. 위 예제의 경우 아무것도 하지 않는 printNum함수를 parent에도 작성해 놓고 가상으로 선언하면 문제는 해결된다. 그러나 기반 클래스를 건드려야 한다는 면에서 일반적인 해결책이라고 보기는 어렵다. 왜냐하면 기반 클래스는 함부로 수정할 수 있는 대상이 아닌 경우도 많기 때문이다.

나. typeid 연산자

RTTI기능은 typeid연산자로 사용한다. 이 연산자는 클래스의 이름이나 객체 또는 객체를 가리키는 포인터를 피연산자로 취하며 피연산자의 타입을 조사한다. typeid 연산자의 리턴 타입은 const type_info&이며 type_info는 클래스의 타입에 대한 정보를 가지는 또 다른 클래스이다.

class type_info {

public:

virtual ~type_info();

bool operator==(const type_info& rhs) const;

bool operator!=(const type_info& rhs) const;

int before(const type_info& rhs) const;

const char* name(__type_info_node* __ptype_info_node = &__type_info_root_node) const;

const char* raw_name() const;

private:

void *_m_data;

char _m_d_name[1];

type_info(const type_info& rhs);

type_info& operator=(const type_info& rhs);

static const char *_Name_base(const type_info *,__type_info_node* __ptype_info_node);

static void _Type_info_dtor(type_info *);

};

name 멤버 함수는 문자열로 된 타입의 이름을 조사하는데 클래스 이름이라고 보면 된다.

raw_name은 장식명을 조사하는데 사람이 읽을 수 없는 문자열이므로 비교에만 사용할 수 있다.

이 외에도 type_info객체가 같은지, 다른지를 조사하는 ==, !=연산자가 오버로딩되어 있어 원하는 타입인지 아닌지를 알 수 있다.

만약 typeid의 피연산자가 NULL포인터로부터 읽은 값일 경우 bad_typeid예외를 발생시킨다.

typeid 연산자를 사용하려면 typeinfo헤더파일을 포함하고, 프로젝트 설정 대화상자에서 RTTI 옵션도 선택해야 한다.

  • 예제 typeid

#include <iostream>

#include <typeinfo>

   

class parent

{

public:

virtual void printMe() { printf("parent\n"); }

};

   

class child : public parent

{

private:

int num;

public :

child(int anum = 1234) : num(anum) {}

virtual void printMe() { printf("child\n"); }

void printNum() { printf("child = %d\n", num); }

};

   

void main()

{

parent p, *pP;

child c, *pC;

pP = &p;

pC = &c;

   

printf("p = %s, pP = %s, *pP = %s\n", typeid(p).name(), typeid(pP).name(), typeid(*pP).name());

printf("c = %s, pC = %s, *pC = %s\n", typeid(c).name(), typeid(pC).name(), typeid(*pC).name());

   

pP = &c;

printf("pP = %s, *pP = %s\n", typeid(pP).name(), typeid(*pP).name());

}

main에서 객체 p와 c 그리고 각 타입의 포인터 pP와 pC를 선언하여 포인터가 객체를 가리키도록 했다.

이 상태에서 typeid로 객체, 포인터, 그리고 포인터가 가리키는 대상체의 타입을 조사하여 이름을 출력했다.

앞 두줄의 결과는 당연하고 상식적이다. parent타입의 p나 포인터 타입의 pP나 대상체는 모두 parent클래스 타입이며 child도 마찬가지다. 그러나 마지막 줄의 결과는 조금 다르다. parent 타입의 포인터 pP는 파생 객체인 c의 번지를 대입받을 수 있는데 이 상태에서 pP의 타입과 *pP의 타입이 각각 다르게 나타난다.

pP는 포인터 자체의 타입이므로 parent*, *pP는 현재 child타입을 가리키고 있으므로 child라는 결과가 나온다. 부모 타입의 포인터가 자식 객체를 가리키고 있음을 인식한다는 얘기이다.

각 객체와 클래스에 타입에 대한 정보가 없다면 pP가 정확하게 누구를 가리키는지를 아는 것은 불가능하지만 RTTI에 의해 이런 정보가 유지되고 조사되는 것이다. 실행 중에 포인터가 누구를 가리키는지를 정확하게 알 수 있으므로 앞의 예제에서 func함수를 RTTI를 사용해 수정해 보자.

void func(parent *p)

{

p->printMe();

if( strcmp(typeid(*p).name(), "class child") == 0 )

{

((child*)p)->printNum();

}

else

{

puts("이 객체는 num을 가지고 있지 않습니다.");

}

}

이름을 직접 비교하는 것보다는 type_info 클래스의 == 연산자로 원하는 클래스의 타입 정보와 같은지 비교하는 것이 훨씬 더 좋다.

void func(parent *p)

{

p->printMe();

if( (typeid(*p) == typeif(child) )

{

((child*)p)->printNum();

}

else

{

puts("이 객체는 num을 가지고 있지 않습니다.");

}

}

*p의 타입 정보와 child 클래스의 타입 정보를 비교하여 같으면 p가 child형의 객체를 가리키는 것으로 판단한다. typeid 연산자는 객체나 객체의 포인터뿐만 아니라 클래스 타입도 인수로 받을 수 있으므로 원하는 클래스 이름을 바로 쑬 수 있다.

다. RTTI의 내부

컴파일러가 클래스의 타입 정보를 어떤 식으로 저장하는지를 안다면 이 방식을 유사하게 흉내냄으로써 실행 중에 타입 정보를 조사할 수 있는 클래스를 만들 수도 있다.

  • 예제 CStyleRTTI

#include <iostream>

   

class parent

{

protected:

const char *name;

public:

virtual void printMe() { name = "parent"; printf("parent\n"); }

virtual const char *getName() { return name; }

};

   

class child : public parent

{

private:

int num;

public:

child(int anum = 1234) : num(anum) { name = "child"; }

virtual void printMe() { printf("child\n"); }

void printNum() { printf("child = %d\n", num); }

};

   

void func(parent *p)

{

p->printMe();

if( strcmp(p->getName(), "child") == 0 )

{

((child*)p)->printNum();

}

else

{

puts("이 객체는 num이 없음");

}

}

   

void main()

{

parent p;

child c(5);

   

func(&c);

func(&p);

}

type_info는 vtable을 통해 각 클래스마다 하나씩 생성되는데 비해 이 예제의 타입 정보는 객체마다 하나씩 생성되기 때문에 용량상의 낭비가 심한 편이다. 정적 멤버를 사용하면 클래스마다 하나씩의 타입 정보를 생성할 수 있지만 정적 멤버는 상속되지 않기 때문에 각 파생 클래스마다 고유의 멤버를 따로따로 만들어야 하는 번거로움이 있다.

33-2 C++의 캐스트 연산자

가. C의 캐스트 연산자

C의 캐스트 연산자는 변수의 타입을 마음대로 바꿀 수 있다는 면에서 무척 편리하고 유연한 코드 작성을 도와준다. 가급적이면 타입을 맞추어 쓰고 캐스트 연산자를 피하는 것이 좋지만 void*의 경우처럼 반드시 캐스트 연산자가 있어야 하는 경우도 있다. 그러나 너무 관대해서 사용자의 요구대로 무조건 타입을 바꾼다는 점에 있어서 부작용이 많다.

  • 예제 ccast1

#include <iostream>

   

void main()

{

char *str = "korea";

int *pi;

   

pi = (int*)str;

printf("%d %x\n", *pi, *pi);

}

pi가 가리키는 번지로부터 4바이트를 읽되 리틀 엔디안은 뒤쪽 번지에 높은 값이 있으므로 4문자가 거꾸로 읽혀져 정수형이 되는 것이다.

C의 캐스트 연산자는 이런 의미없는 타입 변환까지도 허용하여 실수를 했을 때 엉뚱한 결과가 나오도록 방치한다. 때로는 캐스트 연산자로 인한 강제 타입 변환으로 프로그램의 안정성이 위협받기도 한다.

  • 예제 ccast2

#include <iostream>

   

void main()

{

char *str = "korea";

int *pi;

char *pc;

   

pi = (int*)str;

pc = (char*)*pi;

printf("%s\n", pc);

}

위 코드는 문자열을 정수형으로 해석해서 정수형을 번지로 강제로 바꿔 그 번지를 읽는 연산을 하는데 이는 어떤 의미도 없고 말도 안되는 코드다.

문제는 이런 터무니없는 코드도 냉큼 컴파일된다는 점인데 컴파일러는 개발자가 지시했으므로 아무군말없이 연산자의 지시대로 타입을 바꿀 뿐이다.

C언어의 캐스트 연산자는 확실히 너무 무책임하고 개발자에게 모든 것을 떠넘긴다. 그래서 C++ 에서는 좀 더 안전하고 변환 목적에 맞게 골라 쓸 수 있는 4개의 새로운 캐스트 연산자를 제공한다. 이 연산자들은 C의 캐스트 연산자에 비해 규칙이 다소 엄격해 실수를 줄일 뿐만 아니라 어떤 의도의 타입 변환인지를 좀 더 분명히 표시하는 장점이 있다.

나. static_cast

지정한 타입으로 변경하는데 무조건 변경하는 것이 아니라 논리적으로 변환 가능한 타입만 변환한다.

static_cast<타입>(대상)

  • 예제 static_cast

#include <iostream>

   

void main()

{

char *str = "korea";

int *pi;

double d = 123.456;

int i;

   

i = static_cast<int>(d);                // 가능

pi = static_cast<int*>(str);        // 에러

pi = (int*)str;                        // 가능

}

실수형을 정수형으로 반대로 정수형을 실수형으로 캐스팅하는 것은 허용된다. 상호 호환되는 열거형과 정수형과의 변환, double과 float의 변환 등도 허용된다.

그러나 포인터의 타입을 다른 것으로 변환하는 것은 허용되지 않으며 컴파일 에러로 처리한다.

  • 예제 static_cast2

#include <iostream>

   

class parent {};

class child : public parent {};

   

void main()

{

parent p, *pP;

child c, *pC;

int i = 1;

   

pP = static_cast<parent*>(&c);        // 가능

pC = static_cast<child*>(&p);        // 가능하지만 위험

pP = static_cast<parent*>(&i);        // 에러

pC = static_cast<child*>(&i);        // 에러

}

상속 관계에 있는 클래스 포인터끼리는 상호 타입 변환할 수 있다.

첫 번째 줄은 자식 객체의 번지를 부모형의 포인터로 업 캐스팅(UpCasting)한다. 상속 계층의 위쪽으로 이동하는 변환을 업 캐스팅이라고 한다. 사실 이 변환은 캐스트 연산자를 사용하지 않아도 가능한 대입이며 언제나 안전하다. 왜냐하면 pP로 가리킬 수 있는 멤버 변수나 멤버 함수는 항상 c에 포함되어 있기 때문이다.

두 번째 줄은 부모 객체의 번지를 자식 객체의 포인터로 다운 캐스팅(DownCasting)한다. 상속 계층의 아래쪽으로 이동하기 때문에 다운 캐스팅이라고 하는데 이는 캐스트 연산자의 도움 없이 허가되지 않는다. 부모 객체가 자식 클래스의 모든 멤버를 가지고 있지 않으므로 무척 위험한 변환이다.

다. dynamic_cast

이 캐스트 연산자는 포인터끼리 또는 레퍼런스끼리 변환하는데 반드시 포인터는 포인터로 변환해야 하고 레퍼런스는 레퍼런스로 변환해야 한다. 포인터끼리 변활할 때도 반드시 상속 계층에 속한 클래스끼리만 변환할 수 있다.

부모 자식 간의 변환할 때 업 캐스팅은 원래부터 허용되는 것이므로 이 캐스트 연산자가 있으나 없으나 당연히 가능하다. 하지만 다운 캐스팅을 할 때는 무조건 변환을 허용하지 않고 안전하다고 판단될 때만 허용한다. 안전한 경우란 변환 대상 포인터가 부모 클래스형 포인터 타입이되 실제로 자식 객체를 가리키고 있을 때 자식 클래스형 포인터로 다운 캐스팅할 때이다. 즉, 실제로 가리키고 있는 객체의 타입대로 캐스팅했으므로 이 포인터로 임의의 멤버를 참조해도 항상 안전하다.

반대로 부모 클래스형 포인터가 부모 객체를 가리키고 있는 상황일 때 자식 클래스형으로의 다운 캐스팅은 안전하지 않은 변환이다. 왜냐하면 부모 객체를 다운 캐스팅해서 자식 객체를 가리키는 포인터에 대입한 후 이 포인터로 자식에게만 있는 멤버를 참조할 수도 있기 때문이다. dynamic_cast 연산자는 이럴 경우 캐스팅을 허용하지 않고 NULL을 리턴하여 위험한 변환을 허가하지 않는다.

  • 예제 dynamic_cast

#include <iostream>

   

class parent

{

public:

virtual void printMe() { printf("parent\n"); }

};

   

class child : public parent

{

private:

int num;

public:

child(int anum = 1234) : num(anum) {}

virtual void printMe() { printf("child\n"); }

void printNum() { printf("child = %d\n", num); }

};

   

void main()

{

parent p, *pP, *pP2;

child c, *pC, *pC2;

pP = &p;

pC = &c;

   

pP2 = dynamic_cast<parent*>(pC);        // 업 캐스팅-안전

pC2 = dynamic_cast<child*>(pP2);        // 다운캐스팅 - 경우에 따라 다름

printf("pC2 = %p\n", pC2);

pC2 = dynamic_cast<child*>(pP);                // 캐스팅 불가

printf("pC2 = %p\n", pC2);

}

pC를 업 캐스팅하여 부모 포인터 타입으로 바꾸는 연산은 안전하다.

다운 캐스팅의 경우는 대상 변수가 실제로 어떤 객체를 가리키는가에 따라 가능할 수도 있고 그렇지 않을 수도 있다. pP2를 pC2로 다운 캐스팅하는 경우를 보면 메모리상황은 다음과 같다.

p객체를 pP가 가리키고 c객체를 pC가 가리키는 상황에서 pP2가 pC를 업캐스팅 했으므로 pP2도 c를 같이 가리키고 있다. 이 상태에서 pC2는 pP2가 가리키고 있는 객체의 번지를 대입받고 싶다고 하자.

pP2는 parent* 타입이므로 바로 대입할 수는 없고 child*로 다운 캐스팅해서 대입해야 한다. 이때 pP2가 가리키는 실제 대상은 c객체이므로 캐스팅하고자 하는 타입과 일치하며 캐스팅은 성공하여 pC2가 c객체의 번지를 가리킬 수 있을 것이다.

그러나 두 번째 경우는 다르다. pP가 가리키고 있는 객체를 pC2에 대입하려고 한다. 이때도 타입이 일치하지 않으므로 다운 캐스팅이 필요하다. pP가 가리키는 실제 대상은 child 객체가 아니라 parent 객체이므로 이때는 다운 캐스팅을 허가할 수 없다. 만약 허가한다면 pC2 포인터로 printNum 함수를 부를 경우 제대로 된 값을 출력하지 못할 것이다. 이렇게 안전하지 않을 경우 dynamic_cast 연산자는 NULL을 리턴하여 잘못된 캐스팅임을 알린다.

안전한 객체의 번지에 대해서는 제대로 다운캐스팅을 하고 그렇지 않을 경우에는 캐스팅을 거부한다. static_cast연산자와 dynamic_cast연산자는 상속 관계에 있는 클래스들을 캐스팅한다는 점에 있어서 기능상 동일하지만 다운 캐스팅을 할 때 static_cast는 무조건 변환을 허가하고, dynamic_cast는 실행 중에 타입을 점검하여 안전한 캐스팅만 허가한다는 점에서 다르다.

이 연산자가 변환 가능성을 판단하기 위해서는 실행 중에 객체의 실제 타입을 판별할 수 있어야 한다. 그래서 이 연산자를 사용하려면 RTTI 옵션이 켜져 있어야 하며 변환 대상 타입들끼리는 상속 관계에 있어야 하고 최소한 하나 이상의 가상 함수를 가져야 한다. 만약 가상 함수가 없는 클래스 계층이라면 부모 타입의 포인터에 자식 객체의 번지를 대입할 일이 없을 것이고 캐스팅도 불필요할 것이다.

dynamic_cast 연산자는 포인터가 가리키는 대상이 캐스팅하고자 하는 타입을 가리키고 있을 때만 변환을 허용하므로 이 연산자로 변환한 포인터는 안전하게 사용할 수 있다. RTTI예제에서 실행 중 타입을 판별하기 위해 typeid 연산자를 사용했는데 이 연산자 대신 dynamic_cast연산자를 사용할 수도 있다.

void func(parent *p)

{

p->printMe();

child *c = dynamic_cast<child*>(p);

if(c)

{

c->printNum();

}

else

{

puts("이 객체는 num을 가지고 있지 않습니다.");

}

}

인수로 전달된 p를 child*로 캐스팅하되 p가 가리키는 객체가 child 타입일 때만 제대로 변환되고 그렇지 않을 때는 NULL이 리턴된다. dynamic_cast가 이 변환을 무사히 했다면 p의 대상체가 child 타입임을 확실히 알 수 있고 따라서 이 객체로부터 printNum을 불러도 안전하다. 이 연산자를 사용하면 실행 중에 포인터의 타입 점검을 할 수 있을 뿐만 아니라 캐스팅까지 할 수 있으므로 typeid연산자보다 훨씬 더 편리하다.

이 연산자는 주로 상속 관계이 있는 포인터를 캐스팅할 때 사용하는데 레퍼런스에 대해서도 캐스팅할 수 있다. 단 레퍼런스는 에러에 해당하는 NULL을 리턴할 수 없으므로 bad_cast예외를 던진다. 따라서 레퍼런스를 변환할 때는 반드시 캐스팅 코드를 try 블록에 작성하고 bad_cast예외를 잡아서 처리해야 한다.

라. const_cast

이 캐스트 연산자는 포인터의 상수성만 변경하고 싶을 때 사용한다. 상수 지시 포인터를 비상수 지시 포인터로 잠시 바꾸고 싶을 때 const_cast 연산자를 쓴다.

이 연산자는 포인터의 const 속성을 넣거나 빼거나 할 수 있으며 잘 사용되지는 않지만 비슷한 성격의 지정자인 volatile 속성과 __unaligned 속성에 대해서도 변경할 수 있다.

  • 예제 const_cast

#include <iostream>

   

void main()

{

char str[] = "string";

const char *c1 = str;

char *c2;

   

c2 = const_cast<char*>(c1);

c2[0] = 'a';

   

printf("%s\n", c2);

}

상수 지시 포인터 c1은 비상수 지시 포인터 str을 별다른 제약 없이 대입받을 수 있다. 이렇게 대입받은 포인터를 다른 비상수 지시 포인터 c2에 대입하고자 할 때 c2 = c1으로 바로 대입할 수 없다. 두 포인터의 상수성이 다르며 c1이 가리키는 읽기 전용 값을 c2로 부주의하게 바꿔 버릴 위험이 있기 때문이다. 그러나 이 경우 c1이 가리키는 대상(최초 대입받은 str)이 변경 가능한 대상이라는 것을 확실히 알고 있으므로 c1의 상수성만 잠시 무시하면 대입 가능하다. 이 때 const_cast연산자로 c1을 char*로 캐스팅 할 수 있다.

만약 str이 char*로 선언되어 있다면 이때 str은 실행 파일의 일부분을 가리키고 있으므로 변경할 수 없다. 이 경우 포인터의 상수성을 함부로 변경하면 위험해진다. 이 연산자는 변수의 상수성만 변경할 수 있을 뿐이며 그 외의 타입 변환은 허용하지 않는다.

마. reinterpret_cast

이 캐스트 연산자는 임의의 포인터 타입끼리 변환을 허용하는 상당히 위험한 캐스트 연산자이다.

int *pi;

char *pc;

pi = reinterpret_cast<int>(12345678);

pc = reinterpret_cast<chat*>(pi);

일종의 강제 변환이므로 안전하지 않고 이식성도 없다.

이 연산자는 포인터 타입간의 변환이나 포인터와 수치형 데이터의 변환에만 사용하며 기본 타입들끼리의 변환에는 사용할 수 없다.

   

연산자별로 가능한 연산이 있고 그렇지 않은 연산이 있으므로 목적에 맞게 골라서 사용해야 하며 부주의한 캐스팅을 조금이라도 방지하는 효과가 있다. 컴파일러는 캐스트 연산자의 목적에 맞게 재대로 캐스팅을 했는지 컴파일 중에 미리 에럴르 발견할 수 있을 것이다. 그리고 모양이 아주 특이하기 때문에 캐스트 연산자인지를 금방 알아볼 수 있다는 점도 또 다른 이점이기도 하다.

33-3. 멤버 포인터 연산자

가. 멤버 포인터 변수

멤버 포인터 변수란 특정 클래스(구조체도 포함)에 속한 멤버만을 가리키는 포인터이다. 일반 포인터가 메모리상의 임의 지점을 가리킬 수 있는데 비해 객체 내의 한 지점만을 가리킨다는 점에서 독특한다.

타입 클래스::*이름;

포인터 변수이므로 당연히 대상체의 타입이 필요하다. 그리고 특정 클래스 소속의 변수만을 가리킬 수 있으므로 어떤 클래스의 멤버들을 가리킬 것인지도 밝혀야 하며 클래스 소속 뒤에 포인터임을 나타내는 구두점 *와 변수의 이름을 적는다.

  • 예제 MemberPointer

#include <iostream>

   

class myClass

{

public:

int i, j;

double d;

};

   

void main()

{

myClass c;

int myClass::*pi;

double myClass::*pd;

int num;

   

pi = &myClass::i;

pi = &myClass::j;

pd = &myClass::d;

   

// pd = &myClass::i;

// pi = &myClass::d;

// pi = &num;

}

멤버 포인터 변수를 초기화할 때는 어떤 클래스에 속한 어떤 변수의 번지를 가리킬 것인지 &class::member 식으로 대입한다. 이 대입식은 특정 변수의 번지를 가리키도록 하는 것이 아니라 클래스의 어떤 멤버를 가리킬 것인가만 초기화하는 것이므로 이 상태에서 멤버 포인터 변수에 대입되는 번지가 결정되는 것은 아니다. 다만 가리키는 멤버가 클래스의 어디쯤에 있는지 위치에 대한 정보만을 가질 뿐이다. 클래스 전체를 하나의 작은 주소 공간으로 보고 클래스내의 멤버 위치를 기억하는 것이다.

멤버 포인터 변수로 객체의 실제 멤버를 액세스 할 때는 멤버 포인터 연산자라는 특수한 연산자가 필요하다.

obj.*mp

pObj->*mp

.*연산자는 좌변의 객체에서 멤버 포인터 변수 mp가 가리키는 멤버를 읽는다.

멤버 포인터 변수가 실제로 어떻게 초기화되고 .*연산자가 객체의 멤버를 어떻게 읽을 것인가는 컴파일러에 따라 구현 방식이 다를 것이다. 주로 클래스 내의 멤버 위치인 오프셋을 기억해 두었다가 .*연산자가 적용될 때 객체의 오프셋을 대상체 타입만큼 읽는 방법을 쓴다.

  • 예제 MemberPointOp

#include <iostream>

#include <conio.h>

#include <windows.h>

   

void gotoxy(int x, int y);

   

class position

{

public:

int x, y;

char ch;

   

position() { x = 0 ; y = 0 ; ch = 'a' ; }

void outPosition()

{

gotoxy(x,y); putch(ch);

}

};

   

void main()

{

position here;

position *pPos = &here;

int position::*pi;

   

pi = &position::x;

here.*pi = 30;

pi = &position::y;

pPos->*pi = 5;

here.outPosition();

}

   

void gotoxy(int x, int y)

{

COORD Pos = { x, y };

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

일반 포인터는 메모리 내의 임의 위치에 있는 지정한 타입의 변수를 가리킬 수 있지만 멤버 포인터는 지정한 타입의 변수를 가리킬 수 있되 그 범위가 클래스 내로만 국한된다는 점이 다르다.

pi가 position의 x를 가리키고 있을 때 here.*pi는 here.x와 같은 표현식이며 here.*pi에 값을 대입하면 here의 정수형 멤버 x가 변경된다. 가리킬 수 있는 범위가 객체 내부의 멤버일 뿐이지 일반 포인터에 비해 대상체를 간접적으로 액세스한다는 면에서 동일하다.

나. 멤버 포인터 연산자의 활용

주로 멤버 함수를 간접적으로 호출할 수 있다는 면에서 실용성이 있다. 멤버 포인터 연산자의 실용적인 활용예를 든다면 조건에 따라 적절한 멤버 함수를 선택하여 호출하는 기법을 들 수 있을 것이다.

  • 예제 MemPtr1

#include <iostream>

   

class test

{

public:

void op1(int a, int b) { printf("%d\n", a+b); }

void op2(int a, int b) { printf("%d\n", a-b); }

void op3(int a, int b) { printf("%d\n", a*b); }

};

   

void main()

{

int ch;

test t;

int a = 3, b = 4;

   

printf("연산 방법을 선택, 0=더하기, 1=빼기, 2=곱하기 : ");

scanf("%d", &ch);

   

switch(ch)

{

case 0:

t.op1(a,b);

break;

case 1:

t.op2(a,b);

break;

case 2:

t.op3(a,b);

break;

}

}

op1, op2, op3은 모두 두 개의 정수형 인수를 취하고 리턴값이 없는 void(*)(int,int) 함수 타입이므로 이런 함수를 가리키는 포인터 변수 pf를 선언하고 pf가 함수를 가리키도록 하면 될 것 같기도 하다. 그러나 이 코드를 컴파일 해보면 에러로 처리되는데 클래스에 속한 멤버 함수는 일반 함수와는 달리 호출하는 방법이 다르며 따라서 이런 함수를 가리키는 포인터를 선언하는 문법도 달라야 하기 때문이다. 멤버 함수는 반드시 호출하는 객체에 대한 정보를 가지는 this라는 암시적인 인수를 전달받아야 한다. 그래서 클래스의 멤버를 가리키는 멤버 포인터 변수와 멤버 포인터 연산자가 필요한 것이다.

  • 예제 MemPtr2

#include <iostream>

   

class test;

typedef void (test::*fpop)(int,int);

   

class test

{

public:

void op1(int a, int b) { printf("%d\n", a+b); }

void op2(int a, int b) { printf("%d\n", a-b); }

void op3(int a, int b) { printf("%d\n", a*b); }

};

   

void main()

{

int ch;

test t;

int a = 3, b = 4;

static fpop arop[3] = {&test::op1, &test::op2, &test::op3};

   

printf("연산 방법을 선택, 0=더하기, 1=빼기, 2=곱하기 : ");

scanf("%d", &ch);

   

if( ch >= 0 && ch <= 2 )

{

(t.*arop[ch])(a,b);

}

}

함수 포인터의 장점 중 하나는 함수를 다른 함수의 인수로 전달할 수 있다는 점이다. 예를 들어 qsort함수는 정렬은 직접 하되 자료의 대소 비교는 사용자가 제공한 함수를 호출하여 결정한다. 멤버 포인터 변수와 연산자를 사용하면 마찬가지로 멤버 함수를 다른 멤버 함수의 인수로 전달할 수 있다.

  • 예제 MemFuncArgument

#include <iostream>

   

class test;

typedef void (test::*fpop)(int,int);

   

class test

{

public:

void doCalc(fpop fp, int a, int b)

{

puts("연산결과");

printf("%d와 %d의 연산 결과 : ", a, b);

(this->*fp)(a,b);

}

void op1(int a, int b) { printf("%d\n", a+b); }

void op2(int a, int b) { printf("%d\n", a-b); }

void op3(int a, int b) { printf("%d\n", a*b); }

};

   

void main()

{

int ch;

test t;

int a = 3, b = 4;

static fpop arop[3] = {&test::op1, &test::op2, &test::op3};

   

printf("연산 방법을 선택, 0=더하기, 1=빼기, 2=곱하기 : ");

scanf("%d", &ch);

   

t.doCalc(arop[ch], a,b);

}

다. 멤버 포인터의 특징

멤버 포인터 변수는 클래스 내의 변수를 가리킨다는 면에서 일반 포인터와는 다른 면이 많은데 여기서는 멤버 포인터 변수의 몇 가지 특징에 대해 정리해 보자. 상속 관계에 있는 클래스의 멤버를 가리킬 때의 특징을 설명하는 다음 예제를 보자.

  • 예제 MemPtrInherit

#include <iostream>

   

class A

{

public:

int a;

};

   

class B : public A

{

public:

int b;

};

   

void main()

{

int A::*pa;

int B::*pb;

   

pa = &A::a;

pb = &B::b;

pb = &A::a;

pb = &B::a;

//pa = &B::b;

}

pa = &B::b는 에러로 처리되는데 A클래스 자식의 멤버를 가지지 않기 때문이다. 요약하자면 멤버 포인터 변수는 타입만 일치한다면 기반 클래스로부터 상속받은 멤버도 가리킬 수 있다. 단 다중 상속에 의해 한 멤버가 두 번 상속되었을 경우는 실제 어떤 멤버를 가리켜야 할 지 모호하므로 에러로 처리될 것이다.

멤버 포인터 변수는 정적 멤버 변수를 가리킬 수 없으며 레퍼런스 멤버를 가리킬 수도 없다.

  • 예제 MemPtrToStatic

#include <iostream>

   

class A

{

public:

int &ri;

static int a;

A(int &i) : ri(i) {}

};

int A::a = 4;

   

void main()

{

int A::*pa;

int *pi;

   

//pa = &A::ri;

//pa = &A::a;

pi = &A::a;

}

정적변수 a를 int A::*pa에 대입하면 에러로 처리된다. 애냐하면 정적 멤버 변수는 클래스 소속일 뿐 객체완느 상관없는 별개의 변수이며 객체 내에서 위치를 가지지도 않으므로 멤버 포인터 변수에 위치를 대입할 수 없기 때문이다.

레퍼런스 멤버의 번지도 대입할 수 없는데 C++은 멤버에 대한 레퍼런스라는 개념은 제공하지 않는다.

멤버 포인터의 규칙도 일반 포인터에 비해 조금 다르다. 가장 쉬운 차이점을 예로 든다면 증감 연산자를 쓸 수 없다는 정도인데 이는 함수 포인터를 증감할 수 없는 것과 같다.

반응형

'책정리 > 혼자 연구하는 C,C++ 2' 카테고리의 다른 글

36장 표준 라이브러리  (0) 2015.03.11
34장 네임 스페이스  (0) 2015.03.10
32장 예외 처리  (0) 2015.03.07
31장 템플릿  (0) 2015.03.05
30장 다형성  (0) 2015.03.03