2015. 2. 4.

[C++]객체 포인터의 참조관계

 객체 포인터의 참조관계는 C++의 다형성(polymorphism)을 이해하는 출발점이다. 참조관계를 이해하기 위해서는 상속에 대한 지식이 필요하다. 지식이 필요하다고 해서 엄청나게 거창한 내용들이 아니다. '상속'하면 떠오르는 'A is a B'만 알고 있어도 크게 무리는 없다. 그럼 상속의 개념이 객체 포인터의 참조관계에 어떻게 적용되는지 살펴보자.


상속과 포인터 참조 관계

 'A is a B'는 B는 A의 한 종류라는 의미이다. 예를 들면 학생은 사람의 한 종류라는 말은 당연하게 느껴질 것이다. 축구선수는 사람의 한 종류라는 말도 마찬가지이다. 이걸 이해하지 못하는 사람은 없을 것이라 생각한다. 이 익숙한 개념이 객체 포인터의 참조관계에도 그대로 적용된다. 객체 포인터의 참조관계라는 긴 용어가 익숙하지 않을지도 모르겠다. 단순하게 말하면 객체를 가리키는 포인터를 사이에 어떤 관계가 있는가 정도로 설명할 수 있다. 용어는 중요한 것이 아니니 코드로 먼저 보자.

 주석을 보면 디폴트 생성자디폴트 소멸자에 대한 언급이 보일 것이다. 궁금한 사람은 링크를 찾아가보자. 중요한 부분은 new 연산자를 통해 객체를 생성하는 곳이다. Person의 포인터로 Person의 객체를 가리키는 것은 너무나 자연스러운 일이다. 그 다음 줄을 보면 Person의 포인터로 Student의 객체를 가리키고 있다. 이는 너무나 자연스러운 일이 아니다. 이것이 자연스럽다면 아래의 내용을 볼 필요가 없다. 상속과 객체 포인터의 관계를 잘 이해하고 있기 때문이다.

 부모 클래스 Person의 포인터로 자식 클래스 Student의 객체를 가리키는 것이 가능할까? 가능하다. 상속의 개념을 떠올려보자. Student는 Person의 한 종류이다. 이 개념을 포인터로 확장하면 된다. Person이 Student가 될 수 있듯이, Person의 포인터는 Student의 객체를 가리킬 수 있다. 상속의 개념을 포인터에 그대로 적용하면 된다. 이는 C++만의 특징이기도 하다. 다시 말해 C++에서는 클래스 A의 포인터는 A의 객체는 물론이고, A로부터 상속받은 클래스들도 참조할 수 있다. 문법적인 원리를 따지려기보다는 상속의 개념을 C++에 적용했다고 생각하는 것이 좋다. C++의 다양한 컴파일러가 이런 일을 가능하도록 해주고 있으니 우리는 이해하기 위해서 공부하고 있다라고 생각해도 나쁘지 않겠다.

 Person <- Student <- ElementalStudent의 구조로 상속 관계가 이루어져 있다. 일반적으로 상속 관계를 나타낼 때는 화살표를 부모 방향으로 표시한다. 뜬금없는 ElementalStudent는 마법을 배우는 학생이다. 갑자기 떠올라서 그냥 그렇게 썼다. 여튼 상속을 하면 부모 클래스의 멤버함수, 멤버변수 모두 상속받는다. 그러니 자식 클래스의 객체에서 부모 클래스의 멤버함수를 당연히 호출할 수 있다.

 그렇다면 이런 경우는 어떨까? 부모 클래스의 포인터를 이용하여 자식 클래스의 멤버함수를 호출하려고 한다. 상속의 개념에 따르면 부모 클래스의 포인터는 자식 클래스를 참조하는 것이 아주 당연하다. 그렇다면 자식 클래스의 함수 또한 실행시킬 수 있지 않을까? 다음 코드를 보자.

 Person의 포인터로 Student의 객체를 참조하고 있다. 이는 앞에서 말했듯이 아무런 문제가 없다. Student의 포인터로 ElementalStudent의 객체를 참조하고 있다. 이 역시 아무런 문제가 없다. 문제는 다음에서 발생한다. Person 포인터 pPtr2는 Student를 참조하고 있음에도 Student의 멤버함수를 호출할 수 없다. Student 포인터 pPtr3은 ElementalStudent를 참조하고 있음에도 ElementalStudent의 멤버함수를 호출할 수 없다. 분명히 'A is a B'이고, 그래서 부모는 자식이 될 수 있다고 말했다. 그런데 현실은 늘 만만치 않듯이 이 경우도 우리 뜻대로 되지 않는다.


컴파일러님을 조금이나마 이해해보자

 상속과 객체 포인터 참조관계가 그러하듯이 이번에도 컴파일러의 뜻을 조금 이해할 필요가 있다. C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다. 이건 매우 중요하다. 이걸 이해하지 못하면 앞으로 C++은 외계인의 언어처럼 느껴질 것이다. 차분하게 다시 살펴보자.
 주석으로 case1이라고 표시된 부분을 보자. 포인터의 자료형은 부모 클래스의 포인터인 Person의 포인터 pPtr2이다. pPtr2가 참조하는 객체는 무엇인가? Student 클래스의 객체이다. 그리고 pPtr2로 Student 클래스의 멤버함수를 호출하려는 상황이다. 이 때 컴파일러는 무슨 생각을 하는지 조금이나마 헤아려보자. 컴파일러는 이렇게 생각할 것이다.
'pPtr2는 확실히 Person의 포인터네. 그럼 pPtr2가 참조할 수 있는 대상은 뭐가 있지? Person의 객체랑 Student의 객체랑 ElementalStudent의 객체를 참조할 수 있겠어. 호출한 함수를 보자. 어?! Play() 함수를 호출했네? 나는 지금 pPtr2가 실제로 어떤 객체를 참조하고 있는지 모르는데?? 어떻게 하나??? 혹시나 pPtr2가 참조하고 있는 객체가 Person의 객체면 Play() 함수는 당연히 호출 못하지. 부모 객체가 어떻게 자식 클래스의 멤버함수를 호출하겠어? 그래. 나는 언제나 최악의 상황은 막아야 하니깐 에러를 발생시키자. 깔끔하게 컴파일 에러로 정했어!'
 컴파일러는 이런 식으로 생각한다. 어찌보면 합리적인 선택이다. 그런데 여기서 생길 수 있는 의문은 pPtr2는 분명 Student의 객체를 참조하고 있는데 컴파일러는 그걸 왜 모르냐는 점이다. 컴파일러는 불필요한 포인터 연산을 허용하지 않음으로 문제를 발생 확률을 최소화하도록 정해진 C++의 문법에 근거해서 이런 판단을 한다. Student의 객체가 생성되었다고 판단하는 것은 프로그램이 실행된 후에 실제로 확인할 수 있는 점이기도 하다. 여튼 컴파일러는 이렇게 판단한다.


 갑자기 너무나 중요한 두 가지 사항이 등장했다. 한 번 정리하고 넘어가자.

  1. C++에서는 클래스 A의 포인터는 A의 객체는 물론이고, A로부터 상속받은 클래스들도 참조할 수 있다.
  2. C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.
 이제 우리는 이 두 가지 원칙을 기준으로 객체 포인터의 참조관계를 따지면 된다. 

 위의 코드를 다시 살펴보자. 이번엔 case2이다. 결국은 같은 상황이다. pPtr3는 Student의 포인터이고, 자식 클래스인 ElementalStudent의 객체를 참조한다. 중요한 사실은 1번 원칙에 따라서 pPtr3은 ElementalStudent의 객체를 참조할 수 있다는 점이다. 그럼 pPtr3을 이용하여 Cast() 함수를 호출하는 것은 어떠한가? 포인터 연산의 가능 여부는, 즉 멤버함수 호출도 해당하는데, 포인터의 자료형을 기준으로 판단한다고 그랬다. 2번 원칙이다. pPtr3은 Student 클래스의 포인터이다. 포인터의 자료형을 기준으로 보면 pPtr3이 호출할 수 있는 함수는 Eat(), Play() 두 개이다.

정리하면
  • 포인터로 객체을 참조할 수 있느냐 없느냐는 상속의 개념을 판단할 수 있다.
  • 포인터로 멤버함수를 호출할 수 있느냐 없느냐는 전적으로 포인터 자료형을 기준으로 판단한다. 실제 참조하는 객체가 무엇인지는 컴파일러가 알지 못한다.

댓글 5개:

  1. 감사합니다 덕분에 이해가 됬습니다!!

    답글삭제
  2. 작성자가 댓글을 삭제했습니다.

    답글삭제
  3. 간만에 하려니깐 급 헷갈려서 검색했는데 아주 좋은 설명이네요

    답글삭제
  4. 감사합니다...! 돌고 돌다가 드디어 이해하고 갑니다..

    답글삭제
  5. 감사합니다. 많은 도움이 되었습니다.

    답글삭제