Skip to content

15. OOP 상속성(OOP Inheritance)


1. 키워드

  • OOP 상속성(OOP Inheritance)
  • 클래스 상속(Class Inheritance)
  • 기초 클래스(Base Class), 부모 클래스(Parent Class), 상위 클래스(Super Class)
  • 파생 클래스(Derived Class), 자식 클래스(Child Class), 하위 클래스(Sub Class)
  • 오버라이딩(Overriding)
  • 가상 함수(Virtual Function)와 virtual 키워드
  • 단일 상속(Single Inheritance)과 다중 상속(Multiple Inheritance)


2. 파생 클래스

1) 상속

  • 상속은 추상화, 캡슐화와 더불어 OOP를 구성하는 중요한 특징 중 하나이다.
  • 상속은 사용자에게 높은 수준의 코드 재활용성을 제공하며, 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련한다.


2) 클래스 상속

  • C++에서 클래스 상속이란 기존에 정의되어 있는 클래스의 모든 멤버 변수와 멤버 함수를 물려받아, 새로운 클래스를 작성하는 것을 의미한다.
  • 이때 기존에 정의되어 있던 클래스를 기초 클래스 또는 부모 클래스, 상위 클래스라고도 한다.
  • 그리고 상속을 통해 새롭게 작성되는 클래스를 파생 클래스 또는 자식 클래스, 하위 클래스라고도 한다.
  • 이와 같은 클래스의 상속은 다음과 같은 장점을 가진다.


1] 기존에 작성된 클래스를 재활용할 수 있다.

2] 공통적인 부분은 기초 클래스에 미리 작성하여, 파생 클래스에서 중복되는 부분을 제거할 수 있다.


3) 파생 클래스

  • 파생 클래스란 기초 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 가리킨다.


  • C++에서 파생 클래스는 다음과 같은 문법을 통해 선언한다.


class 파생클래스이름 : 접근제어지시자 기초클래스이름[, 접근제어지시자 기초클래스이름, ...]
{
    // 파생 클래스 멤버 리스트
}


  • 접근 제어 지시자는 기초 클래스의 멤버를 사용할 수 있는 파생 클래스의 접근 제어 권한을 설정한다.
  • 이때 접근 제어 지시자를 생략하면, 파생 클래스의 접근 제어 권한은 private으로 기본 설정된다.
  • 또한 ,(콤마)를 사용하여 상속 받을 기초 클래스를 여러 개 명시할 수 있다.
  • 이때 파생 클래스가 상속 받는 기초 클래스가 하나이면 단일 상속이라고 한다.
  • 만약 여러 개의 기초 클래스를 상속 받으면 다중 상속이라고 한다.


4) 파생 클래스의 특징

  • C++에서 파생 클래스는 다음과 같은 특징을 가지고 있다.


1] 파생 클래스는 반드시 자신만의 생성자를 작성해야 한다.

2] 파생 클래스에는 기초 클래스의 접근할 수 있는 모든 멤버 변수들이 저장된다.

3] 파생 클래스는 기초 클래스의 접근할 수 있는 모든 멤버 함수를 사용할 수 있다.

4] 파생 클래스에는 필요한 만큼 멤버를 추가할 수 있다.


  • 우선 다음과 같은 Person 클래스가 미리 작성되어 있다고 가정한다.


class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    void ShowPersonInfo();
};

...

Person::Person(const string &name, int age) {  // 기초 클래스 생성자의 정의
    name_ = name;
    age_ = age;
}

void Person::ShowPersonInfo() {
    cout << name_ << "의 나이는 "
         << age_ << "살입니다." << endl;
}


  • 다음은 기존에 작성된 Person 클래스를 상속받는 Student 클래스를 작성하는 것이다.


class Student : public Person {
   private:
    int student_id_;

   public:
    Student(int sid, const string &name, int age);  // 파생 클래스 생성자의 선언
};

...

Student::Student(int sid, const string &name, int age) : Person(name, age) {  // 파생 클래스 생성자의 선언
    student_id_ = sid;
}


  • 위의 코드에서 보듯이 파생 클래스의 생성자는 기초 클래스의 생성자를 사용하고 있다.
  • 그 이유는 파생 클래스의 생성자가 기초 클래스의 private 멤버에 접근할 수 없기 때문이다.
  • 따라서 기초 클래스의 생성자를 사용해야만 기초 클래스의 private 멤버를 초기화할 수 있다.
  • 이때 기초 클래스의 생성자를 명시하지 않으면, 프로그램은 기초 클래스의 디폴트 생성자를 사용하게 된다.


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

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    void ShowPersonInfo();
};

class Student : public Person {
   private:
    int student_id_;

   public:
    Student(int sid, const string &name, int age);  // 파생 클래스 생성자의 선언
};

int main(void) {
    Student hong = Student(123456789, "길동", 29);
    hong.ShowPersonInfo();

    return 0;
}

Person::Person(const string &name, int age) {  // 기초 클래스 생성자의 정의
    name_ = name;
    age_ = age;
}

void Person::ShowPersonInfo() {
    cout << name_ << "의 나이는 "
         << age_ << "살입니다." << endl;
}

Student::Student(int sid, const string &name, int age) : Person(name, age) {  // 파생 클래스 생성자의 선언
    student_id_ = sid;
}

// 길동의 나이는 29살입니다.


  • 만약 다음과 같이 기초 클래스에 대한 접근 제어 지시자가 private인 경우, 파생 클래스에서는 기초 클래스의 어떠한 멤버에도 접근할 수 없다.


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

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    int test_person_ = 10;
    void ShowPersonInfo();
};

class Student : private Person {
   private:
    int student_id_;

   public:
    Student(int sid, const string &name, int age);  // 파생 클래스 생성자의 선언
    int test_student_ = 20;
};

int main(void) {
    Student hong = Student(123456789, "길동", 29);
    // 기초 클래스의 private 영역이므로 접근 불가
    // cout << hong.name_ << endl;
    // 기초 클래스의 public 영역이어도 접근 불가
    // cout << hong.test_person_ << endl;
    // 파생 클래스의 private 영역이므로 접근 불가
    // cout << hong.student_id_ << endl;
    // 파생 클래스의 public 영역이므로 접근 가능
    cout << hong.test_student_ << endl;

    return 0;
}

Person::Person(const string &name, int age) {  // 기초 클래스 생성자의 정의
    name_ = name;
    age_ = age;
}

void Person::ShowPersonInfo() {
    cout << name_ << "의 나이는 "
         << age_ << "살입니다." << endl;
}

Student::Student(int sid, const string &name, int age) : Person(name, age) {  // 파생 클래스 생성자의 선언
    student_id_ = sid;
}

// 20


5) 파생 클래스의 객체 생성 순서

  • 다음 그림은 파생 클래스의 생성자가 호출되면 수행되는 동작을 설명하고 있다.


001


  • C++에서 파생 클래스의 객체가 생성되는 순서는 다음과 같다.


1] 파생 클래스의 객체를 생성하면, 프로그램은 제일 먼저 기초 클래스의 생성자를 호출한다.

2] 이때 기초 클래스 생성자는 상속 받은 멤버 변수의 초기화를 진행한다.

3] 그리고서 파생 클래스의 생성자가 호출된다.

4] 반대로 파생 클래스의 수명이 다하면, 먼저 파생 클래스의 소멸자가 호출되고, 그 후에 기초 클래스의 소멸자가 호출된다.


3. 멤버 함수 오버라이딩

1) 오버라이딩

  • 함수 오버로딩(Overloading)이란 서로 다른 시그니처를 가지는 여러 함수를 같은 이름으로 정의하는 것이었다.
  • 함수 오버라이딩이란 이미 정의된 함수를 무시하고, 같은 이름의 함수를 새롭게 정의하는 것이라고 할 수 있다.


2) 멤버 함수 오버라이딩

  • C++에서 파생 클래스는 상속을 받을 때 명시한 접근 제어 권한에 맞는 기초 클래스의 모든 멤버를 상속 받는다.
  • 이렇게 상속 받은 멤버 함수는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용할 수도 있다.
  • 오버라이딩이란 멤버 함수의 동작만 재정의하는 것이므로, 함수의 원형은 기존 멤버 함수의 원형과 같아야 한다.
  • C++에서는 다음과 같은 방법을 통해 멤버 함수를 오버라이딩할 수 있다.


1] 파생 클래스에서 직접 오버라이딩하는 방법

2] 가상 함수를 이용해 오버라이딩하는 방법


3) 파생 클래스에서의 오버라이딩

  • C++에서는 파생 클래스에서 상속 받은 기초 클래스의 멤버 함수를 직접 재정의할 수 있다.


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

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    void ShowPersonInfo();
};

class Student : public Person {
   private:
    int student_id_;

   public:
    Student(int sid, const string &name, int age);  // 파생 클래스 생성자의 선언
    void ShowPersonInfo();                          // 파생 클래스에서 상속 받은 멤버 함수의 재정의
};

int main(void) {
    Person lee = Person("순신", 35);
    lee.ShowPersonInfo();

    Student hong = Student(123456789, "길동", 29);
    hong.ShowPersonInfo();

    return 0;
}

Person::Person(const string &name, int age) {  // 기초 클래스 생성자의 정의
    name_ = name;
    age_ = age;
}

void Person::ShowPersonInfo() {
    cout << name_ << "의 나이는 "
         << age_ << "살입니다." << endl;
}

Student::Student(int sid, const string &name, int age) : Person(name, age) {  // 파생 클래스 생성자의 선언
    student_id_ = sid;
}

void Student::ShowPersonInfo() {
    cout << "이 학생의 학번은 "
         << student_id_ << "입니다." << endl;
}

// 순신의 나이는 35살입니다.
// 이 학생의 학번은 123456789입니다.


  • 위의 코드에서는 Person 클래스의 ShowPersonInfo() 멤버 함수를 Student 클래스에서 상속 받아 재정의하고 있다.
  • 그리고서 Student 객체에서 ShowPersonInfo() 멤버 함수를 호출하면 기초 클래스인 Person 클래스의 멤버 함수가 아닌 재정의한 멤버 함수가 호출됨을 알 수 있다.


  • 또한, ::(범위 지정 연산자)를 사용하면 파생 클래스에서 기초 클래스의 원래 멤버 함수를 호출할 수도 있다.


Student hong = Student(123456789, "길동", 29);
hong.ShowPersonInfo();
hong.Person::ShowPersonInfo();

// 이 학생의 학번은 123456789입니다.
// 길동의 나이는 29살입니다.


  • 위의 코드에서 Student 객체는 자신이 재정의한 ShowPersonInfo() 함수뿐만 아니라 기초 클래스의 원래 함수도 호출하고 있다.


4) 파생 클래스에서 오버라이딩의 문제점

  • 위와 같이 파생 클래스에서 멤버 함수를 직접 재정의하는 방법은 일반적인 상황에서는 잘 동작한다.
  • 하지만 이 방법은 다음 코드처럼 포인터 변수를 사용할 때, 예상하지 못한 결과를 반환할 수도 있다.


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

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    void ShowPersonInfo();
};

class Student : public Person {
   private:
    int student_id_;

   public:
    Student(int sid, const string &name, int age);  // 파생 클래스 생성자의 선언
    void ShowPersonInfo();                          // 파생 클래스에서 상속 받은 멤버 함수의 재정의
};

int main(void) {
    Person *ptr_person;  // Person 타입을 기준으로 함수를 호출함
    Person lee = Person("순신", 35);
    Student hong = Student(123456789, "길동", 29);

    ptr_person = &lee;
    ptr_person->ShowPersonInfo();
    ptr_person = &hong;
    ptr_person->ShowPersonInfo();

    return 0;
}

Person::Person(const string &name, int age) {  // 기초 클래스 생성자의 정의
    name_ = name;
    age_ = age;
}

void Person::ShowPersonInfo() {
    cout << name_ << "의 나이는 "
         << age_ << "살입니다." << endl;
}

Student::Student(int sid, const string &name, int age) : Person(name, age) {  // 파생 클래스 생성자의 선언
    student_id_ = sid;
}

void Student::ShowPersonInfo() {
    cout << "이 학생의 학번은 "
         << student_id_ << "입니다." << endl;
}

// 순신의 나이는 35살입니다.
// 길동의 나이는 29살입니다.


  • 위의 코드는 먼저 Person 객체를 가리킬 수 있는 포인터 변수 ptr_person을 생성하여, Person 객체의 주소값을 대입한다.
  • 그 후 포인터 변수에 ->(멤버 접근 연산자)를 사용하여 Person 객체의 ShowPersonInfo() 함수를 호출한다.
  • 그 다음에 Student 객체의 주소값을 대입하고, 이번에는 Student 객체의 ShowPersonInfo() 함수를 호출한다.


  • 하지만 결과는 두 번 모두 Person 객체의 ShowPersonInfo() 함수가 호출된다.
  • 왜냐하면, C++ 컴파일러는 포인터 변수가 실제로 가리키는 객체의 타입을 기준으로 함수를 호출하는 것이 아니라, 해당 포인터의 타입을 기준으로 함수를 호출하기 때문이다.
  • 따라서 Person 객체를 가리킬 수 있는 포인터 변수로는 Person 객체의 멤버 함수만을 호출할 수 있다.


  • 이러한 문제점을 해결하기 위해서 C++에서는 virtual 키워드를 사용한 가상 함수를 제공하고 있다.


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

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    virtual void ShowPersonInfo();
};

class Student : public Person {
   private:
    int student_id_;

   public:
    Student(int sid, const string &name, int age);  // 파생 클래스 생성자의 선언
    virtual void ShowPersonInfo();                  // 파생 클래스에서 상속 받은 멤버 함수의 재정의
};

int main(void) {
    Person *ptr_person;  // Person 타입을 기준으로 함수를 호출함 -> 가상 함수 이용
    Person lee = Person("순신", 35);
    Student hong = Student(123456789, "길동", 29);

    ptr_person = &lee;
    ptr_person->ShowPersonInfo();
    ptr_person = &hong;
    ptr_person->ShowPersonInfo();

    return 0;
}

Person::Person(const string &name, int age) {  // 기초 클래스 생성자의 정의
    name_ = name;
    age_ = age;
}

void Person::ShowPersonInfo() {
    cout << name_ << "의 나이는 "
         << age_ << "살입니다." << endl;
}

Student::Student(int sid, const string &name, int age) : Person(name, age) {  // 파생 클래스 생성자의 선언
    student_id_ = sid;
}

void Student::ShowPersonInfo() {
    cout << "이 학생의 학번은 "
         << student_id_ << "입니다." << endl;
}

// 순신의 나이는 35살입니다.
// 이 학생의 학번은 123456789입니다.


  • 위의 코드는 앞선 코드에서 사용한 ShowPersonInfo() 함수를 가상 함수로 선언한 것이다.
  • 이렇게 멤버 함수를 가상 함수로 선언하면 포인터가 실제로 가리키는 객체에 따라 호출하는 대상을 바꿀 수 있게 된다.


4. 다중 상속

  • 다중 상속이란 두 개 이상의 클래스로부터 멤버를 상속 받아 파생 클래스를 생성하는 것을 의미한다.
  • C++에서는 ,를 사용하여 상속 받을 여러 개의 기초 클래스를 명시하는 것으로 간단히 다중 상속을 구현할 수 있다.


class 파생클래스이름 : 접근제어지시자 기초클래스이름, 접근제어지시자 기초클래스이름[, 접근제어지시자 기초클래스이름, ...]
{
    // 파생 클래스 멤버 리스트
}


1) 다중 상속의 문제점

  • 다중 상속은 여러 개의 기초 클래스가 가진 멤버를 모두 상속 받을 수 있다는 점에서 매우 강력한 상속 방법이다.
  • 하지만 이러한 다중 상속은 단일 상속에는 없는 여러 가지 새로운 문제를 발생시킨다.


1] 상속 받은 여러 기초 클래스에 같은 이름의 멤버가 존재할 가능성이 있다.

2] 하나의 클래스를 간접적으로 두 번 이상 상속 받을 가능성이 있다.

3] 가상 클래스가 아닌 기초 클래스를 다중 상속하면, 기초 클래스 타입의 포인터로 파생 클래스를 가리킬 수 없다.


  • 위와 같이 다중 상속은 프로그래밍을 복잡하게 만들 수 있으며, 그에 비해 실용성은 그다지 높지 않다.
  • 반드시 다중 상속을 사용해야만 풀 수 있는 문제란 거의 없으므로, 될 수 있으면 사용을 자제하는 것이 좋다.

References