read

열여섯번째 열혈강의


mobile

이제 오늘부터는 상속의 문법적인 이해 부터 시작할 것이다. 사실 C 부터 배운 사람들은 상속에 들어왔을 때쯤 지쳐가기 마련이다. 뭔가 몰라도 기본적인 학부생의 능력안에서 프로젝트를 완료하긴 가능할 것이기 때문이다. 그래서 조금은 어렵게 느껴질지도 모르겠다. 그러나 차근히 한번 가보도록 하자.


상속이란


흔히, 상속이란 단어를 보면 재산의 상속을 생각한다. 그러나 상속의 대상은 재산뿐 아니라 다음과 같을 수도 있다.

철수는 아버지로부터 좋은 목소리와 큰 키를 물려받았다.(상속받았다)

따라서 철수는 자신이 가지고 있는 고유의 특성 이외에 아버지로부터 물려 받은 목소리와 큰 키라는 또 다른 특성을 함께 지니게 되었다. 이것이 바로 상속 이 갖는 의미다. 그럼 이것을 클래스로 옮겨보자.

UnivStudent 클래스가 Person 클래스를 상속한다.

UnivStudent 클래스가 Person 클래스를 상속하게 되면, UnivStudent 클래스는 Person 클래스가 지니고 있는 모든 멤버를 물려받는다. 즉, UnivStudent 객체에는 UnivStudent 클래스에 선언되어 있는 멤버뿐만 아니라 Person 클래스에 선언되어 있는 멤버도 존재하게 된다.


상속의 방법과 그 결과


그럼 예제를 통해서 실제 상속의 결과를 확인해보겠다. 그럼 먼저 Person 클래스를 정의해 보자.

class Person{
  private:
    int age;        //나이
    char name[50];  //이름

  public:
    Person(int myage, char* myname) : age(myage){
      strcpy(name,myname);
    }

    void WhatYourName() const{
      cout << "My name is " << name << endl;
    }

    void HowOldAreYou() const{
      cout << "I am " << age << " years old" << endl;
    }
};

이 클래스와 관련해서는 추가 설명을 하지 않겠다. 그럼 계속해서 UnivStudent 클래스를 보자.

class UnivStudent : public Person{ // Person 클래스의 상속을 의미함
  private:
    char major[50];
  public:
    UnivStudent(char* myname, int myage, char* mymajor) : Person(myage, myname){
      srcpy(major, mymajor);
    }

    void WhoAreYou() const{
      WhatYourName();
      HowOldAreYou();
      cout << "My major is " << major << endl << endl;
    }
  };

위의 클래스 정의해서

class UnivStudent : public Person 이 의미하는 바는 public 상속 이다.

그런데 public의 의미는 잠시후에 보도록 하고, 우선 Person 클래스를 상속했다는데 주목하자. 그럼 이번에는 멤버함수 WhoAreYou를 살펴보자(생성자는 추후 설명하겠다)

void WhoAreYou() const{
  WhatYourName();
  HowOldAreYou();
  cout << "My major is " << major << endl << endl;
}

UnivStudent 클래스에는 WhatYourName 함수와 HowOldAreYou 함수가 정의되어 있지 않음에도 불구하고, 이 두 함수를 호출하 수 있는 이유는 UnivStudent 클래스가 Person 클래스를 상속했기 때문이다. 즉, 이 두 함수는 Person 클래스의 멤버함수 이기 때문에 호출이 가능하다. 그리고 이것이 바로 상속의 가장 두드러진 특징이다.


상속받은 클래스의 생성자 정리


앞서 정의했던 생성자를 한번 살펴보자.

UnivStudent(chcar* myname, int myage, char* mymajor) : Person(myage, myname){
  strcpy(major, mymajor);
}

일단 위의 생성자 코드를 유심히 한번 관찰하기 바란다(이해되지 않는 부분이 있어도 괜찮다) 그리고나서 다음을 보자.

UnivStudent 클래스의 생성자는 Person 클래스의 멤버까지 초기화해야 할 의무가 있을까?

UnivStudent 객체가 생성되면, 그 객체안에는 다음의 멤버변수가 존재하게 된다.

  • Person의 멤버변수
    • age, name
  • UnivStudent의 멤버변수
    • major

따라서 UnivStudent 클래스의 생성자에서는 이들 모두에 대한 초기화에 책임을 져야한다.

UnivStudent 클래스의 생성자는 Person 클래스의 멤버까지 초기화해야 할 의무가 있다.

그럼 다음을 또 보자.

UnivStudent 클래스의 생성자가 Person 클래스의 멤버를 어떻게 초기화하는 것이 좋겠는가?

멤버는 클래스가 정의될 때, 멤버의 초기화를 목적으로 정의된 생성자를 통해서 초기화하는 것이 가장 안정적이다 (당시에 초기화의 내용 및 방법이 결정되므로) 그것이 비록 상속의 관계로 묶여있다고 할지라도 말이다. 따라서 대답은 다음과 같다.

UnivStudent 클래스의 생성자는, Person 클래스의 생성자를 호출해서 Person 클래스의 멤버를 초기화하는 것이 좋다.

그럼 정리해 보자.

UnivStudent 클래스의 생성자는 자신이 상속한 Person 클래스의 멤버를 초기화할 의무를 지닌다. 그래서 UnivStudent 의 생성자는 Person 의 생성자를 호출하는 형태로 Person 클래스의 멤버를 초기화 하는 것이 좋다.

그럼 이제 UnivStudent 클래스의 생성자를 살펴보자.

UnivStudent(char* myname, int myage, char* mymajor) : Person(myage, myname){
  srcpy(major, mymajor);
}

생성자의 매개변수 선언을 보면, UnivStudent의 멤버뿐 아니라, Person의 멤버를 초기화하기 위한 인자의 전달까지 요구 하고 있음을 알 수 있다. 이어서 이니셜라이저가 등장하는데, 이것이 의미하는 바는 생성자의 호출이다. 즉, Person 클래스의 생성자를 호출하면서 인자로 myage와 myname에 저장된 값을 전달하는 것이다. 이렇게 UnivStudent 클래스와 같이 상속받은 클래스 는 이니셜라이저를 이용해 상속하는 클래스 의 생성자 호출을 명시할 수 있다.

그럼 예제를 확인하고 실행해 보자.

#include <iostream>
#include <cstring>
using namespace std;

class Person{
  private:
    int age;        //나이
    char name[50];  //이름

  public:
    Person(int myage, char* myname) : age(myage){
      strcpy(name,myname);
    }

    void WhatYourName() const{
      cout << "My name is " << name << endl;
    }

    void HowOldAreYou() const{
      cout << "I am " << age << " years old" << endl;
    }
};

class UnivStudent : public Person{ // Person 클래스의 상속을 의미함
  private:
    char major[50];
  public:
    UnivStudent(char* myname, int myage, char* mymajor) : Person(myage, myname){
      srcpy(major, mymajor);
    }

    void WhoAreYou() const{
      WhatYourName();
      HowOldAreYou();
      cout << "My major is " << major << endl << endl;
    }
};

int main(void){
  UnivStudent ustd1("Lee", 22, "Computer eng.");
  ustd1.WhoAreYou();

  UnivStudent ustd2("Yoon", 21, "Electronic eng.");
  ustd2.WhoAreYou();
  return 0;
}

혹시 이런 궁금증이 생길지도 모르겠다.

UnivStudent 클래스의 멤버함수 (또는 생성자) 내에서는 Person 클래스에 private으로 선언된 멤버변수 age와 name에 접근이 가능한가?

만약에 private의 접근제한이 객체를 기준으로 결정된 거라면 접근이 가능해야 한다. 왜냐하면 UnivStudent의 객체에는 UnivStudent 멤버함수와 Person의 멤버변수가 함께 존재하기 때문이다. 그러나 접근제한의 기준은 클래스 이다. 클래스 외부에서는 private 멤버에 접근이 불가능하다. 따라서 UnivStudent의 멤버함수 내에서는 person의 멤버변수에 직접 접근이 불가능하다.

private로 선언된 멤버는 상속이 되지 않는가?

동일한 객체 내에서도 접근이 불가능하다고 하니, 이렇게 생각할 수도 있다. 그러나 위의 에제에서도 보았듯이 상속은 이루어진다. 그래서 초기화도 가능했고, 해당 정보가 출력이 된 것이다. 다만 직접 접근이 불가능하기 때문에 Person클래스에 정의된 public 함수를 통해 간접적으로 접근해야한다. 즉, 정보의 은닉은 하나의 객체 내에서도 진행이 된다.


용어의 정리


이쯤에서 한차례 용어를 정리할 필요가 있다. 계속해서 ‘상속을 한’ ‘상속을 받은’과 같이 쓸 수는 없다.

Person <-> UnivStudent
상위 클래스 <-> 하위 클래스
기초(bass) 클래스 <-> 유도(derived) 클래스
슈퍼(super) 클래스 <-> 서브(sub) 클래스
부모 클래스 <-> 자식 클래스

여기서는 기초 클래스 혹은 유도 클래스 라는 표현으로 사용한다.


유도 클래스의 객체 생성과정


mobile

이 내용은 상당히 중요하기 때문에 정확히 이해하고 넘어가자. 다음 예제의 실행결과를 통해 유도 클래스의 객체 생성과정을 나름 정리하길 바란다. 참고러 여기서 중요한 것은 기초 클래스의 생성자 호출이다.

#include <iostream>
using namespace std;

class SoBase{
  private:
    int baseNum;
  public:
    SoBase() : baseNum(20){
      cout << "SoBase()" << endl;
    }

    SoBase(int n) : baseNum(n){
      cout << "SoBase(int n)" << endl;
    }    

    void ShowBaseData(){
      cout << baseNum << endl
    }
};

class SoDerived : public SoBase{
  private:
    int derivNum;
  public:
    SoDerived() : derivNum(30){
      cout <<"SODerived()" << endl;
    }

    SoDerived(int n) : derivNum(n){
      cout << "SoDerived(int n)" << endl;
    }

    SoDerived(int n1, int n2) : SoBase(n1), derivNum(n2){
      cout << "SoDerived(int n1, int n2)" << endl;
    }

    void ShowDerivData(){
      ShowBaseData();
      cout << derivNum << endl;
    }
};

int main(void){
  cout << "case1..." << endl;
  SoDerived dr1;
  dr1.ShowDerivData();
  cout << "----------------" << endl;
  cout << "case2..." << endl;
  SoDerived dr2(12);
  dr2.ShowDerivData();
  cout << "----------------" << endl;
  cout << "case3..." << endl;
  SoDerived dr3(23, 24);
  dr3.ShowDerivData();
  return 0;
}

출력은 다음과 같다.

case1...
SoDerived()
20
30
\----------------\
case2...
SoBase()
SoDerived(int n)
20
12
\----------------\
case3...
SoBase(int n)
SoDerived(int n1, int n2)
23
24

이를 통해 우리는 2가지 사실을 알 수 있다.

유도 클래스의 객체생성 과정에서 기초 클래스의 생성자는 100% 호출된다.

유도클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면, 기초 클래스의 void 생성자가 호출된다.

앞에 언급은 안했지만, 유도클래스의 객체생성 과정에서는 생성자가 2번 호출된다. 하나는 기초 클래스 생성자고 또하나는 유도 클래스의 생성자이다. 그럼 main 함수를 보고 객체의 생성과정을 보도록 하자.

SoDerived dr3(23, 24);

기본적으로 메모리 공간이 할당된 다음에 생성자가 호출되어야 하니, 첫번째메모리 공간의 할당이 진행된다.

이어 생성자가 호출되어야 하고, 객체 생성문에 의해 23과 24가 전달되며 유도클래스의 생성자가 호출된다.

생성자가 호출되는데 SoBase 클래스를 상속하고 있는 걸 보고 기초 클래스의 생성자 호출을 위해 이니셜라이저를 살핀다.

SoBase(n1)

이 후, 매개변수 n1으로 전달된 값을 인자로 전달받을 수 있는 SoBase 클래스의 생성자를 호출한다.

이렇게 해서 기초 클래스의 생성자 호출이 완료되고, 이로 인해 기초 클래스의 멤버변수가 먼저 초기화 된다. 그리고 호출은 되었지만 아직 완전히 실행이 끝나지 않은 유도 클래스의 생성자 실행이 완료되면서 유도클래스의 멤버변수도 초기화가 완료된다.

이제 여기까지 왔으면 비로소 객체 라 부를 수 있다.

다소 순서가 복잡해진 것 같지만, 실제로는 기초 클래스의 생성자 호출이 추가된 것이 전부 이다.

그럼 이어서

SoDerived dr1;

을 보자. 먼저 메모리 공간이 할당되고 유도클래스의 void 생성자가 호출된다.

이어서 SoDerived 클래스는 유도 클래스이므로, 이니셜라이저 부분에서 기초 클래스의 생성자 호출과 관련해서 명시된 내용을 찾는다. 하지만 아무런 내용도 명시되어 있지 않으므로 기초 클래스의 void 생성자를 대신 호출한다.

이 후 기초 클래스의 멤버변수 초기화가 완료되고 유도 클래스 생성자의 나머지 부분이 진행되어 객체 가 생성된다.

이렇게 해서 유도 클래스의 객체생성 과정에 대한 설명이 끝났다. 이제 이 사실을 보고 유도 클래스의 객체 생성과정에서도 다음이 지켜짐을 알아야 한다.

클래스의 멤버는 해당 클래스의 생성자를 통해서 초기화해야 한다.

여기까지 생성과정을 살펴 보았고, 이후 유도 클래스의 소멸과정 부터는 다음 포스팅에 적도록 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview