본문 바로가기
Basic/C/C++

[C++ 언어] 제 7 강 : 상속

by boxbop 2012. 6. 28.
반응형

 

 

 오늘은 C++언언에서의 상속에 대한 내용을 공부해보도록 하겠습니다. 상속의 기본개념부터 생성 및 소멸 과정, 다양한 형태의 상속을 배워보겠습니다~!! 상속을 공부하기 전에 몇가지 이야기를 해보도록 하겠습니다.

 

다음 예제는 급여 관리 시스템의 프로그램 코드입니다. 이 시스템에서의 직원의 근무 형태는 오직!!! 고용직(Permanent)하나 뿐 입니다. 클래스는 이름과 급여정보 정도를 저장할 수 있도록 간단히 정의하였습니다.

 

class Permanent

{

  char name[10];

  int salary;

public:

  Permanent(char* _name, int sal);

  const char*  GetName();

  int GetPay();

};

 

Permanent::Permanent(char* _name, int sal)

{

  strcpy(name, _name);

  salary = sal;

}

 

const char* Permanent::GetName()

{

  return name;

}

 

int Permanent::GetPay()

{

  return salary;

}

 

 다음은 위에서 정의한 Permanent 객체를 저장하고 관리하기 위한 클래스입니다.

 

class Department

{

  Permanent*  empList[10];

  int index;

public:

  Department() : index(0) {};

  void AddEmployee(Permanent* emp);

  void ShowList();

};

 

void Department::AddEmployee(Permanent* emp)

{

  empList[index++] = emp;

}

void Department::ShowList()

{

  for(int i = 0; i < index; i++)

  {

    cout<<"name:"<<empList[i]->GetName()<<endl;

    cout<<"salary:"<<empList[i]->GetPay()<<endl;

    cout<<endl;

  }

}

 

 다음은 메인 함수 입니다.

 

int main()

{

   Department department;

 

   department.AddEmployee(new Permanent ("box", 1000));

   department.AddEmployee(new Permanent ("bop", 2000));

 

   department.ShowList();

   return 0;

 }

 

 위의 코드들은 잘 이해하셨으리라 생각됩니다. 그렇다면 이러한 프로그램을 잘 사용하고 있다가 회사 업무의 형태가 바뀌어서 프로그램 기능의 변경을 요구할 수도 있고, 추가적인 기능을 요구하게되면 프로그램의 변경 및 확장이 수월해야 합니다. 즉, 객체지향에서 중요시하는 것 중 하나는 요구사항의 변화에 따른 프로그램 유연성입니다.

 

 자 그럼 회사가 부서도 많아지고, 직원도 늘어나게 되었습니다. 첫 번째로 고용의 형태를 늘려봅시다. 새로운 고용형태는 판매직(Sales Person), 임시직(Temporary)입니다. 또한 급여제도는 기본급과 인센티브제도를 도입했다고 가정합시다.

 

                       <급여 계산방식>

고용직 : 기본급여

판매직 : 기본급여 + 인센티브

임시직 : 일한 시간 X 시간당 급여

 

우리가 하고자하는 작업은 전혀 단순한게 아닙니다. 생각해보세요. 판매직과 임시직의 등장으로 기존에 정의되어 있던 Department 클래스가 변경되어야 합니다. 또한 판매직, 임시직의 객체 저장을 위하여 배열을 늘려야하고 배열의 인덱스 정보를 담고 있는 맴버 변수도 늘려야합니다. 또한 AddEmployee 기능도 판매직과 임시직을 위해 추가되어야 합니다. 제가 하고자 하는 이야기는 임시직과 판매직 클래스를 등장시켜도 기존에 있던 클래스에 전혀 영향을 미치지 않은 방법. 다시 말해서 새로운 클래스가 추가되어도 프로그램의 다른 영역에는 전혀 변경이 가해지지 않는 방향을 추구해야된다는 말입니다.

 

 이러한 문제를 Employee Problem 이라고 하도록하겠습니다. Employee Problem을 해결하기 위한 방법은 천천히 설명드리도록 하겠구요 해결하기 위해 필요한 요소들을 지금부터 설명하도록 하겠습니다.

 

[상속의 개념]

 

 "Student 클래스가 Person 클래스를 상속한다" 를 생각해봅시다. 학생은 학생이기전에 사람이라는 하나의 객체적인 성격을 나타내고 있습니다. 즉, Student 클래스는 Person 클래스가 지니고 있는 모든 맴버 변수와 맴버 함수를 물려받아야 합니다. 다음 예제를 통해서 이해해보도록 하겠습니다.

 

class Person

{

  int age;

  char name[20];

public:

  int GetAge()

 {

    return age;

  }

  char* GetName()

 {

   return name;

  }

 Person(int _age=1, char* _name="noname")

 {

   age = _age;

   strcpy(name, _name);

  }

};

 

class Student : public Person

{

  char major[20];

public:

  Student(char* _major)

 {

   strcpy(major, _major);

  }

  char* GetMajor()

 {

   return major;

  }

  void ShowData()

 {

   cout<<"name : "<<GetName()<<endl;

   cout<<"age : "<<GetAge()<<endl;

   cout<<"major : "<<GetMajor()<<endl;

  }

};

 

 위 예제에서 볼드 처리된 class Student : public Person 부분은 Student 클래스가 Person클래스를 Public 상속한다는 선언입니다. Public 상속에 관해서는 조금 있다가 설명드리도록 하겠습니다. 이제 상속을 했으니 Student 클래스는 Person 클래스를 상속하므로 Student 객체는 Person 클래스에 선언되어 있는 맴버를 모두 포함하게 되었습니다. 때문에 Person 에서 정의한 맴버 변수나 맴버 함수를 Student 클래스에서 그대로 사용할 수 있게되었죠.

 그리고 이제부터는 Person 처럼 상속되는 클래스를 Base 클래스, Student 클래스처럼 상속하는 클래스를 Derived 클래스라고 표현하도록 하겠습니다.

 

[객체의 생성 및 소멸]

 

#include <iostream>

using namespace std;

 

class AAA

{

public:

  AAA()

  {

     cout<<"AAA() call"<<endl;

   }

  AAA(int i)

  {

     cout<<"AAA(int i) call"<<endl;

   }

};

 

class BBB: public AAA

{

public:

  BBB()

  {

     cout<<"BBB() call"<<endl

   }

   BBB(int j)

  {

     cout<<"BBB(int j) call"<<endl;

   }

};

 

int main(void)

{

  cout<<"객체 1 생성"<<endl;

  BBB bbb1;

 

  cout<<"객체 2 생성"<<endl;

  BBB bbb2(2);

 

  return 0;

}

 

 

 *실행결과----->

객체 1 생성

AAA() call

BBB() call

객체 2 생성

AAA() call

BBB(int j) call

 

"실행결과만 봐도 아~ 대충 이렇게 생성이되는구나!" 라는 감이 오지않나요? BBB bbb1 을 생성할때는 인자가 없으므로 void 생성자가 호출될 것 이고, BBB bbb2(2) 를 생성할때는 인자가 있으므로 인자 값을 받을 수 있는 생성자를 호출할 것 입니다.

 

 객체생성의 기본순서를 살펴보겠습니다.

제일 먼저 메모리 공간을 할당하는 것이 먼저입니다. 위 예제 같은 경우는 BBB의 맴버만을 고려하는 것이 아니라, BBB가 상속하고있는 AAA의 맴버까지 고려해서 메모리 공간이 할당됩니다.

그 다음으로는 메모리 공간을 할당했으니, 생성자를 호출합니다. BBB의 경우 BBB클래스의 생성자를 호출하겠죠? 이렇게 생성자를 호출하고 생성자의 몸체 부분을 실행하려고 보니까 AAA클래스를 상속하고 있는게 아닙니까? 때문에 BBB 클래스의 생성자가 호출은 되었지만, 몸체 부분이 실행되지 않습니다. 왜냐하면 BBB 클래스가 상속하고 있는 AAA클래스의 생성자가 우선적으로 실행되어야 하기때문입니다. 생각보다 중요한 과정입니다. 그렇다면 AAA() 와 AAA(int i) 생성자 중에서 어떤 것이 호출될까요? 별 다른 선언이 존재하지 않는 이상 기본적으로 Base 클래스의 void 생성자가 호출됩니다. 그럼 별 다른 선언은 어떤 것일까 하는 궁금증이 생기기 마련입니다. 다음 선언을 봅시다!

 

 BBB(int j) : AAA(j)

{

   cout<<"BBB(int j) call"<<endl;

}

 

이러한 선언에서는 AAA(int i) 생성자가 호출 될겁니다. 어디서 본 것 같은 느낌이 들지 않나요? 바로 맴버 이니셜라이져 선언입니다. AAA맴버를 j로 초기화 하라는 의미입니다. 즉, j를 인자로 받을 수 있는 AAA클래스의 생성자를 호출하라! 라는 의미죠.

 

마지막으로 메모리 공간 할당도 끝났고, Base 클래스의 생서자 호출 및 실행도 끝이 났으니 Derived 클래스의 생성자가 호출되고 실행되며 끝이 납니다. 총 3단계로 나눠 이야기할 수 있습니다.

 

자 그럼 아까 Studen와 Person 클래스를 위에서 배운 과정을 이용해 좀 더 프로페셔널하게 수정해봅시다.

 

 

class Person

{

  int age;

  char name[20];

public:

  int GetAge()

 {

    return age;

  }

  char* GetName()

 {

   return name;

  }

 Person(int _age=1, char* _name="noname")

 {

   age = _age;

   strcpy(name, _name);

  }

};

 

class Student : public Person

{

  char major[20];

public:

 

 Student(int _age, char* _name, char* _major)

 {

    age = _age;    //컴파일 에러

    strcpy(name, _name);  //컴파일 에러

    strcpy(major, _major);

  }

 

  char* GetMajor()

 {

   return major;

  }

  void ShowData()

 {

   cout<<"name : "<<GetName()<<endl;

   cout<<"age : "<<GetAge()<<endl;

   cout<<"major : "<<GetMajor()<<endl;

  }

};

 

int main(void)

{

  Student Box(20, "Boxbop" , "computer");

  Box.ShowData();

 

  return 0;

}

 

 볼드 처리된 첫 번째 부분을 봅시다. 자신이 상속하고 있는 Person 클래스의 맴버 변수를 초기화하는 선언입니다. 그러나 조금 이상하지 않나요? 분명히 Person 클래스에서  age와 name은 private로 설정해놨습니다만 Student 클래스에선 그냥 사용하고 있죠. 그렇다고 age와 name을 public 으로 선언하는 것은 좋지 않습니다. 때문에 다음과 같은 과정이 필요합니다.

 

Student(int _age, char* _name, char* _major) : Person(_age, _name)

 {

        strcpy(major, _major);

  }

 

여기서는 Person 클래스의 생성자를 명시적으로 호출하고 있습니다. Student 클래스 생성자의 첫 번째와 두 번째 인자로 전달된 값들은 그대로 Person 클래스의 생성자를 호출하는데 사용하고 있습니다. 게다가 Person 클래스의 생성자는 pulbic 으로 선언되어 있기 때문에 아무문제가 없습니다. 이제 정보 은닉을 무너뜨리지 않고, 객체 생성 시 모든 맴버 변수들을 초기화 할 수 있게 되었습니다.

 

 이번에는 객체의 소멸과정이 어떻게 이루어지는지 알아봅시다.

 

class AAA

{

public:

  AAA()

  {

     cout<<"AAA() call"<<endl;

   }

  ~AAA()

  {

     cout<<"~AAA() call"<<endl;

   }

};

 

class BBB: public AAA

{

public:

  BBB()

  {

     cout<<"BBB() call"<<endl

   }

   ~BBB()

  {

     cout<<"~BBB() call"<<endl;

   }

};

 

int main(void)

{

 BBB bbb; 

  return 0;

}

 

*실행결과---->

AAA() call

BBB() call

~BBB() call

~AAA() call

 

객체의 생성 과정과 달리 Derived 클래스의 소멸자가 먼저 실행이 되고, Base 클래스의 소멸자가 그 다음으로 실행됩니다. 마찬가지로 상속하고 있는 클래스의 객체는 자신이 상속하고 있는 클래스의 소멸자까지도 호출하고 있습니다.

 

[private, protected, public 맴버]

 

protected 맴버는 외부에서 보면 private 으로 보이고, 상속 관계에서 보면 public 으로 보입니다. 즉 protected 맴버는 private 맴버와 똑같습니다. 다만, private 맴버와 달리 상속 관계에 놓여 있는 경우에 접근을 허용할 뿐입니다.

 

 class AAA

{

  private:

    int a;

  protected:

    int b;

};

 

class BBB : public AAA

{

   public:

    void SetData()

    {

        a = 10;

        b = 20; //b는 protected 맴버이므로 상속관계에서는 접근허용!!!

     }

};

 

int main(void)

{

   AAA aaa;

   aaa.a = 1; //컴파일 에러!!! private 맴버이므로!

   aaa.b = 2; //컴파일 에러!!! protected 맴버이지만 외부 접근은 허용이 안되므로!

 

   BBB bbb;

   bbb.SetData();

 

   return 0;

}

 

 protected 맴버가 어떠한 성질을 가지고 있는지 아시겠죠? 아까 Student, Person 클래스의 문제에서도 age와 name을 private가 아닌 protected 맴버로 설정해주면 별도의 수정없이 사용할 수 있겠지요?

 

[ 3가지 형태의 상속]

 

이번에는 상속의 형태를 간단하게 알아보도록 하겠습니다. 역시 상속의 형태도 마찬가지로 private, protected, public 처럼 3가지 형태가 존재합니다. 아래의 표?를 보시죠!

 

                                     public상속        protected상속        private상속 (상속 형태)

   pulbic 맴버                public              protected                   private

   protected 맴버          protected               protected                   private

   private 맴버              접근 불가               접근 불가                  접근 불가

  (Base 클래스)

 

예를들어 Protected 맴버는 pulbic 상속을 하게되면 protected가 됨을 알 수 있습니다. 상속하는 클래스에선 사용이 가능하고 외부 접근은 불가능하게되죠. private맴버는 어떠한 상속을 하더라도 접근이 불가능 하구요. 각각이 어떠한 의미를 갖는지는 잘 이해하실거라 믿습니다. 이번 강의는 여기서 마치도록 하구요 다음 강에서는 상속과 다형성에 대해서 살펴보도록 하겠습니다~

 

 

 

 

 

 

반응형