2016년 3월 12일 토요일

[c++] 상속 4 - 객체 포인터 와 객체 레퍼런스

객체 포인터란 객체를 가리킬 수 있는 포인터를 의미한다. (객체의 주소 값을 저장할 수 있는 포인터)

  - 예를 들어 A 클래스의 포인터는 A 객체뿐만 아니라, A클래스를 상속하는 파생 클래스 객체의 주소 값도 저장이 가능하다

 다음 상속 관계를 한번 살펴 보도록 하자.
 is-a관계에 의해서 ScholarStd 객체는 Student 객체이자 Person 객체도 동시에 된다.  ("ScholarStd 는 Person 객체이다." 이런 말도 틀린말은 아니다) 그러면 "Student 객체는 ScholarStd  객체이다" 는 성립이 안된다. is-a 관계는 아래쪽으로 성립이 안된다. 우리가 프로그램상에서 서로의 객체를 생성했다고 해보자. 아래의 예제는 is-a 관계에서 위의 그림은 소스코드로 나타낸 것이다. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person  
  5. {  
  6. public:  
  7.     void Sleep(){   
  8.         cout<<"Sleep"<<endl;  
  9.     }  
  10. };  
  11.   
  12. class Student : public Person  
  13. {  
  14. public:  
  15.     void Study(){  
  16.         cout<<"Study"<<endl;  
  17.     }  
  18. };  
  19.   
  20. class ScholarStd  : public Student  
  21. {  
  22. public:  
  23.     void Receive_Scholar(){  
  24.         cout<<"Work"<<endl;  
  25.     }  
  26. };  
  27.   
  28. int main(void)  
  29. {  
  30.     Person* p1=new Person;  
  31.     Person* p2=new Student;  
  32.     Person* p3=new ScholarStd ;  
  33.   
  34.     p1->Sleep();  
  35.     p2->Sleep();  
  36.     p3->Sleep();  
  37.   
  38.     return 0;  
  39. }  
여기서 p1,2,3 포인터는 Person 클래스의 포인터니까 Person클래스의 객체를 가리킬 수 있는 포인터가 되는것이다. (*p1는 Person 클래스의 객체를 가리킬 수 있는 포인터) 
- p2라는 포인터는 Student 클래스의 객체를 가리키는 것도 문제가 없을 것이다. 
- p3라는 포인터는 ScholarStd 클래스의 객체를 가리키는 것도 문제가 없을것이다. 

그럼 "Student *s;" 라고 선언할시를 생각해 보자. 이 선언은 ScholarStd 클래스의 객체를 가리키는 것은 문제 없다. (ScholarStd 객체는 Student 객체이자 Person 객체이므로) 하지만 포인터 s는 Person 클래스의 객체를 가리키는 것은 문제가 된다. 왜냐하면 모든 Person 객체들이 다 Student가 아니기 때문이다. 그리고 클래스의 객체를 가리킨다는 의미는 역으로 is-a관계가 성립해야 된다는 걸 의미한다.

객체 포인터의 권한 
  지금까지는 객체가 가리키는 것이 어떤 경우가 합당한지를 알아 봤는데, 이 객체를 가지고 그 안의 함수를 사용하는 경우는 어떨까? 객체 포인터 권한에 대해 알아보자. 
  예를 들어 여기 A라는 클래스가 있다 A는 B에 의해 상속되어 지고, C는 B클래스에 의해 상속되어 진다고 가정하고 각각의 클래스에는 클래스 이름에 맞는 a,b,c() 함수가 존재한다고 해보자. 
 그 후, C 클래스의 객체를 생성한다고 하자. 그러면 C클래스의 객체 안에는 아마도, A클래스내에 선언되어 있는 a()함수도 있을것도 b()도 있고 자신의 클래스의 함수에 c()도 있을것이다. 아래의 그림 처럼 말이다. (아래의 그림처럼 함수 경계선이 있는것은 아니다.) C 클래스의 객체는 B객체이자 A 객체가 되므로 A,B,C 클래스의 포인터를 가지고 이 것을 가리킬수 있을 것이다. 
  이렇게 포인트 변수들이 가리키는 대상이 같을 경우, 만약 포인터 A (a*)가 가리키는 C 객체의 주소값이 0x10번지 라고 하면, 포인터 b,c의 주소값은 0x10번지로 으로 다 똑같다.  
  A클래스의 포인터를 가지고 가리키는 대상이 비록 C객체 이지만, A클래스의 포인터를 가지고 호출 할 수 있는 함수는 A클래스 내에 선언되어 있는 멤버 변수나 멤버함수로 제한적이다.  "A클래스 포인터(A* a) 가 가리키는 대상이 C 객체 일지라도, A 클래스의 포인터로 접근 할 수 있는 영역은 A 클래스내로 제한된다." 라는 이야기 이다.  


 위의 이야기를 염두에 두고 소스코드를 보자.  C클래스의 포인터 c는 C클래스 객체를 가리키고 있다. 그러므로 C클래스의 포인터로 접근할 수 있는 영역은 C클래스내로 제환된다는 이야기 이다. is-a관계에 의해 C는 모든 함수에 접근 권한을 가지게 되므로 아래의 코드는 문제가 없다. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class A{  
  5. public:  
  6.     void a(){ cout<<"call a function"<<endl; }  
  7. };  
  8.   
  9. class B : public A  
  10. {  
  11. public:  
  12.     void b(){ cout<<"call b function"<<endl; }  
  13. };  
  14.   
  15. class C : public B  
  16. {  
  17. public:  
  18.     void c() { cout<<"call c function"<<endl; }  
  19. };  
  20.   
  21. void main()  
  22. {  
  23.     C* c = new C;  
  24.     c->a();  
  25.     c->b();  
  26.     c->c();  
  27. }  

다음의 코드를 한번 살펴보자. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class A{  
  5. public:  
  6.     void a(){ cout<<"call a function"<<endl; }  
  7. };  
  8.   
  9. class B : public A  
  10. {  
  11. public:  
  12.     void b(){ cout<<"call b function"<<endl; }  
  13. };  
  14.   
  15. class C : public B  
  16. {  
  17. public:  
  18.     void c() { cout<<"call c function"<<endl; }  
  19. };  
  20.   
  21. void main()  
  22. {  
  23.     C* c = new C;  
  24.     B* b = c;   
  25.     A* a = b;  
  26.   
  27.     cout<<c <<endl;  
  28.     cout<<b <<endl;  
  29.     cout<<a <<endl;  
  30. }  


 우선 24번째 줄에 있는 B* b = c;  문법을 보자. 여기서 우리는 포인터의 타입이 일치 하지 않는데 어떻게 이런 문법이 가능한가 라는 의구심을 가지게 될것이다. 하지만 앞서 배웠듯이, C클래스의 포인터는 C클래스의 포인터 이자, A,B 클래스의 포인터도 되므로 이런 문법은 성립이 되는 것이다. 
 문제 이야기로 넘어와 보자. 위 예제는 객체 포인터 권한 도입부분에 설명한 부분을 코드로 풀어 놓은 것이다. c,b,a를 출력해보면 같은 주소값을 가짐을 확인할 수 있다. 
  그리고 또 확인해야 할 것이 있다. B클래스의 포인터를 이용해 각각 함수를 호출해 보자. 메인함수에 아래와 같은 코드를 넣어 컴파일해 보자.
  1. b->a();  
  2. b->b();  
  3. b->c(); //에러  
위와 같이 컴파일 에러를 확인해 볼수 있다. 왜그런 것일까? 우리는 b라는 포인터가 가리키는 객체가 C클래스의 객체라는 것을 알고 있지만, 컴파일러는 b라는 포인터가 가리키는 대상이 C클래스의 객체라는것을 모른다. (그게 C건 어떤 것이건 아예 모른다.) b라는 포인터가 어느 클래스의 객체를 가리키는지는 runtime에 결정 되기 때문에 컴파일러는 이 대상이 어느 객체를 가리키는지 결정을 못내리는 것이다.

 이와 마찬가지로 A의 클래스 포인터 a 가 가리키는 것이 우리는 C클래스의 객체라는것을 알고 있지만, 컴파일러는 무엇을 가리키는지 알지 못한다. 그래서 컴파일러는 포인터 a가 가리키는 대상이 무엇이든지 a함수의 호출은 전혀 문제가 안될것이라는 판단하에 a함수 호출만을 보장하는 것이다.


객체 레퍼런스란 객체를 참조 할 수 있는 레퍼런스로 클래스 포인터(객체 포인터)의 특성과 일치 한다.  

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person  
  5. {  
  6. public:  
  7.     void Sleep(){   
  8.         cout<<"Sleep"<<endl;  
  9.     }  
  10. };  
  11.   
  12. class Student : public Person  
  13. {  
  14. public:  
  15.     void Study(){  
  16.         cout<<"Study"<<endl;  
  17.     }  
  18. };  
  19.   
  20. class ScholarStd : public Student  
  21. {  
  22. public:  
  23.     void Receive_Scholar(){  
  24.         cout<<"Work"<<endl;  
  25.     }  
  26. };  
  27.   
  28. int main(void)  
  29. {  
  30.     ScholarStd p;  
  31.     Student& ref1=p;  
  32.     Person& ref2=p;  
  33.   
  34.     p.Sleep();  
  35.     ref1.Sleep();  
  36.     ref2.Sleep();  
  37.   
  38.     return 0;  
  39. }  
 앞서 배웠듯이 is-a 관계에서 어떤 클래스의 포인터는 자신 객체 뿐만 아니라, 자신을 상속하고 있는 클래스의 객체도 가리킬수가 있다. Person 클래스의 포인터를 가지고, 위의 코드에서의 세개 클래스 객체를 다 가리킬 수 있다. Person 클래스의 참조도 마찬가지로, 여기 세개의 클래스를 다 참조 할 수 있다. 저번에 포스팅했던 객체 포인터의 특성과 일치하는 것을 알 수 있다. 

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person  
  5. {  
  6. public:  
  7.     void Sleep(){   
  8.         cout<<"Sleep"<<endl;  
  9.     }  
  10. };  
  11.   
  12. class Student : public Person  
  13. {  
  14. public:  
  15.     void Study(){  
  16.         cout<<"Study"<<endl;  
  17.     }  
  18. };  
  19.   
  20. class PartTimeStd : public Student  
  21. {  
  22. public:  
  23.     void Work(){  
  24.         cout<<"Work"<<endl;  
  25.     }  
  26. };  
  27.   
  28. int main(void)  
  29. {  
  30.     PartTimeStd p;  
  31.     p.Sleep();  
  32.     p.Study();  
  33.     p.Work();  
  34.   
  35.     Person& ref=p;  
  36.     ref.Sleep();  
  37. //  ref.Study(); // Error의 원인  
  38. //  ref.Work();  // Error의 원인  
  39.   
  40.     return 0;  
  41. }  
 객체의 레퍼런스 권한도 객체 포인터 권한과 마찬가지이다. 객체 포인터때도 지겹도록 반복해서 언급을 했지만, A클래스의 참조는 B객체도 C객체도 참조 할 수 있는데, 접근할 수 있는 영역은 A클래스내에 선언되어 있거나, A 클래스가 상속하고 있는 멤버로서 제한되어 진다. 

인터와 레퍼런스는 분명 차이가 있는 개념이지만 쉽게 혼용해서 사용할수 있기때문에 초보자들의 경우 많은 오해를 할 수 있습니다. 여느 C/C++ 프로그램들이 그렇듯이 잘못된 포인터, 레퍼런스의 사용은 프로그램을 hang 시키는 아주 심각한 결과를 초래하게 되므로 이에 대한 정확한 이해가 필요합니다.(참고로 C++에서는  포인터, 레퍼런스를 모두 지원하지만 Java, C#과 같은 고수준 언어에서는 레퍼런스만 지원합니다.)



포인터는 C/C++를 공부하는 사람이라면 누구나 아는것처럼 어떤 대상을 가리키는 것입니다.
레퍼런스는 어떤 대상을 지칭하는 또 하나의 이름이라는 의미가 있습니다.

무슨 말장난 처럼 들리겠지만 이 둘 사이에는 다음과 같이 분명하고 확실한 차이점이 있습니다.


1. NULL 포인터는 존재하지만 NULL 레퍼런스는 존재하지 않는다.
포인터는 어떤 대상을 가리킬수 있지만, 아무것도 가리키지 않을수도 있습니다. 후자의 경우 일반적으로 NULL 포인터라고 말합니다. 이와는 대조적으로 NULL 레퍼런스라는 것은 존재하지 않습니다. 이유가 무엇일까요?
레퍼런스란 어떤 대상을 지칭하는 또 하나의 이름이라는 뜻이 있다고 했는데, 여기서 말하는 어떤 대상이랑 반드시 존재하는 것입니다. 어떤 사람이 있다고 할때 이 사람의 이름과 이사람들 지칭하는 많은 별명들이 있을수 있습니다. 별명은 그 사람이 존재할 때 의미가 있는것이지, 존재하지도 않는 사람한테 별명을 붙일수는 없는 노릇이지요.
레퍼런스도 마찬가지 입니다. 존재하지 않는 대상에 대한 레퍼런스 따위는 아무 의미도 없습니다.
  1. int main()  
  2. {  
  3.     int *a; // 정상  
  4.     int &b; // 에러  
  5. }  
위 코드에서 처럼 초기화를 하지 않는 포인터의 선언은 가능하지만, 초기화를 하지 않는 레퍼런스의 선언은 컴파일러가 에러로 처리합니다. 레퍼런스는 반드시 선언과 동시에 초기화를 해야합니다.(멤버변수일 경우 생성자에서 초기화를 처리해줘야 합니다.)


2. 포인터는 가리키는 대상을 변경할 수 있지만 레퍼런스는 한번 대상을 지칭하면 변경할수 없다.
포인터는 타입만 다르지 않다면 한개의 포인터를 가지고 어떤 대상이든 자유롭게 가리킬수 있습니다.(설사 타입이 다르더라도 가능합니다!)  하지만 레퍼런스에서는 초기화시에 한번 지칭한 대상을 끝까지 변경할 수 없습니다.


만약 어떤 대상이 있고 이 대상을 포인터로 가리켜야 할지 레퍼런스로 참조해야 할지 헷갈린다면 다음과 같은 가이드라인을 따르도록 합니다.

  • 존재할수도 있고 아닐수도 있는 객체라면 이것은 포인터로 가리키게 한다.
  • 객체가 언제나 존재한다면 레퍼런스로 참조한다.
  • 특정 객체를 가리키고 있다가 다른 객체를 가리키고 싶다면 포인터를 사용한다.
  • 특정 객체의 생명주기를 예측할수 없다면 포인터를 사용한다.
보다 자세한 내용은 C++ 프로그래머의 필독서 이펙티브 C++(스캇 마이어스) 1장을 찾아보길 권합니다.

댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...