2016년 3월 2일 수요일

[C++] 상속(Inheritance), 다형성(Polymorphism)

상속은 기존의 클래스를 토대로 해서 새로운 클래스를 만드는 방법이라고 할 수 있다. 객체지향 프로그래밍을 사용해서 워드 프로세서를 만드는 경우를 생각해보자. 고객으로부터 웹페이지 형식으로 문서를 저장할 수 있게 요구 받았다고 하면, 이때는 문서 저장과 관련된 기존의 클래스를 조금 개조해서 웹 페이지 형식으로 저장하게 만들 수 있다. 
  이렇게 기존의 클래스를 조금 고쳐서 새로운 클래스를 만들고 싶을 때 상속을 사용할 수 있다. 일상생활에서의 상속은 부모의 재산이나 부채를 그대로 이전하는 것을 의미하는데, 객체지향 프로그래밍에서의 상속도 이와 비슷하다. 클래스 A가 클래스 B를 상속받게 만들면, 클래스 B는 "부모 클래스" (or Base Class , Super Class)가 되고, 클래스 A는 "자식 클래스" (or Derived Class)가 된다. 부모 클래스는 자식 클래스에게 자신의 모든 멤버 변수와 함수를 물려준다. 물론 OOP에서는 물질적인 것 뿐만 아니라, 어떤 기본적인 사람의 특성들도 상속의 특성이 될 수 있다. (아래는 상속을 사용한 클래스의 간단한 예제)

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person  
  5. {  
  6.     int age;  
  7.     char name[20];  
  8. public:  
  9.   
  10.     int GetAge() const {  
  11.         return age;  
  12.     }  
  13.     const char* GetName() const {  
  14.         return name;  
  15.     }  
  16.   
  17.     Person(int _age=1, char* _name="noname"){  
  18.         age=_age;  
  19.         strcpy(name, _name);  
  20.     }  
  21. };  
  22.   
  23. class Student: public Person  
  24. {  
  25.     char major[20]; //전공  
  26. public:  
  27.     Student(char* _major){  
  28.         strcpy(major, _major);  
  29.     }  
  30.     const char* GetMajor() const {  
  31.         return major;  
  32.     }  
  33.     void ShowData() const {  
  34.         cout<<"이름: "<<GetName()<<endl;  
  35.         cout<<"나이: "<<GetAge()<<endl;  
  36.         cout<<"전공: "<<GetMajor()<<endl;  
  37.     }  
  38. };  
  39.   
  40. int main(void)  
  41. {  
  42.     Student Kim("computer");  
  43.     Kim.ShowData();  
  44.   
  45.     return 0;  
  46. };  
 상속받는 클래스를 만들기 위해서 class Student : public [상속할 클래스 이름] 이런식으로 상속이라는 문법을 사용할 수 있다. 상속을 사용한 프로그램은 메모리 공간 할당 -> 부모 클래스의 생성자 실행 -> 자식 클래스의 생성자 실행 이라는 순서를 가지게 된다.



 다형성(Polymorphism)은 OOP(Object Oriented Programming)의 개념을 설명할 때 추상화(Abstraction)과 더불어 가장 중요하게 등장하는 용어이다. 다형성을 이해하기 위해 간단한 예를 들어 보도록 하자. 예를 들어 돈을 생각해보자. 돈 만원은 만원짜리 한장, 천원짜리 열장, 백원짜리 동전 100개로도 구성 할 수 있다. 즉 만원을 구성하는 형태는 다 틀리지만, 모두 동일한 가치인 만원을 의미하게 된다. 이렇게 만원을 다양한 형태로 나타낼 수 있는 것이 다형성이다. 
  객체지향 언어에서 의미하는 다형성은 다음과 같은 의미로 정의할 수 있다. 
서로 다른 객체가 동일한 메시지에 대하여 서로 다른 방법으로 응답할 수 있는 기능
여기서 "서로 다른 객체"는 서로 다른 클래스를 의미한다. 물론 상속의 경우에도 해당이 된다. 다음의 "동일한 메시지"라는 의미는 서로 다른 객체에게 같은 메세지를 보낸다는 의미가 되겠다. 마지막으로 "서로 다른 방법"으로 응답한다는 의미는 무엇일까? 
 예를 들어 보자. 우선 어떤 도형 모형의 클래스와 이 클래스를 상속 받는 삼각형, 사각형, 원형... 등의 이런 형태를 갖는 클래스가 있다고 하자. "서로 다른 객체"는 삼각형, 사각형에서 각자 생성한 객체가 될것이고,  "동일한 메시지"는 도형을 그려라 라는 의미를 가지는 메소드인 Draw 함수가 될것이다. 그렇다면 "서로 다른 방법"은 draw라는 같은 형태의 메소를 받아 삼각형, 사각형 객체는 삼각형, 사각형을 그릴텐데, 삼각형을 그리기 위한 방법과 사각형을 그리기 위한 방법. 이것들은 서로 다를 것이다. 즉, 같은 메소드 호출에 대해 서로 다른 방법으로 응답을 하게 되는 것이다. 
 위와 같은 개념을 객체지향에서 다형성이라고 한다. 위와 같은 계층 구조를 그림으로 나타내면 아래와 같다. 
Figure 클래스는 하위 클래스에서 모형을 그리는데 사용될 수 있는 draw 함수를 가지고 있다. 그러나 Figure 클래스에는 실제 도형을 그리는 함수 구현 부분을 정의할 수가 없다. 그 이유는 Figure의 하위 클래스인 Triangle, Square, Circle 클래스마다 그리는 방법이 다르기 때문이다. 객체지향에서는 이러한 경우 Figure Class를 추상 클래스(Abstract class)로 정의 하고 draw 함수 역시 추상 함수(메소드)로 정의한다. 

 추상 메소드(함수)란 함수의 선언부분만 있고 구현 부분이 없는 함수를 말한다. 이렇게 선언된 추상 클래스는 하위 클래스에서 구현되어 사용된다. 즉, 각각의 하위 클래스에서 상속받은 추상 메소드를 서로 다른 방법으로 구현하게 되는 것이다. 이를 코드로 정리 해보면 아래와 같은 코드가 나올 것이다. 
  1. #include <iostream>  
  2. #include <string>  
  3. using namespace std;  
  4.   
  5. class Figure  
  6. {  
  7. public:  
  8.     virtual string draw() = 0;    
  9. };  
  10.   
  11. class Triangle : public Figure  
  12. {  
  13. public:  
  14.     virtual string draw() { return "Draw Triangle"; }  
  15. };  
  16.   
  17. class Square : public Figure  
  18. {  
  19. public:  
  20.     virtual string draw() { return "Draw Square"; }  
  21. };  
  22.   
  23. class Circle : public Figure  
  24. {  
  25. public:  
  26.     virtual string draw() { return "Draw Circle"; }  
  27. };  
  28.   
  29. int main()  
  30. {  
  31.     Figure* F1 = new Triangle;  
  32.     Figure* F2 = new Square;  
  33.     Figure* F3 = new Circle;  
  34.   
  35.     cout << F1->draw() <<endl;  
  36.     cout << F2->draw() <<endl;  
  37.     cout << F3->draw() <<endl;  
  38.       
  39.     delete F1;  
  40.     delete F2;  
  41.     delete F3;  
  42.           
  43.     return 0;  
  44. }  




정보 은닉, 멤버 이니셜라이저
우선 다음의 상속이 구현된 소스 코드를 보고 문제점이 무엇인지 알아 보자.  
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person  
  5. {  
  6.     int age;  
  7.     char name[20];  
  8. public:  
  9.   
  10.     int GetAge() const {  
  11.         return age;  
  12.     }  
  13.     const char* GetName() const {  
  14.         return name;  
  15.     }  
  16.   
  17.     Person(int _age=1, char* _name="noname"){  
  18.         age=_age;  
  19.         strcpy(name, _name);  
  20.     }  
  21. };  
  22.   
  23. class Student: public Person  
  24. {  
  25.     char major[20]; //전공  
  26. public:  
  27.     Student(char* _major){  
  28.         strcpy(major, _major);  
  29.     }  
  30.     const char* GetMajor() const {  
  31.         return major;  
  32.     }  
  33.     void ShowData() const {  
  34.         cout<<"이름: "<<GetName()<<endl;  
  35.         cout<<"나이: "<<GetAge()<<endl;  
  36.         cout<<"전공: "<<GetMajor()<<endl;  
  37.     }  
  38. };  
  39.   
  40. int main(void)  
  41. {  
  42.     Student Kim("computer");  
  43.     Kim.ShowData();  
  44.   
  45.     return 0;  
  46. };  
 위의 소스 코드 문제점을 보자면 main에서 객체 생성을 하는 Student Kim 객체선언 하는것을 보아 이름이 Kim으로 예상할 수 있겠다. 하지만 여기서 나이와 전공은 우리가 원하는 형태로 초기화 하지 못한다는 점이 바로 문제점이다. 즉, Student 클래스 객체가 생성될때, 자신의 멤버는 생성자내에서 초기화 하고 있지만, Person 클래스의 멤버는 default 값으로 초기화 되고 있는게 문제라고 할 수 있다. 
 잠깐 소스를 살펴 보자면, showdata 함수 내에서 GetName이라는 함수를 호출 하고 있는데, Student 클래스내에서는 GetName이라는 함수가 없지만, 이것을 호출 할 수 있는 이유는 Student 클래스가 Person 클래스를 상속하고 있기 때문에 Person 에 GetName 이라는 함수가 있어서 이런 호출이 가능한 것이다. (상속의 특성이라 할 수 있다.)

 문제로 돌아와서, Student 클래스에 Person의 멤버 변수들도 Student 클래스의 멤버로 상속되어 지니까 아예 Student 클래스를 정의 할때, 인자값으로 나이와 이름을 초기화 해서 Default로 초기화 되는것을 개선해 보자는 것이다.  
  1. Student (int _age, char* _name , char * _major)  
  2. {  
  3.     age = _age;  
  4.     strcpy(name, _name);  
  5.     strcpy(major, _major);  
  6. }  
 위와 같이 Student 생성자에서 age와 name을 초기화 해주면 될것이다. 하지만 Person 멤버들이 private 으로 선언되어 있으니 위와 같이 코딩을 하면 컴파일 에러가 날것이다. 왜 이런 에러가 나는 것일까? 비록 Person 클래스의 멤버는 Student 클래스에 의해 상속 되어 지지만, Person 클래스의 멤버들이 private이기 때문에 Person 클래스 내에서만 접근 가능 하기 때문이다. 
 그렇다고 이 문제를 해결하기 위해 public으로 멤버 변수들을 선언하면 문제는 해결되겠지만, 객체지향의 정보은닉이 무너지는 결과를 낳을 것이다. 그래서 필요한게 바로 멤버 이니셜라이저(member initializer)이다. (또는 초기화 리스트)

 멤버 이니셜라이저
  1. Student(int _age, char* _name, char* _major)  
  2.     : Person(_age, _name)  
  3. {  
  4.     strcpy(major, _major);  
  5. }  
 보통 멤버 이니셜라이저에서는 멤버 변수의 초기화를 이루기 위해 멤버변수 이름이 오지만, 여기에서는 클래스 이름이 나왔다. 이 의미는 _age와 _name 이 두개의 인자값을 받을 수 있는 생성자를 호출하라는 의미로 바뀐다. 이제 완성된 코드로 실행해보자. 이제 원하는 대로 출력이 되는 것을 알 수 있다. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person  
  5. {  
  6.     int age;  
  7.     char name[20];  
  8. public:  
  9.   
  10.     int GetAge() const {  
  11.         return age;  
  12.     }  
  13.     const char* GetName() const {  
  14.         return name;  
  15.     }  
  16.   
  17.     Person(int _age=1, char* _name="noname"){  
  18.         age=_age;  
  19.         strcpy(name, _name);  
  20.     }  
  21. };  
  22.   
  23. class Student: public Person  
  24. {  
  25.     char major[20]; //전공  
  26. public:  
  27.     Student(int _age, char* _name, char* _major)  
  28.         : Person(_age, _name)  
  29.     {  
  30.         strcpy(major, _major);  
  31.     }  
  32.     const char* GetMajor() const {  
  33.         return major;  
  34.     }  
  35.     void ShowData() const {  
  36.         cout<<"이름: "<<GetName()<<endl;  
  37.         cout<<"나이: "<<GetAge()<<endl;  
  38.         cout<<"전공: "<<GetMajor()<<endl;  
  39.     }  
  40. };  
  41.   
  42. int main(void)  
  43. {  
  44.     Student Pac(19, "Pacs.tistory.com""computer");  
  45.     Pac.ShowData();  
  46.   
  47.     return 0;  
  48. };  

 protected 
 : protected는 접근제어 키워드 중 하나로, 정보은닉 포스팅에서 이미 언급한 바가 있는데, protected는 private과 기능이 완전히 동일 하지만, 이것이 상속관계에서 쓰일 때는 파생 클래스가 기본 클래스로의 접근이 가능해진다. 
  1. class Person  
  2. {  
  3. protected:  
  4.     int age;  
  5.     char name[20];  
  6. public:  
  7.       .................  
  8. };  
 사용할 때는 위와 같이 private 대신에 protected를 써주면 되겠다. 하지만 protected를 쓴다고 해도 멤버 이니셜라이저를 쓰는것이 좋은 구조이다. 만약 멤버 변수의 이름이 변경되어 지는 경우가 생겼다고 한다면, 이 바뀐 이름들을 자신 클래스는 물론이고, 상속을 하고 있는 클래스에서도 이름을 바꿔야 하는 문제가 생긴다.  단순히 이름을 바꾸면 된다고 생각하기 쉽지만, 한클래스의 변경은 다른 클래스의 변경을 유발 시켰기 때문에 심각한 문제라고 볼 수 있다. 
  
 ※ Derived class 생성자 내에서는 Derived class의 멤버 변수만 직접적으로 초기화 시켜 주고 Base class 멤버는 Base class의 생성자를 통한 초기화가 가장 좋은 방법이라고 말할 수 있다. 

댓글 없음:

댓글 쓰기

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

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