icon
10장 : 객체 지향 프로그래밍 심화

상속의 개념과 구현

객체 지향 프로그래밍의 핵심 개념 중 하나인 상속(Inheritance) 에 대해 자세히 알아보겠습니다.

상속은 기존 클래스의 속성과 기능을 물려받아 새로운 클래스를 만드는 메커니즘으로, 코드 재사용성을 극대화하고 클래스 간의 계층 구조를 형성하는 데 매우 중요한 역할을 합니다.


상속이란?

상속은 기존 클래스(부모 클래스, 기반 클래스, 슈퍼 클래스)의 멤버(변수와 함수)를 새로운 클래스(자식 클래스, 파생 클래스, 서브 클래스)가 물려받는 것을 의미합니다.

자식 클래스는 부모 클래스의 모든 멤버를 가지며, 필요에 따라 새로운 멤버를 추가하거나 부모 클래스의 멤버를 재정의(override)할 수 있습니다.

상속의 장점

  • 코드 재사용성 증가: 기존 클래스의 코드를 다시 작성할 필요 없이, 상속을 통해 그대로 사용할 수 있습니다.
  • 유지보수성 향상: 부모 클래스의 변경 사항이 모든 자식 클래스에 자동으로 반영되므로, 코드 수정이 용이합니다.
  • 확장성: 기존 클래스를 수정하지 않고, 상속을 통해 새로운 기능을 추가할 수 있습니다.
  • 클래스 계층 구조 형성: 클래스 간의 관계를 명확하게 표현하여 코드의 구조를 이해하기 쉽게 만듭니다.

상속 관계 표현

  • "A is a B" 관계: 예를 들어, "개(Dog)는 동물(Animal)이다"와 같이, 자식 클래스는 부모 클래스의 한 종류임을 나타냅니다.

상속의 기본 형식

C++에서 상속은 다음과 같은 형식으로 구현합니다.

C++ 상속 형식
class 자식클래스이름 : 접근지정자 부모클래스이름 {
    // 자식 클래스의 멤버 (새로운 멤버 추가 또는 부모 클래스 멤버 재정의)
};
  • : (콜론) 기호를 사용하여 상속 관계를 나타냅니다.
  • 접근지정자는 상속 방식(public, private, protected)을 지정합니다. (뒤에서 자세히 다룸)

예시 1: Animal 클래스를 상속받는 Dog 클래스

Animal.h
#pragma once
#include <iostream>
#include <string>

// 부모 클래스 (기반 클래스)
class Animal {
private:
    std::string name;    // 이름
protected:
    std::string species; // 종 (자식 클래스에서 접근 가능)
    int age;             // 나이 (자식 클래스에서 접근 가능)

public:
    Animal(std::string n, std::string s, int a) : name(n), species(s), age(a) {
        std::cout << "Animal 생성자 호출: " << name << " (" << species << ")" << std::endl;
    }
    virtual ~Animal() { // virtual 소멸자 (다형성 관련, 뒤에서 자세히 다룸)
        std::cout << "Animal 소멸자 호출: " << name << std::endl;
    }

    void displayInfo() const {
        std::cout << "이름: " << name << ", 종: " << species << ", 나이: " << age << std::endl;
    }

    // Getter 함수
    std::string getName() const { return name; }
    std::string getSpecies() const { return species; }
    int getAge() const { return age; }
};
Dog.h
#pragma once
#include "Animal.h" // Animal 클래스 포함

// 자식 클래스 (파생 클래스)
class Dog : public Animal { // public 상속
private:
    std::string breed; // 견종

public:
    // Dog 생성자: Animal 생성자 호출하여 초기화
    Dog(std::string n, int a, std::string b) : Animal(n, "개", a), breed(b) {
        std::cout << "Dog 생성자 호출: " << getName() << " (" << breed << ")" << std::endl;
    }
    ~Dog() override { // override 키워드는 선택 사항 (C++11부터)
        std::cout << "Dog 소멸자 호출: " << getName() << std::endl;
    }

    void bark() const {
        std::cout << "멍멍!" << std::endl;
    }

    void displayInfo() const override { // 부모 클래스의 displayInfo() 재정의 (override)
        Animal::displayInfo(); // 부모 클래스의 displayInfo() 먼저 호출
        std::cout << "견종: " << breed << std::endl; // 추가 정보 출력
    }
};
main.cpp
#include "Dog.h" // Dog 클래스 포함

int main() {
    Dog myDog("바둑이", 3, "진돗개"); // Dog 객체 생성
    myDog.displayInfo(); // Animal::displayInfo() + Dog::displayInfo()
    myDog.bark();        // Dog::bark()

    // 부모 클래스의 멤버에도 접근 가능
    std::cout << myDog.getName() << "는 " << myDog.getAge() << "살입니다.\n";

    return 0; // myDog 객체 소멸 (Dog 소멸자 -> Animal 소멸자 순서로 호출)
}

상속 접근 지정자

자식 클래스가 부모 클래스의 멤버를 상속받을 때, 상속 접근 지정자를 사용하여 상속받는 멤버의 접근 권한을 제어할 수 있습니다.

  1. public 상속

    • 부모 클래스의 public 멤버는 자식 클래스에서도 public이 됩니다.
    • 부모 클래스의 protected 멤버는 자식 클래스에서 protected가 됩니다.
    • 부모 클래스의 private 멤버는 자식 클래스에서 접근할 수 없습니다.
  2. protected 상속

    • 부모 클래스의 public 멤버는 자식 클래스에서 protected가 됩니다.
    • 부모 클래스의 protected 멤버는 자식 클래스에서 protected가 됩니다.
    • 부모 클래스의 private 멤버는 자식 클래스에서 접근할 수 없습니다.
  3. private 상속

    • 부모 클래스의 public 멤버는 자식 클래스에서 private이 됩니다.
    • 부모 클래스의 protected 멤버는 자식 클래스에서 private이 됩니다.
    • 부모 클래스의 private 멤버는 자식 클래스에서 접근할 수 없습니다.

일반적인 상속 방식

  • 대부분의 경우 public 상속을 사용합니다. 이는 "A is a B" 관계를 명확하게 표현하며, 부모 클래스의 인터페이스를 자식 클래스가 그대로 유지하도록 합니다.
  • protected 또는 private 상속은 특수한 경우에 사용됩니다.
상속 접근 지정자에 따른 멤버 접근 권한 변화
class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

// public 상속
class PublicDerived : public Base {
public:
    void accessBaseMembers() {
        publicVar = 10;    // OK: public 멤버는 그대로 public
        protectedVar = 20; // OK: protected 멤버는 그대로 protected
        // privateVar = 30; // 오류! private 멤버는 접근 불가
    }
};

// protected 상속
class ProtectedDerived : protected Base {
public:
    void accessBaseMembers() {
        publicVar = 10;    // OK: public 멤버가 protected로 변경됨
        protectedVar = 20; // OK: protected 멤버는 그대로 protected
        // privateVar = 30; // 오류! private 멤버는 접근 불가
    }
};

// private 상속
class PrivateDerived : private Base {
public:
    void accessBaseMembers() {
        publicVar = 10;    // OK: public 멤버가 private로 변경됨
        protectedVar = 20; // OK: protected 멤버는 private로 변경됨
        // privateVar = 30; // 오류! private 멤버는 접근 불가
    }
};

int main() {
    PublicDerived pd1;
    pd1.publicVar = 100; // OK: public 멤버는 외부에서 접근 가능

    ProtectedDerived pd2;
    // pd2.publicVar = 100; // 오류! protected 멤버는 외부에서 접근 불가

    PrivateDerived pd3;
    // pd3.publicVar = 100; // 오류! private 멤버는 외부에서 접근 불가

    return 0;
}

생성자와 소멸자의 상속

  • 생성자: 자식 클래스의 생성자는 부모 클래스의 생성자를 명시적으로 호출하여 부모 클래스의 멤버를 초기화해야 합니다. 이는 멤버 초기화 리스트를 통해 수행합니다. 만약 부모 클래스의 생성자를 명시적으로 호출하지 않으면, 부모 클래스의 기본 생성자(매개변수 없는 생성자)가 자동으로 호출됩니다.
  • 소멸자: 자식 클래스의 소멸자는 자동으로 부모 클래스의 소멸자를 호출합니다. 소멸자 호출 순서는 자식 클래스 소멸자 -> 부모 클래스 소멸자 순서입니다.

예시: 생성자와 소멸자의 상속

Animal.hDog.h의 예시 코드를 참고하세요.

  • Dog 클래스의 생성자에서 : Animal(n, "개", a)를 통해 Animal 클래스의 생성자를 명시적으로 호출하여 name, species, age를 초기화합니다.
  • DogAnimal 클래스 모두 소멸자를 가지고 있으며, Dog 객체가 소멸될 때 Dog의 소멸자가 먼저 호출되고, 그 다음 Animal의 소멸자가 호출됩니다.

멤버 함수 재정의 (Overriding)

자식 클래스는 부모 클래스의 멤버 함수를 재정의(Override) 하여 자신에게 맞게 기능을 변경할 수 있습니다.

함수 재정의는 부모 클래스와 동일한 이름, 반환 타입, 매개변수 리스트를 가지는 멤버 함수를 자식 클래스에서 다시 정의하는 것을 의미합니다.

  • override 키워드 (C++11): 자식 클래스에서 재정의하는 함수에 override 키워드를 붙이면, 컴파일러가 재정의가 올바르게 이루어졌는지 검사해줍니다. (오타 등으로 재정의가 되지 않았을 경우 컴파일 오류 발생)

예시: 멤버 함수 재정의

Animal.hDog.h의 예시 코드를 참고하세요.

  • Dog 클래스는 Animal 클래스의 displayInfo() 함수를 재정의하여, 부모 클래스의 정보를 먼저 출력하고, 자신의 추가적인 정보(breed)를 출력합니다.

다중 상속 (Multiple Inheritance) (심화)

C++은 하나의 자식 클래스가 여러 부모 클래스를 상속받는 다중 상속(Multiple Inheritance) 을 지원합니다.

다중 상속은 강력하지만, 복잡성을 증가시키고 여러 가지 문제점(예: 다이아몬드 상속 문제)을 야기할 수 있으므로 신중하게 사용해야 합니다.

다중 상속에 대한 자세한 내용은 다음 장에서 다루겠습니다.