jam 블로그

[C++] 008. 상속과 다형성 본문

개발 및 관련 자료/C

[C++] 008. 상속과 다형성

kid1412 2013. 5. 12. 20:15
728x90

I. 상속의 조건(미완성)

  • 상속은 만병통치약이 아니다.

    • 잘못된 상속의 경우 클래스의 관계를 복잡하게 만드는 단점이 있다.
  • IS-A 관계에 의한 상속

    • is a 관계라는 것은 ~은 ~이다. 라는 뜻으로 class Student : public Person 이면 Student class는 public Person 이다. 라는 것이다.
  •  Has-A 관계에 의한 상속! 그리고 대안

    • 상속은 소유를 표현하기 위해서도 사용된다.

 

II. 상속된 객체와 포인터의 관계

  • 객체 포인터 : 객체의 주소 값을 저장할 수 있는 포인터
  1. #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     void Sleep()
     {
      cout<<"Sleep"<<endl;
     }
    };
  4. class Student : public Person
    {
    public:
     void Study()
     {
      cout<<"Study"<<endl;
     }
    };
  5. class PartTimeStd : public Student
    {
    public:
     void Work()
     {
      cout<<"Work"<<endl;
     }
    };
  6. int main()
    {
     Person* p1 = new Person;
     Person* p2 = new Student;
     Person* p3 = new PartTimeStd;
  7.  p1->Sleep();
     p2->Sleep();
     p3->Sleep();
     return 0;
    }
  • 메인에서 p1은 Person을 p2는 Student를 p3는 PartTimestd를 가리킨다. 그렇기 때문에 sleep을 사용할 수 있다.

    • Person* p1 = new PartTimestd; 처럼 바꾸어도 컴파일이 된다.

      • 이유는 PartTimestd 클래스의 객체는 PartTimestd 객체이자, Student객체이면서 동시에 Person객체 이기도 하다.

 

  • Employee problem 해결의 첫번째 단계

    • 이부분은 책으로 봐주세요

 

  • 객체 포인터의 권한

    • 클래스의 객체 포인터는 가리키는 대상이 어떠한 객체이건, 클래스 타입 내에 선언된 멤버와 클래스가 상속한 클래스의 멤버에만 접근이 가능하다.

III. 상속된 객체와 참조의 관계

  • 객체 레퍼런스 : 객체를 참조할 수 있는 레퍼런스

    • AA 클래스의 포인터는 *AAA 객체의 주소 값 뿐만 아니라, AAA 클래스를 상속받는 클래스 객체의 주소 값도 저장이 가능하다.
    • AAA 클래스의 레퍼런스는 AAA 객체 뿐만아니라, AAA 클래스를 상속받는 클래스의 객체도 참조 가능하다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     void Sleep()
     {
      cout<<"Sleep()"<<endl;
     }
    };
  4. class Student : public Person
    {
    public:
     void Study()
     {
      cout<<"study()"<<endl;
     }
    };
  5. class PartTimeStd : public Student
    {
    public:
     void Work()
     {
      cout<<"Work()"<<endl;
     }
    };
  6. int main()
    {
     PartTimeStd p;
     Student& ref1 = p;
     Person& ref2 = p;
  7.  p.Sleep();
     ref1.Sleep();
     ref2.Sleep();
     
     return 0;
    }
  • 객체를 참조하는 레퍼런스의 특징을 보여주는 소스이다.

 

  • 객체 레퍼런스의 권한

    • AAA 클래스의 객체 포인터는 가리키는 대상이 어떠한 객체이건, AAA 클래스 타입 내에 선언된 멤버와 AAA클래스가 상속한 클래스의 멤버에만 접근이 가능하다.
    • AAA 클래스의 레퍼런스는 참조하는 대상이 어떠한 객체이건, AAA 클래스 타입 내에 선언된 멤버와 AAA ㅡㅋㄹ래스가 상속한 클래스의 멤버에만 접근이 가능하다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     void Sleep()
     {
      cout<<"Sleep()"<<endl;
     }
    };
  4. class Student : public Person
    {
    public:
     void Study()
     {
      cout<<"study()"<<endl;
     }
    };
  5. class PartTimeStd : public Student
    {
    public:
     void Work()
     {
      cout<<"Work()"<<endl;
     }
    };
  6. int main()
    {
     PartTimeStd p;
     p.Sleep();
     p.Study();
     p.Work();
  7.  Person& ref = p;
     ref.Sleep();
     ref.Study();
     ref.Work();
     
     return 0;
    }
  • 위의 소스에서 오류가 나는데 메인에서 ref.Study()와 ref.Work()가 오류가 난다.

    • 이유는 ref에 p를 선언 하였지만 Person클래스 이기 때문에 Person클래스 안에 있는 Sleep만 불러 올수 있다.

 

IV. Static Binding & Dynamic Binding

  •  오버라이딩의 이해
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
  5. int main()
    {
     BBB b;
     b.fct();
     return 0;
    }
  •   class BBB나 class AAA 에서 fct 함수를 같이 썼는데 메인에서 BBB에 있는 fct를 호출하게 된다.

    • 이유는 AAA에 정의했던 fct는 BBB에서 정의했던 fct 함수에 가져진 것이다.

 

  • 오버라이딩된 함수를 호출 하는 방법
  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
  5. int main()
    {
     BBB *b = new BBB;
     b->fct();
  6.  AAA* a = b;
     a->fct();
  7.  delete b;
     return 0;
    }
  •  오버라이딩 된 함수를 호출 하는 방법은 포인터를 써서 하는 것이다.

    • AAA* a = b; 처럼 포인터 b가 지니고 있는 값을 a에 대입 시켜 주므로써 fct를 호출 하면 AAA에 있는 것이 호출 된다.

      • 여기는 설명이 좀 이상하니 나중에 보충하겠습니다.

 

  • 멤버 함수를 가상(virtual)으로 선언하기

    • 오버라이딩되는 함수를 가상으로 선언할수 있다. 즉, dynamic binding을 한다는 의미이다.
  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
  5. int main()
    {
     BBB *b = new BBB;
     b->fct();
  6.  AAA* a = b;
     a->fct();
  7.  delete b;
     return 0;
    }
  • 결과값을 보면 AAA 클래스 안에 fct가 없는 것처럼 나온다. 가상의 함수로 설정해 놨기 때문에 BBB AAA 가 나와야 할것이 BBB BBB가 나온 것이다.

 

  • 가상 함수의 특성은 상속 된다.
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
    class CCC : public BBB
    {
    public:
     void fct()
     {
      cout<<"CCC"<<endl;
     }
    };
  5. int main()
    {
     BBB *b = new CCC;
     b->fct();
  6.  AAA* a = b;
     a->fct();
  7.  delete b;
     return 0;
    }
  •  위의 결과를 찍어보면 CCC CCC가 나오는데 AAA에 있는 virtual 로 선언하여 상속 받은 BBB나 CCC가 fct 함수가 virtual이 선언 되어 있는 것과 같으나 최종적으로 오버라이딩한 함수를 제외한 나머지 함수는 가려지게 되어서 CCC에있는 fct 함수가 호출된다.

 

  • 무엇이 static binding이고, 무엇이 dynamic binding인가?
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
    int main()
    {
     BBB b;
     b.fct();
  5.  AAA* a = new BBB;
     a->fct();
     return 0;
    }
  •  main을 보면 b.fct() 가 static binding이고 AAA* a = new BBB가 dynamic binding 이다.

    • 왜냐하면 b.fct는 컴파일하는 동안에 호출될 함수가 결정되기 때문이다. 즉 호출할 함수가 이미 고정되어 있다는 것이다
    • AAA* a = new BBB는 컴파일 동안이 아닌 실행하는 동안에 호출될 함수가 결정된다. 즉 포인터가 가리키는 객체가 무엇이냐에 따라서 그 문장이 호출되는 함수가 유동적이기 때문이다.

 

  • 오버라이딩된 함수 호출하기

    • 범위 지정 연산자를 통해서 오버라이딩된 함수를 호출 가능하다.
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      AAA::fct();
      cout<<"BBB"<<endl;
     }
    };
    int main()
    {
     AAA* a = new BBB;
     cout<<"1"<<endl;
     a->fct();
  5.  cout<<"2"<<endl;
     a->AAA::fct();
     return 0;
    }
  •  첫번째 방법은  BBB를 호출시 AAA쪽에 fct를 호출하라는 뜻이다.
  • 두번째 방법은 a를 AAA:fct()로 아예 경로를 지정해 주는 방법이다.

V. Employee Problem 완전 해결

  • 여기 부분은 책을 봐주세요
  •  순수 가상 함수와 추상 클래스

    • virtual int GetPay() = 0 이런식으로 선언 한것이 순수 가상 함수이다.

      • 컴파일러에게 이 함수가 호출될 일이 없거든 선언만 하고 정의를 안한 것이다. 라고 얘기 해주는 정도의 내용이다
      • 이러한 멤버함수가 하나 이상이 있으면 추상 클래스라고 한다.

 

VI. virtual 소멸자의 필요성

  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
     char* str1;
    public:
     AAA(char* _str1)
     {
      str1 = new char[strlen(_str1)+1];
      strcpy(str1,_str1);
     }
     ~AAA()
     {
      cout<<"~AAA() call"<<endl;
      delete []str1;
     }
     virtual void ShowString()
     {
      cout<<str1<<' ';
     }
    };
  4. class BBB : public AAA
    {
     char* str2;
    public:
     BBB(char* _str1,char* _str2):AAA(_str1)
     {
      str2 = new char[strlen(_str2)+1];
      strcpy(str2,_str2);
     }
     ~BBB()
     {
      cout<<"~BBB() call"<<endl;
      delete []str2;
     }
     virtual void ShowString()
     {
      AAA::ShowString();
      cout<<str2<<endl;
     }
    };
  5. int main()
    {
     AAA* a = new BBB("Good","123");
     BBB* b = new BBB("Good","345");
  6.  a->ShowString();
     b->ShowString();
  7.  cout<<"-----객체 소멸 직전-----"<<endl;
     delete a;
     delete b;
     return 0;
    }
  • 위의 소스에서 main 함수를 보면 delete a를 해줄때 포인터 a를 통한 BBB 객체의 소멸을 시도 하지만 AAA 타입이기 때문에 BBB객체를 AAA 객체로 인식한다. 그래서 AAA의 소멸자만 호출이 된다.

 

  • virtual 소멸자

    • 위에 해결책은 AAA 소멸자에 virtual을 해주면 된다.
  1.  virtual ~AAA()
     {
      cout<<"~AAA() call"<<endl;
      delete []str1;
     }
  •  위의 방식은 먼저 AAA클래스의 소멸자를 호출하려 할때 virtual 관계여서 같은 역할을 하는 BBB 소멸자를 호출한다. 그런데 BBB 클래스는 AAA클래스를 상속 받고 있기 때문에 BBB 클래스의 소멸자를 한 후에 AAA 클래스의 소멸자를 호출한다.
Comments