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

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

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



학습 개요


  • 상속은 일반화와 특수화를 통해 계층적으로 클래스를 선언함으로써 추상화를 하는 유용한 기능
  • 기초 클래스의 일반적 속성 및 메소드는 파생 클래스들에 상속 됨으로써 파생 클래스들에 공통적으로 필요한 속성 및 메소드를 중복적으로 프로그래밍 하지 않아도 됨
  • 기초 클래스와 파생 클래스로 구성되는 클래스 계층 구조를 구현하는 클래스를 선언하고 파생 클래스의 객체를 통해 기초 클래스에 포함된 멤버를 활용하는 방법에 대하여 학습함
  • 심화 학습 주제로 final을 이용하여 파생 클래스를 더 이상 선언하지 못하게 막는 방법과 동일 이름이 선언된 영역에 따라 어떻게 사용할 수 있는지 학습함



학습 목표


  • 계층 관계로 표현되는 대상들을 기초 클래스와 파생 클래스로 표현할 수 있음
  • 기초 클래스의 멤버를 파생 클래스에서 어떻게 사용할 수 있는지 가시성과 연관 지어 설명할 수 있음
  • 기초 클래스와 파생 클래스의 생성자 및 소멸자가 어떻게 실행되는지 설명할 수 있음
  • 파생 클래스의 멤버로서 기초 클래스로부터 상속된 멤버의 가시성을 지정할 수 있음



주요 용어


  • 상속(inheritance)
    • 상위 클래스가 갖는 일반적 속성 및 행위를 하위 클래스가 이어 받게 하는 기능
  • 기초 클래스(base class)
    • 클래스 계층 구조에서 일반적 속성 및 메소드를 포함하는 상위 클래스
  • 파생 클래스(derived class)
    • 클래스 계층 구조에서 상위 클래스의 일반적 개념을 상속 받으면서, 해당 클래스에 특수한 속성 및 메소드가 추가된 하위 클래스
  • 클래스 멤버의 가시성 (visibility)
    • 클래스의 멤버가 외부에 공개되는 범위
  • 멤버 함수 재정의(overriding)
    • 기초 클래스에 정의된 멤버 함수와 동일한 멤버 함수를 파생 클래스에서 그것에 맞게 다시 정의하는 것
  • 이름 은폐(name hiding)
    • 어떠한 영역에 선언된 이름을 그 영역에 내포 된 영역에서 다시 선언하면 내포 된 영역에서는 바깥 영역의 이름이 은폐 되는 것



강의록


기초 클래스와 파생 클래스

클래스의 상속

  • 공통적인 멤버를 포함하는 유사한 유형의 클래스

    image.png

  • 일반화와 특수화를 통한 클래스 계층 구조 설계

    image.png

파생 클래스의 선언

  • 파생 클래스 선언 형식

    1
    2
    3
    4
    5
    6
    
      class DClassName : visibilitySpec BClassName {
        visibilitySpec_1:
          데이터 멤버 또는 멤버 함수 리스트;
        visibilitySpec_2:
          데이터 멤버 또는 멤버 함수 리스트;
      };
    
    • DClassName
      • 파생 클래스 이름
    • BClassName
      • 기초 클래스 이름
    • visibilitySpec
      • 가시성 지시어

Person 클래스와 Student 클래스

  • Person 클래스
    • 사람을 나타내는 클래스를 선언하고자 한다.
    • 사람 객체는 이름을 가지고 있으며, 이름을 지정하거나 이름을 알릴 수 있다.
    • ex) Dudley
  • Student 클래스
    • 학생을 나타내는 클래스를 선언하고자 한다.
    • 학생 객체는 사람의 기능을 상속 받으면서, 학교 이름을 지정하거나 저장된 학교 이름을 알리는 기능이 추가된다.
    • ex) Harry goes to Hogwarts
  • 클래스 구조

    image.png

Person 클래스의 선언 - Person1.h

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

class Person {
public:
  string name;
  void setName(const string& n) { name = n; }
  string getName() const { return name; }
  void print() const { cout << name; }
};

#endif // PERSON1_H_INCLUDED

Student 클래스의 선언 - Student1.h

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

// 파생 클래스 Student를 Person을 상속 받아 선언함
class Student : public Person {
public:
  string school;
  void setSchool(const string& s) { school = s; }
  string getSchool() const { return school; }
  void print() const {
    Person::print();
    cout << " goes to " << school;
  }
};

Person 및 Student 객체의 사용

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
#include <iostream>
#include "Person1.h"
#include "Student1.h"
using namespace std;

int main() {
  Person dudley; // 기초 클래스의 객체 선언
  dudley.setName("Dudley"); // 기초 클래스의 함수 호출
  Student harry; // 파생 클래스의 객체 선언
  harry.setName("Harry"); // 파생 클래스의 함수 호출
  harry.setSchool("Hogwarts"); // 파생 클래스의 함수 호출
  dudley.print(); // 기초 클래스의 함수 호출
  cout << endl;
  harry.print(); // 파생 클래스의 함수 호출
  cout << endl;
  dudley.print(); // 기초 클래스의 함수 호출
  cout << endl;
  harry.Person::print(); // 기초 클래스의 함수 호출
  cout << endl;
  return 0;
}

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

클래스 계층

image.png

파생 클래스의 생성자 및 소멸자

파생 클래스의 생성자 및 소멸자

  • 생성자 선언 형식

    1
    2
    3
    4
    
      DClassName(fParameterList) : BClassName(bArgsList)
      {
        // 파생 클래스 생성자에서 추가되는 사항
      }
    
    • DClassName
      • 파생 클래스 생성자
      • 파생 클래스 이름을 사용
    • BClassName
      • 기초 클래스 생성자
      • 기초 클래스 이름을 사용
    • fParameterList
      • 파생 클래스 생성자 형식 매개 변수 목록
    • bArgsList
      • 기초 클래스 생성자에 전달할 인수 목록
  • 생성자 및 소멸자의 실행 순서

    • 생성자
      • 기초 클래스 생성자 → 파생 클래스 생성자
      • 파생 클래스는 기초 클래스의 내용을 바탕으로 하고 있음
        • 객체의 기초 클래스 해당 내용이 먼저 준비 된 후 파생 클래스에 선언된 내용을 초기화할 필요가 있음
    • 소멸자
      • 파생 클래스 소멸자 → 기초 클래스 소멸자
      • 기초 클래스의 속성이 제거되기 전에 이를 활용할 가능성이 있는 파생 클래스 객체를 제거해야 함

Person 클래스와 Student 클래스 (생성자/소멸자 추가)

image.png

Person 클래스의 선언 - Person2.h

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
    string name;
public:
  Person(const string& n) {
    cout << "Person의 생성자" << endl;
    name = n;
  }
  ~Person() {
    cout << "Person의 소멸자" << endl;
  }
  string getName() const { return name; }
  void print() const { cout << name; }
};

Student 클래스의 선언 - Student2.h

1
2
3
4
5
6
7
8
9
10
11
class Student : public Person {
  string school;
public:
  Student(const string& n, const string& s) : Person(n) {
    cout << "Student의 생성자" << endl;
    school = s;
  }
  ~Student() {
    cout << "Student의 소멸자" << endl;
  }
};

생성자 및 소멸자 동작 - Student2.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "Person2.h"
#include "Student2.h"
using namespace std;

int main() {
  Student harry("Harry", "Hogwarts");
  cout << harry.getName() << " goes to " << harry.getSchool() << endl;
  return 0;
}

// Person의 생성자
// Student의 생성자
// Harry goes to Hogwarts
// Student의 소멸자
// Person의 소멸자

엑세스 제어

가시성

  • 가시성 지시어

    가시성 지시어공개 범위
    private(디폴트)소속 클래스의 멤버 함수
     친구 클래스의 멤버 함수 및 친구 함수
    protected소속 클래스의 멤버 함수
     친구 클래스의 멤버 함수 및 친구 함수
     파생 클래스의 멤버 함수
     파생 클래스의 친구 클래스의 멤버 함수 및 친구 함수
    public전 범위

가시성의 상속

  • 기초 클래스로부터 상속 받은 멤버의 가시성

    1
    2
    3
    
      class DClassName : visibilitySpec BClassName {
        
      };
    
    • visibilitySpec
      • 기초 클래스로부터 상속 된 멤버가 파생 클래스의 멤버로서 가지게 되는 가시성을 제어함
      • private, protected, public
      • class를 선언할 때는 private이 디폴트
      • struct를 선언할 때는 public이 디폴트
    • visibilitySpec에 지시된 것이 가시성의 상한이 되도록 제한됨

가시성의 상속

  • 기초 클래스로부터 상속 받은 멤버의 가시성

    가시성 상속 지시어B의 public멤버는B의 protected멤버는
    class D1 : private B {}D1의 private멤버D1의 private멤버
    class D2 : protected B {}D2의 protected멤버D2의 protected멤버
    class D3 : public B {}D3의 public 멤버D3의 protected멤버
  • 기초 클래스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      class BaseC {
      private:
        int a;
      protected:
        int b;
      public:
        int c;
        int geta() const {
          return a;
        }
        void set(int x, int y, int z) {
          a = x; b = y; c = z;
        }
      };
    
  • public파생 클래스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      class Drvd1 : public BaseC {
      public:
        int sum() const {
          return geta() + b + c;
        }
        void printbc() const {
          cout << "b << " << b << ", c " << c;
        }
      };
    
  • 객체 사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      // 객체 사용
      int main() {
        Drvd1 d1;
        // d1.a = 1; // 에러(private 멤버)
        // d1.b = 2; // 에러(protected 멤버)
        d1.c = 3; // OK
        d1.set(10, 20, 30); // 값 설정
        d1.printbc(); // b와 c 출력
        return 0;
      }
    
  • protected파생 클래스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      class Drvd1 : protected BaseC {
      public:
        int sum() const {
          return geta() + b + c;
        }
        void printbc() const {
          cout << "b << " << b << ", c " << c;
        }
      };
    
  • 객체 사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      // 객체 사용
      int main() {
        Drvd1 d1;
        // d1.a = 1; // 에러(private 멤버)
        // d1.b = 2; // 에러(protected 멤버)
        // d1.c = 3; // 에러(protected 멤버) 
        // d1.set(10, 20, 30); // 값 설정 (protected)
        d1.printbc(); // b와 c 출력 (public)
        return 0;
      }
    

심화 학습

final클래스

  • 파생 클래스 선언의 금지
    • final클래스로 선언된 클래스는 파생 클래스를 더 이상 정의할 수 없음

      1
      2
      3
      4
      
        class A { ...... };
        class B : public A { ...... };
        class C : final public B { ...... };
        class D : public C { ...... }; // 에러
      
      • final은 키워드가 아닌 식별자(identifier)임
        • final클래스 지정과 같이 특별히 정해진 위치에 사용되지 않은 경우 식별자(예: 변수 이름)의 용도로 사용할 수도 있음(바람직한 용법은 아님)

이름 은폐

  • 이름 은폐(name hiding)
    • 어떤 영역에 선언된 이름을 그 영역에 내포된 영역에서 다시 선언하면 내포된 영역에서는 바깥 영역의 이름이 은폐 됨

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      
        void f(int x) {
          cout << "f(int x) --> " << x << endl;
        }
              
        void f(double x) {
          cout << "f(double x) --> " << x << endl;
        }
              
        int main() {
          f(10); // int
          f(20.0); // double
        }
              
        // f(int x) --> 10
        // f(double x) --> 20
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
        void f(int x) {
          cout << "f(int x) --> " << x << endl;
        }
              
        void f(double x) {
          cout << "f(double x) --> " << x << endl;
        }
              
        int main() {
        	void f(int x);
          f(10); // int
          f(20.0); // int
          ::f(30.0); // double
        }
              
        // f(int x) --> 10
        // f(int x) --> 20
        // f(double x) --> 30
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
        void f(int x) {
          cout << "f(int x) --> " << x << endl;
        }
              
        void f(const char *x) {
          cout << "f(const char *x) --> " << x << endl;
        }
        int main() {
          void f(int x);
          f(10); // void f(int x);
          f("abc"); // 에러
          ::f("abc"); // void f(const char* x)
        }
      
  • 클래스 상속에서의 이름 은폐
    • 기초 클래스에 선언된 이름을 파생 클래스에서 재정의하면 파생 클래스의 객체에서 기초 클래스의 이름이 은폐 됨

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
        class A {
        public:
          void f(int x) { cout << "A::f() --> " << x << endl; }
        };
              
        class B : public A {
        public:
          void f(double x) { cout << "B::f() --> " << x << endl; }
        };
              
        int main() {
          B objB;
          objB.f(10.0);
          objB.f(20);
        }
              
        // B::f() --> 10
        // B::f() --> 20
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
        class A {
        public:
          void f(int x) { cout << "A::f() --> " << x << endl; }
        };
              
        class B : public A {
        public:
          using A::f; // A 클래스의 f도 보이게 만듦
          void f(double x) { cout << "B::f() --> " << x << endl; }
        };
              
        int main() {
          B objB;
          objB.f(10.0);
          objB.f(20);
        }
              
        // B::f() --> 10
        // A::f() --> 20
      



연습 문제


  1. 다음 중 클래스 계층 구조를 설계할 경우 나머지 클래스의 기초 클래스가 될 수 있는 것은?

    a. 자동차

    • 기초 클래스는 일반적인 개념, 파생 클래스는 기초 클래스의 성질을 공유하면서 개개의 특수한 성질을 갖는 하위 개념을 정의할 수 있어야 함
  2. 위 지문과 같이 클래스가 선언 되었을 때 ㈀에서 사용할 수 없는 문장은?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
     class A {
       char addr[20];
     protected:
       char name[10];
     public:
       void printA() { cout << name << addr; }
     };
        
     class B : public A {
       char dep[10];
     public:
        
       void f() // { (ㄱ) }
     };
    

    a. cout << addr;

    • 파생 클래스의 멤버 함수에서는 기초 클래스의 protected멤버와 public멤버를 직접 사용할 수 있으며, private멤버인 addr은 직접 액세스 할 수 있음
    • 사용할 수 있는 문장
      • cin >> name;
      • cin >> dep;
      • printA();
  3. 위 지문의 ㈀은 클래스 B의 생성자를 선언하는 문장이다. 두 개의 정수를 인수로 전달 받아 첫 번째 인수는 A의 a, 두 번째 인수는 B의 b에 초기화한다. ㈀에 넣을 내용은 무엇인가?

    1
    2
    3
    4
    5
    6
    7
    8
    
     class A {
       int a;
     public:
       A(int x) : a(x) {}
       void display() const {
         cout << a;
       }
     };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     class B : public A {
       int b;
     public:
       // (ㄱ)
       void display() const {
         A::display();
         cout << " " << b;
       }
     };
    

    a. B(int x, int y) : A(x), b(y) {}

    • 생성자의 초기화 리스트로 기초 클래스의 생성자가 호출 되도록 함
  4. 소멸자에 대한 올바른 설명은?

    a. 파생 클래스 객체가 제거될 때는 파생 클래스 소멸자가 실행된 후 기초 클래스의 소멸자가 실행된다.

    • 파생 클래스 객체는 기초 클래스를 바탕으로 만들어지므로, 기초 클래스의 속성이 정리되기 전에 이를 활용할 가능성이 있는 파생 클래스 객체의 정리 작업이 이루어져야 함
  5. 위 지문과 같이 클래스 A와 B가 선언되었다. 가시성을 고려하였을 때 ㈀과 ㈁에서 사용할 수 있는 문장을 나열한 것은?

    1
    2
    3
    4
    5
    6
    7
    8
    
     class A {
       int a1;
     protected:
       int a2;
     public:
       int a3;
        
     };
    
    1
    2
    3
    4
    5
    6
    7
    8
    
     class B : protected A {
       int b1;
     protected:
       int b2;
     public:
       int b3;
       int f() // { (ㄱ) }
     };
    
    1
    2
    3
    4
    
     int main() {
       B objB;
       // (ㄴ)
     }
    

    a. ㈀ cin >> a2;   ㈁ objB.f( );

    • B의 멤버 함수에서는 A의 protected멤버와 public멤버, B의 모든 멤버를 직접 액세스할 수 있음
    • 클래스 B는 가시성 상속 지시어 protected로 A로부터 상속을 받으므로 A의 protectedpublic멤버는 클래스 B의 protected멤버가 됨



정리 하기


  • 클래스의 계층 구조에서 기초 클래스는 일반적 속성 및 메소드를 파생 클래스는 특수한 속성 및 메소드를 포함
  • 기초 클래스의 protected멤버는 파생 클래스에서 액세스할 수 있음
  • 파생 클래스의 객체가 생성 될 때 기초 클래스의 생성자가 먼저 실행 된 후 파생 클래스의 생성자 몸체 블록이 실행
  • 파생 클래스 객체가 소멸될 때는 파생 클래스 생성자 몸체 블록이 먼저 실행된 후 기초 클래스의 생성자가 실행
  • 기초 클래스로부터 상속 받은 멤버의 가시성은 가시성 상속 지시어에 의해 공개 범위의 상한이 결정 됨
  • final로 선언된 클래스는 파생 클래스를 더 이상 정의할 수 없음

[C++ 프로그래밍] 9강 - 연산자 다중 정의

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