Home [C++ 프로그래밍] 11강 - 상속
Post
Cancel

[C++ 프로그래밍] 11강 - 상속

💡해당 게시글은 방송통신대학교 이병래 교수님의 'C++ 프로그래밍' 강의를 개인 공부 목적으로 메모하였습니다.



학습 개요


  • 상속을 활용하여 기초 클래스와 파생 클래스를 선언함으로써 클래스 계층 구조를 만들었을 경우, 기초 클래스의 포인터는 기초 클래스의 객체뿐만 아니라 해당 클래스 계층 구조에 속하는 파생 클래스들의 객체를 가리키게 할 수 있음
  • 이 때 기초 클래스 포인터가 가리키고 있는 객체가 정확히 어느 클래스의 객체인가에 따라 그 객체에 맞는 동작이 이루어지게 하는 것이 필요함
  • 이러한 클래스 계층 구조에 따른 포인터 활용 및 멤버 함수의 자동 선택이 이루어지게 하는 방법에 대하여 학습함



학습 목표


  • 클래스의 포인터로 객체를 가리켜서 포인터를 통해 객체를 사용할 수 있음
  • 동적 연결을 사용하여 포인터가 가리키는 객체에 맞는 멤버 함수가 자동적으로 선택되어 동작하게 할 수 있음
  • dynamic_cast연산자를 이용하여 안전하게 다운 캐스트를 할 수 있음



주요 용어


  • 정적 연결(static binding)
    • 이름과 그 이름에 해당하는 대상의 연결이 프로그램의 실행이 시작되기 전에 이루어지게 하는 것
  • 동적 연결(dynamic binding)
    • 이름과 그 이름에 해당하는 대상의 연결이 프로그램의 실행되는 동안 결정되게 하는 것
  • 가상 함수(virtual function)
    • 클래스 계층 구조에서 함수를 재 정의 할 때 동적 연결 방식으로 함수가 실행되도록 선언된 함수
  • 업 캐스팅(upcasting)
    • 파생 클래스 포인터를 기초 클래스 포인터로 변환하는 것
  • 다운 캐스팅(downcasting)
    • 기초 클래스 포인터를 파생 클래스 포인터로 변환하는 것



강의록


상속과 포인터

클래스 계층 구조와 포인터

  • 포인터(참조)로 가리킬 수 있는 대상

    image.png

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      int main()
      {
        Person *pPrsn1, *pPrsn2;
        Person dudley("Dudley");
        Student *pStdnt1, *pStdnt2;
        Student harry("Harry", "Hogwarts");
        pPrsn1 = &dudley; // 부모 포인터에 부모 객채 OK
        pStdnt1 = &harry; // 자식 포인터에 자식 객체 OK
        pPrsn2 = &harry; // 부모 포인터에 자식 객체(업캐스팅) OK
        pStdnt2 = &dudley; // 자식 포인터에 부모 객체(다운캐스팅) Error!
        
        return 0;
      }
    

예제: 객체 포인터 배열

  • 객체 포인터의 배열
    • Person 및 Student 클래스의 객체를 가리키는 포인터를 저장하는 배열을 선언하여 객체를 가리키게 하고, 배열에 저장된 객체들을 출력하는 함수를 통해 출력하는 프로그램을 작성하라.

    image.png

    image.png

예제: 객체 포인터 배열 - Person.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef PERSON_H_INCLUDED
#define PERSON_H_INCLUDED
#include <iostream>
#include <string>
using namespace std;

class Person {
  string name;
public:
  Person(const string& n) : name(n) { }
  string getName() const { return name; }
  void print() const { cout << name; }
};
#endif // PERSON_H_INCLUDED

예제: 객체 포인터 배열 - Student.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Person.h"

class Student : public Person {
  string school;
public:
  Student(const string& n, const string& s) :
    Person(n), school(s) { }
  string getSchool() const { return school; }
  void print() const { // print() 함수 오버라이딩
    Person::print(); // 부모 클래스의 print() 함수를 명시적으로 호출
    cout << " goes to " << school;
  }
};

예제: 객체 포인터 배열 - PArrMain.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;

void PrintPerson(const Person * const p[], int n)
{
  for (int i=0; i < n; i++) {
    p[i]->print();
    cout << endl;
  }
}

int main()
{
  Person dudley("Dudley");
  Student harry("Harry", "Hogwarts");
  Student ron("Ron", "Hogwarts");

  dudley.print();      
  cout << endl;
  harry.print();
  cout << endl << endl;

  Person *pPerson[3];
  pPerson[0] = &dudley;
  pPerson[1] = &harry;
  pPerson[2] = &ron;
  PrintPerson(pPerson, 3);

  return 0;
}

// Dudley
// Harry goes to Hogwarts

// Dudley
// Harry
// Ron

가상 함수

객체 포인터와 재정의 된 멤버 함수 호출

image.png

image.png

image.png

정적 연결 - Sbinding.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;

int main()
{
  Person *p1 = new Person("Dudley");
  p1->print(); // Person::print() 호출
  cout << endl;

  Person *p2 = new Student("Harry", "Hogwarts");
  p2->print(); // Person::print() 호출
  cout << endl;

  ((Student *)p2)->print(); // Student::print() 호출 -> p2가 Student 객체를 가리키고 있으리라는 보장이 없음
  cout << endl;
  return 0;
}

// Dudley
// Harry
// Harry goes to Hogwarts

동적 연결

  • 동적 연결(dynamic binding)
    • 객체 포인터를 통해 객체의 멤버 함수를 호출할 경우 포인터가 가리키는 실제 객체가 무엇인 가에 따라 실행 중에 멤버 함수를 결정하는 것
    • C++에서는 가상 함수(virtual function)로 동적 연결을 구현함
    • 기초 클래스에서 가상 함수로 선언한 멤버 함수를 재 정의한 파생 클래스의 함수는 역시 가상 함수이며, 동적 연결이 적용됨
  • 정적 연결로 컴파일하여 동작하는 객체

    1
    2
    3
    4
    5
    
      class Person {
        
        void print() const
          { cout << name; }
      };
    

    image.png

    1
    2
    3
    4
    5
    6
    7
    
      class Student : public Person {
        
        void print() const {
          Person::print();
          cout << " goes to " << school;
        }
      };
    

    image.png

  • 동적 연결로 동작하는 객체

    1
    2
    3
    4
    5
    
      class Person {
        
        virtual void print() const
          { cout << name; }
      };
    

    image.png

    1
    2
    3
    4
    5
    6
    7
    
      class Student : public Person {
        
        void print() const {
          Person::print();
          cout << " goes to " << school;
        }
      };
    

    image.png

예: 동적 연결의 활용 - PArrMain.cpp

1
2
3
4
5
6
7
void PrintPerson(const Person* const p[], int n)
{
  for (int i=0; i < n; i++) {
    p[i]->print();
    cout << endl;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main()
{
  Person dudley("Dudley");
  Student harry("Harry", "Hogwarts");
  Student ron("Ron", "Hogwarts");

  dudley.print();
  cout << endl;
  harry.print();
  cout << endl;

  Person *pPerson[3];
  pPerson[0] = &dudley;
  pPerson[1] = &harry;
  pPerson[2] = &ron;
  PrintPerson(pPerson, 3);

  return 0;
}

// Dudley
// Harry goes to Hogwarts

// Dudley
// Harry goes to Hogwarts
// Ron goes to Hogwarts

소멸자의 동적 연결

  • 소멸자를 가상 함수로 선언하지 않은 경우
    • 기초 클래스의 포인터에 연결된 파생 클래스 객체를 제거할 때 기초 클래스의 소멸자만 동작함
      • 파생 클래스의 소멸자가 동작하지 않아 필요한 작업이 누락 됨
    1
    2
    3
    4
    5
    6
    7
    8
    
      class BaseClass {
        int *ptB;
      public:
        BaseClass(int n) 
          { ptB = new int[n]; }
        ~BaseClass() 
          { delete [] ptB; }
      };
    
    1
    2
    3
    4
    5
    6
    7
    
      class DrvClass : public BaseClass {
        int *ptD;
      public:
        DrvClass(int n1, int n2) : BaseClass(n1)
        { ptD = new int[n2]; }
        ~DrvClass() { delete [] ptD; }
      };
    
    1
    2
    3
    4
    5
    
      BaseClass *pB1 = new BaseClass(5);
      BaseClass *pB2 = new DrvClass(10, 15);
        
      delete pB1; // 기초 클래스의 소멸자 동작
      delete pB2; // 기초 클래스의 소멸자만 동작 (메모리 누수 발생)
    
  • 소멸자를 가상 함수로 선언한 경우
    • 기초 클래스의 포인터에 연결 된 파생 클래스 객체를 제거할 때 파생 클래스의 소멸자가 동작할 수 있게 함
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      class BaseClass {
        int *ptB;
      public:
        BaseClass(int n) 
          { ptB = new int[n]; }
        virtual ~BaseClass() 
          { delete [] ptB; } // virtual 선언
        
      };
    
    1
    2
    3
    4
    5
    6
    7
    
      class DrvClass : public BaseClass {
        int *pD;
      public:
        DrvClass(int n1, int n2) : BaseClass(n1)
        { pD = new int[n2]; }
        ~DrvClass() { delete [] pD; }
      };
    
    1
    2
    3
    4
    5
    
      BaseClass *pB1 = new BaseClass(5);
      BaseClass *pB2 = new DrvClass(10, 15);
        
      delete pB1; // 기초 클래스의 소멸자 동작
      delete pB2; // 파생 클래스와 기초 클래스의 소멸자가 모두 동작
    

업 캐스팅과 다운 캐스팅

  • 업 캐스팅(upcasting)
    • 파생 클래스 포인터를 기초 클래스 포인터로 변환하는 것
    • 묵시적 형 변환을 통해 업 캐스팅을 할 수 있음
  • 다운 캐스팅(downcasting)
    • 기초 클래스 포인터를 파생 클래스 포인터로 변환하는 것
    • 묵시적 형 변환을 할 수 없으며, 형 변환 연산자로 명시적 형 변환을 해야 함
    1
    2
    3
    4
    5
    6
    
      Person *pPrsn1 = new Person("Dudley");
      Student *pStdnt1 = new Student("Harry", "Hogwarts");
        
      Person *pPrsn2 = pStdnt1; // upcasting
        
      Student *pStdnt2 = pPrsn2; // downcasting - Error(묵시적 형 변환 불가)
    

    image.png

  • static_cast연산자를 사용한 다운 캐스팅
    • Person 클래스의 포인터 pPrsn2가 Student 클래스의 객체를 가리키고 있었기 때문에 정상적인 동작을 함

      1
      2
      3
      4
      5
      
        Person *pPrsn1 = new Person("Dudley");
        Student *pStdnt1 = new Student("Harry", "Hogwarts");
        Person *pPrsn2 = pStdnt1; // upcasting
        Student *pStdnt2 = static_cast<Student*>(pPrsn2);
        cout << pStdnt2->getSchool() << endl;
      
    • pPrsn2가 Student 클래스의 객체를 가리키고 있지 않다면 부적절한 변환을 하게 되는 문제가 있음

      1
      2
      3
      4
      5
      
        Person *pPrsn1 = new Person("Dudley");
        Student *pStdnt1 = new Student("Harry", "Hogwarts");
        Person *pPrsn2 = pPrsn1;
        Student *pStdnt2 = static_cast<Student*>(pPrsn2);
        cout << pStdnt2->getSchool() << endl; // 부적절한 호출
      
      • static_cast연산자를 사용하여 다운 캐스팅을 하는 것은 부적절한 변환의 위험이 있음
  • dynamic_cast연산자를 사용한 다운 캐스팅
    • dynamic_cast는 pPrsn1이 가리키는 대상이 Student 객체가 아니므로 nullptr을 반환함

      1
      2
      3
      4
      5
      6
      
        Person *pPrsn1 = new Person("Dudley");
        Student *pStdnt1 = new Student("Harry", "Hogwarts");
        Person *pPrsn2 = pPrsn1; // upcasting
        Student *pStdnt2 = dynamic_cast<Student*>(pPrsn1); // nullptr
        if (pStdnt2)
            cout << pStdnt2->getSchool() << endl;
      
    • pPrsn2가 Student 객체를 가리키고 있으므로 dynamic_cast로 정상적인 다운 캐스팅이 됨

      1
      2
      3
      4
      5
      6
      
        Person *pPrsn1 = new Person("Dudley");
        Student *pStdnt1 = new Student("Harry", "Hogwarts");
        Person *pPrsn2 = pStdnt1; // upcasting
        Student *pStdnt2 = dynamic_cast<Student*>(pPrsn2);
        if (pStdnt2)
            cout << pStdnt2->getSchool() << endl;
      
      • dynamic_cast를 사용하려면 클래스 선언문에 가상 함수를 포함하고 있어야 함

심화 학습

override

  • 가상 함수의 재정의
    • virtual은 기초 클래스의 가상 함수를 재 정의할 때는 지정할 필요가 없으며, 파생 클래스에서 새롭게 가상 함수를 선언하고자 할 때만 사용하는 것이 좋음
    • 파생 클래스에서 어떤 함수가 가상 함수인지 명확히 알기 어려울 수 있으므로, 가상 함수를 재 정의함을 명시적으로 알리고자 할 때에는 override를 지정함
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      class A {
        
        virtual void f() {
        }
      };
      class B : public A {
        
        void f() {
        }
          
        virtual void g() {
        }
      };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      class A {
        
        virtual void f() {
        }
      };
      class B : public A {
        
        void f() override {
        }
          
        virtual void g() {
        }
      };
    

final

  • 가상 함수의 재정의 금지
    • 가상 함수를 더 이상 재 정의 하지 못하게 하려면 final을 지정함
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      class A {
        
        virtual void f() {
        }
      };
        
      class B : public A {
        
        void f() override final { // final 지정으로 재정의 금지
        } 
      };
        
      class C : public B {
        
        void f() override { // error!
        } 
      };
    



연습 문제


  1. DrvClass가 BaseClass의 파생 클래스이고, 이 클래스들의 객체와 포인터가 위 지문과 같이 정의 되었을 때 사용할 수 없는 문장을 모두 나열한 것은?

    1
    2
    3
    4
    5
    6
    7
    8
    
     BaseClass baseObj;
     DrvClass drvObj;
     BaseClass* basePt;
     DrvClass* drvPt;
     basePt = &baseObj; // ㈀
     basePt = &drvObj; // ㈁
     drvPt = &baseObj; // ㈂
     drvPt = &drvObj; // ㈃
    

    a. drvPt = &baseObj;

    • 기초 클래스 포인터는 파생 클래스 객체를 가리킬 수 있으나, 파생 클래스 포인터는 기초 클래스 객체를 가리킬 수 없음
  2. 클래스 B과 D에 대해 위 지문 문장의 실행 결과 출력 되는 결과는?

    1
    2
    3
    4
    5
    6
    7
    
     class B {
     public:
       virtual void f() {
         cout << "B ";
       }
          
     };
    
    1
    2
    3
    4
    5
    6
    7
    
     class D : public B {
     public:
       void f() {
         cout << "D ";
       }
    
     };
    
    1
    2
    3
    4
    
     D *pD = new D;
     B *pB = pD;
     pB->f();
     pD->f();
    

    a. D D

    • f()를 가상 함수로 선언하였으므로 포인터에 연결된 객체가 무엇인가에 따라 함수가 선택되어 실행 됨
  3. DrvClass가 BaseClass의 파생 클래스이고, drvPt와 basePt가 각각 DrvClass와 BaseClass의 포인터라고 하자. 위 지문의 문장을 실행하였을 때 만일 basePt가 BaseClass의 객체를 가리키고 있다면 nullptr이 drvPt에 저장되도록 하려면 ㈀에 무엇을 넣어야 하는가?

    1
    
     drvPt = // ____㈀____(basePt);
    

    a. dynamic_cast<DrvClass*>

    • dynamic_cast는 포인터를 다운 캐스팅 할 때 만일 형 변환이 안전하게 일어날 수 없을 때는 nullptr를 반환함으로써 포인터를 오용 할 가능성을 예방할 수 있게 함
  4. 위 지문은 클래스 A의 소멸자를 선언하는 문장의 일부이다. 클래스 B가 A의 파생 클래스이고, A의 포인터 pA가 동적 할당된 B의 객체를 가리키고 있다. delete연산자로 pA가 가리키고 있는 객체를 반납할 때 클래스 B의 소멸자가 동작하게 하려면 공란에 어떤 단어를 넣어야 하는가?

    1
    2
    3
    4
    5
    6
    7
    
     class A {
    
     // ___________~A() {
    
       }
    
     };
    

    a. virtual

    • 소멸자를 가상 함수로 선언함으로써 포인터에 연결된 객체에 맞는 소멸자가 동작하도록 함



정리 하기


  • 클래스 계층 구조에서 기초 클래스의 포인터는 해당 클래스의 객체 뿐 아니라 파생 클래스의 객체도 가리킬 수 있음
    • 그러나 파생 클래스의 포인터로 기초 클래스의 객체를 가리키게 하면 안 됨
  • 파생 클래스에서 재 정의하는 함수를 기초 클래스에서 가상 함수로 선언하면 기초 클래스의 포인터에 연결된 객체에 따라 해당 함수를 선택하여 동작하게 하는 동적 연결을 할 수 있음
  • 클래스의 계층 구조에서 클래스의 소멸자는 가상 함수로 선언하여 동적 연결에 따라 소멸자가 동작할 수 있게 함
  • 포인터의 업 캐스팅은 묵시적 형 변환을 할 수 있으나, 다운 캐스팅은 명시적으로 형 변환을 지정해야 함
  • 가상 함수를 포함하고 있는 클래스의 경우 dynamic_cast연산자를 이용하여 더 안전하게 다운 캐스팅을 할 수 있음

[C++ 프로그래밍] 10강 - 상속

[C++ 프로그래밍] 12강 - 상속