C++/C++ : Study

상속(3) - 유도 클래스

더블유제이플로어 2025. 3. 20. 05:57

Inheritance

◆  상속의 정의
◆  유도 클래스
◆  protected 멤버
◆  상속에서의 생성자와 소멸자
◆  기본 클래스의 생성자와의 관계
◆  상속과 멤버 함수

실제로 유도클래스를 만드는 방법에 대해 알아보자.


중요한 Part이다.

Deriving classes from existing class

◆   C++  상속 문법

class Base {
		// base class members ...
};

class Derived : public Base {
		// derived class members
};

C++에서 상속을 한다는 의미는 기본 클래스를 가지고
유도 클래스를 만든다는 의미이다.

클래스 뒤에 : public Base
이렇게 사용하고

class Derived : access-specifier Base {
// derived class members...
};

Derived 클래스를 만드는데 Base 클래스를 상속받아서 만든다.
access-specifier 여기에 들어가는 건 public , private , protected 
전부 들어갈 수 있으나 private와 protected는 강의에서는 다루지 않는다.
access-specifier 위치에 public 이 들어간다고 기억하자.

class Derived : public  Base
{ };
Derived 클래스는 Base 클래스를 상속해서 만들었다는 의미이다.
* 유도 클래스를 만드는 문법이다.

새로운 클래스를 만들었으나,
Derived 객체를 만들 수 있다.

클래스 이름은 언제는 바꿀 수 있다.

◆   예시

class Entity {
		// base class members ...
};

class Player  : public Entity {
		// derived class members
};

* Public 위치에 private , protected 키워드를 넣게 되면 다른 방식으로 동작하지만,
잘 사용하지 않으므로 설명하지 않습니다.


◆  public 상속

   ● 가장 흔히 사용되는 상속 방식
   ● "is-a" 관계의 정의와 가장 일치하는 상속 방식

◆  private와 protected 상속

   ● "is-a"와 유사한 관계를 정의하는 상속
   ● 소유와는 차이가 존재

◆  본 간의에서는 public. 상속에 집중

   ● 타 언어에서 protected / private 상속을 아예 지원하지 않는 경우가 많음
   ● protected / private 상속은 public  상속만큼 활용도가 높지 않음 (다른 방식으로 구현한다.) 

유도 클래스는 public 로 쓴다는 걸 외워두자.
private와 protected 도 있기는 하지만 거의 안 쓴다.

거의 안쓴다는 이야기는
C#, Java 등 다른 프로그램 언어(객체지향)에서는
private 나 protected 상속이 아예 없다.

없는 이유는 사용자가 잘 없기 때문이다.
특수한 기능이 있기는 하지만 잘 안 쓰기 때문에
public 상속을 한다고 알아두자.


◆  Player 예제

앞서 배웠던 그림을 코드로 정리한다고 생각하고 예제를 알아보자.

class Entity {
protected :
	int x, y;
public :
	Entity(int x, int y) // 생성자 2개 인자를 받도록 한다.
		:x{ x }, y{ y }
	{ }
	void ShowPosition()
	{
		cout << "[ " << x << ", " << y << " ]" << endl;
	}
	void Talk()
	{
		cout << "안녕하세요." << endl;
	}
};

class Player : public Entity {
private:
	int hp, xp, speed;
public:
	Player(int x, int y, int speed)
		:Entity{ x,y }, speed{ speed }
	{ }
	void Move(int dx, int dy)
	{
		x += dx;
		y += dy;
	}

* Player 에는 x, y 도 없고, Talk() , ShowPosition() 도 없어 보이지만,

int main()
{
	Entity entity1{ 1,1 };
	entity1.Talk();
	entity1.ShowPosition();

	Player player1{ 2,3,1 };
	player1.Talk();
	player1.ShowPosition();
}

* x, y, speed 와 Talk(), ShowPosition을 모두 포함하고 있는 것을 알 수 있다.

Player 객체인 p에 점을 찍어보면 Move() , ShowPosition(), Talk() 함수를 확인할 수 있다.
Player 클래스에는 Move() 메서드를 만들어 두었다.
Player 클래스에 Talk() 와 ShowPosition() 은 없으나 사용할 수 있다.
그 이유는 Entity 클래스를 상속하였기 때문이다.
Player 클래스를 만들때 Entity 클래스가 가지고 있는 데이터와 기능을 모두 포함하도록 만들었기 때문이다.
그렇기에 Entity 클래스의 ShowPosition()과 Talk() 메서드를 Player 객체에서도 사용할 수 있는 것이다.

 

Player 클래스를 새로 만들 때 Entity 클래스를 상속하도록 작성하면
Player 클래스 안에 Entity 클래스가 가지고 있는 모든 데이터와 기능이 포함이 되어서
Player 객체도 Entity 클래스의 데이터와 기능을 모두 포함하고 있다.


Inheritance

◆  상속의 정의 : 기본 클래스를 기반으로 새 클래스를 만드는 기능
◆  유도 클래스 : 그렇게 만들어진 클래스를 유도 클래스라고 하며, 기본 클래스의 모든 것이 포함되어 있다.
◆  protected 멤버
◆  상속에서의 생성자와 소멸자
◆  기본 클래스의 생성자와의 관계
◆  상속과 멤버 함수

기본 클래스 , 유도 클래스를 이야기하였다.
유도 클래스는 기본 클래스를 기반으로 새 클래스를 상속해서 만들게 되는 클래스이다.
기본 클래스의 모든 것, 멤버 변수 멤버 함수, 를 포함되어 있다.

https://youtu.be/pWzIvnH1n9M?si=Frdue1acBAqqCTYC

강사님께서 상속은 추가적인 코드를 보는 게 좋다고 하였으나, 추가적인 코드 자료를 찾지 못하여
AI claude에게 문의하여 추가적인 코드를 살펴보았다.

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

// 기본 클래스 (부모 클래스)
class Person {
protected:
    string name;
    int age;

public:
    // 생성자
    Person(const string& _name, int _age) : name(_name), age(_age) {
        cout << "Person 생성자 호출" << endl;
    }
    // 가상 소멸자 (상속 관계에서는 중요)
    virtual ~Person() {
        cout << "Person 소멸자 호출" << endl;
    }
    // 가상 함수 - 파생ㅇ 클래스에서 오버라이드 가능
    virtual void introduce() const {
        cout << "안녕하세요. 제 이름은 " << name << "이고, "
            << age << "살 입니다." << endl;
    }
    // 일반 메서드
    void sleep() const {
        cout << name << "이(가) 잠을 잡니다." << endl;
    }
};

// 파생 클래스 1 (자식 클래스)
class Student : public Person {
private:
    string schoolName;
    int studentId;

public:
    // 부모 클래스의 생성자를 호출하는 생성자
    Student(const string& _name, int _age, string _schoolName, int _studentId)
        : Person(_name, _age), schoolName(_schoolName), studentId(_studentId) {
        cout << "Student 생성자 호출" << endl;
    }

    ~Student() override {
        cout << "Student 소멸자 호출" << endl;
    }

    // 부모 클래스의 가상 함수 오버라이드
    void introduce() const override {
        cout << "안녕하세요, 제 이름은 " << name << "이고, "
            << age << "살 입니다." << schoolName << "에 다니고 있으며, "
            << "학번은 " << studentId << "입니다." << endl;
    }

    // 추가 메서드
    void study() const {
        cout << name << "이(가) 공부합니다." << endl;
    }
};

// 파생 클래스 2 (다른 자식 클래스)
class Teacher : public Person {
private:
    string subject;
    int yearsOfExperience;

public:
    Teacher(const string& _name, int _age, const string& _subject, int _yearsOfExperience)
        : Person(_name, _age), subject(_subject), yearsOfExperience(_yearsOfExperience) {
        cout << "Teacher 생성자 호출" << endl;
    }

    ~Teacher() override {
        cout << "Teacher 소멸자 호출" << endl;
    }

    void introduce() const override {
       cout << "안녕하세요, 제 이름은 " << name << "이고, "
            << age << "살입니다. " << subject << " 과목을 "
            << yearsOfExperience << "년간 가르치고 있습니다." << endl;
    }

    void teach() const {
        cout << name << "이(가) " << subject << "을(를) 가르칩니다." << endl;
    }
};

// 다중 상속 예제
class TeachingAssistant : public Student, public Teacher {
private:
    string department;

public:
    TeachingAssistant(const string& _name, int _age,
        const string& _schoolName, int _studentId,
        const string& _subject, int _yearsOfExperience,
        const string& _department)
        : Student(_name, _age, _schoolName, _studentId),
        Teacher(_name, _age, _subject, _yearsOfExperience),
        department(_department) {
        cout << "TeachingAssistant 생성자 호출" << endl;
    }

    ~TeachingAssistant() {
        cout << "TeachingAssistant 소멸자 호출" << endl;
    }
   
    // 다중 상속으로 인한 모호성 해결
    void introduce() const override {
        cout << "안녕하세요, 저는 " << department << " 학과의 TA입니다." << endl;
        // Student::introduce(); // 특정 부모 클래스의 메서드 호출
    }
};

int main()
{
    cout << "===== 기본 상속 테스트 =====" << endl;

    // 부모 클래스 객체 생성
    Person person("홍길동", 30);
    person.introduce();
    person.sleep();

    cout << "\n===== 단일 상속 테스트 =====" << endl;

    // 자식 클래스 객체 생성
    Student student("김학생", 20, "우송대학교", 20251234);
    student.introduce(); // 오버라이드된 메서드 호출
    student.sleep(); // 부모 클래스에서 상속 받은 메서드 호출
    student.study(); // 자식 클래스의 고유 메서드 호출

    Teacher teacher("이교수", 45, "컴퓨터 과학", 15);
    teacher.introduce();
    teacher.teach();

    cout << "\n===== 다형성 테스트 =====" << endl;

    // 기본 클래스 포인터로 파생 클래스 참조 (다형성)
    Person* p1 = &student;
    Person* p2 = &teacher;

    p1->introduce(); // 가상 함수 - Student의 introduce() 호출
    p2->introduce(); // 가상 함수 - Teacher의 introduce() 호출

    p1->sleep(); // 비 가상함수 - Person의 sleep() 호출
    p2->sleep(); // 비 가상함수 - Person의 sleep() 호출

    // p1->study(); // 컴파일 에러 : Person 클래스에는 study() 메서드가 없다.

    cout << "\n===== 다중 상속 테스트 =====" << endl;
    TeachingAssistant ta("박조교", 25, "우송대학교", 20230123, "프로그래밍", 2, "컴퓨터공학");
    ta.introduce();
    // 모호성을 명시적으로 해결
    ta.Student::sleep();
    //ta.sleep(); // 컴파일 에러 : TeachingAssistant::sleep() 이 모호하다.
    ta.study();
    ta.Teacher::teach(); // 클래스가 ~소멸자부터 나와서 은근 신경쓰인다.

    return 0;
}

대학교 예시는 S대로 나와있으나, S대 출신이 아니고 W출신이라서 W로 작성하였다.
세상에 라테는 기본클래스가 아니라 부모클래스-자식클래스로 배웠기 때문에
상속 부분에서 몰랐던 개념처럼 공부했는데
학교 다닐 때 딱 상속과 다형성 파트 때부터 헤매기 시작했던 거 같다.
학교 입학한 지 10년도 넘은 상태에서 다시 배우니 즐겁고 생기가 돌았다.
학교 다닐 때 AI 가 있었더라면.. 조금 더 잘하지 않았나 싶지만
그 당시 이해 못 하던걸 지금에서야 이해하기 되니 감개무량하다.

AI claude 가 알려준 개념을 정리해보았다.


◆  1. 기본 상속

   ● Person 클래스를 부모 클래스로 하여 Student 와 Teacher 클래스가 상속받는다.
   ● protected 접근 제어자를 통해 자식 클래스에서만 접근 가능한 멤버 변수를 설정한다.

◆  2. 가상함수와 오버라이딩

   ● virtual 키워드를 사용하여 부모 클래스에 가상 함수를 정의하였다.
   ● 자식 클래스에서 override 키워드로 명시적 오버라이딩을 구현하였다.
   ● 다형성을 통해 부모 클래스 포인터로 자식 클래스의 오버라이드 된 메서드를 호출할 수 있다.

◆  3. 생성자와 소멸자 상속

   ● 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출하는 방법을 보여준다.
   ● 가상 소멸자를 사용하여 메모리 누수를 방지한다.

◆  4. 다중 상속

   ● TeachingAssistant 클래스가 Student 와 Teacher 두 클래스를 모두 상속 받는다.
   ● 다중 상속 시 발생할 수 있는 모호성 문제와 해결 방법을 보여준다.


override 키워드 개념 정리

C++ 에서 상속 관계에서 중요한 두가지 개념이 있다.

◆  1. 가상 함수(Virtual Function)

   : 부모 클래스에서 vitual 키워드로 선언된 함수로, 자식 클래스에서 재정의 할 수 있도록 설계된 함수이다.

◆  2. 오버라이딩(Overriding)

   : 자식 클래스에서 부모 클래스의 가상 함수와 동일한 이름, 매개변수, 반환 타입을 가진 함수를 재정의하는 것이다.

override 키워드는 " 이 함수는 부모클래스의 가상 함수를 재정의 하는 것이다." 라고 컴파일러에게 명시적으로 알려주는 역할이다.
override 키워드는 C++11 에서 도입된 기능으로, 가상 함수를 오버라이딩 할때 사용된다.

class Parent {
public:
    virtual void show() { cout << "부모 클래스" << endl; }
};
class Child : public Parent {
public:
    void show() override { cout << "자식 클래스" << endl; }
};

여기서 override 키워드가 하는일은
1. 부모 클래스에 show() 라는 가상 함수가 실제로 존재하는지 컴파일러가 확인한다.
2. 함수 시그니처(이름, 매개변수, 반환타입)가 부모 클래스의 것과 정확히 일치하는지 확인한다.
3. 만약 부모클래스에 해당 가상 함수가 없거나 시그니처가 다르면 컴파일러 에러를 발생시킨다.

override 키워드가 없이도 오버라이딩은 가능하지만, 이 키워드를 사용하면 의도치 않은 실수를 방지할 수 있다.
예를 들어, 부모 클래스의 함수를 약간 다르게 작성하거나 매개 변수 타입을 잘못 작성하면 오버라이딩이 아닌 완전히
새로운 함수를 정의하게 되는데, override 키워드를 사용하면 이런 실수를 컴파일러가 잡아낼 수 있다.


override 키워드를 사용하는 이유

◆  1. 명시적 의도 표현

   : 코드를 읽는 사람에게 해당 함수가 부모 클래스의 함수를 오버라이딩하고 있다는 것을 명확히 알려준다.

◆  2. 컴파일 타임 검사

    : 가장 중요한 이유로, 컴파일러가 오버라이딩이 정확히 이루어졌는지 검사한다.
   만약 부모클래스에 해당 가상 함수가 없거나 시그니처가 다르면 컴파일 에러가 발생한다.

◆  3. 실수 방지

   : 함수 이름 오타, 매개변수 타입 불일치, 반환 타입 불일치 등의 실수를 방지할 수 있다.


override 사용 시 주의해야 할 점

◆  1. 가상 함수에만 사용

   : override 키워드는 부모 클래스에서 virtual 로 선언된 함수에만 사용할 수 있다.
   부모 클래스에 해당 가상 함수가 없다면 컴파일 에러가 발생한다.

class Parent {
public:
    void normalFunction() {} // 가상 함수가 아니다.
};

class Child :public Parent {
public:
    void normalFunction() override {} // 컴파일 에러 : 부모 클래스의 함수가 가상 함수가 아니다.
};

◆  2. 함수 시그니처 정확히 일치

   : override 함수의 이름, 매개변수 목록, 반환 타입, const 한정자 등이 모두 정확히 일치해야 한다.

class Parent {
public:
    virtual void func(int x) const {}
};

class Child :public Parent {
public:
    void func(int x) override {} // 컴파일 에러 : const 한정자가 없다.
    void func(float x) const override {} // 컴파일 에러 : 매개변수 타입이 다르다
    int func(int x) const override {} // 컴파일 에러 : 반환 타입이 다르다.
};

◆  3. 공변 반환 타입(Covariant return type)

   : 공변 반환 타입(Covariant return type) 은 C++ 에서 가상 함수를 override 할 떄 적용할 수 있는 특별한 규칙이다.
   일반적으로 가상함수를 override 할때는 함수 시그니처(이름, 매개변수 목록, 반환 타입)가 정확히 일치해야 한다.
   하지만 공변 반환타입은 이 규칙을 예외로,
   반환 타입이 포인터나 참조인 경우에 한하여 자식 클래스에서 더 구체적인(파생된) 타입을 반환할 수 있게 해준다.
   
   : 유일한 예외로, 반환 타입이 포인터나 참조인 경우 파생 클래스의 포인터나 참조로 변경 가능하다.

class Base {};
class Derived : public Base {};

class Parent {
public:
    virtual Base* create() { return new Base(); }
};

class Child :public Parent {
public:
    Derived* create() override { return new Derived(); } // 허용된다.  (공변 반환 타입)
};
// 공변 반환 타입 예시
class Shape {
public:
    virtual void draw() { cout << "도형 그리기" << endl; }
};
class Circle : public Shape {
public:
    void draw() override { cout << "원 그리기"  << endl; }
    void calcuateArea() { cout << "원의 면적 계산" << endl; }
};
class ShapeCreator {
public:
    virtual Shape* create() {
        return new Shape();
    }
};

class CircleCreator : public ShapeCreator {
public:
    // 공변 반환 타입 : shape* 에서 Circle*로 변경 가능
    Circle* create() override {
        return new Circle();
    }
};

   이 예제에서  CircleCreator::create() 함수는 부모 클래스 ShapeCreator::create()와 다른 반환 타입 (Circle*) 을 가지고 있지만,
   유효한 override 이다. 이것이 공변 반환 타입의 예제이다.
   공변 반환 타입의 실용적 이점은 다음과 같다.

   int main()
   {
    CircleCreator creator;
    // ShapeCreator 의 포인터로 사용해도 Circle*을 반환한다.
    Circle* circle = creator.create();
    
    // Circle 클래스의 특수 메서드를 캐스팅 없이 직접 호출 가능하다.
    circle->calcuateArea();
    }

   부모 클래스에서 일반적인 Shape* 를 반환하지만, 자식 클래스에서는 더 구체적인 Circle* 을 반환함으로써
   타입 캐스팅이 없이도 Circle 클래스의 특수 메서드를 바로 사용할 수 있다.
   이 방식은 코드 타입 안전성을 높이고 프로그래머의 의도를 더 명확하게 표현할 수 있게 해준다.

◆  4. 다중 상속 상황에서의 모호성

   : 다중 상속 시 두 부모 클래스에 같은 이름의 가상 함수가 있는 경우, 모호성을 해결해야 한다.

class A {
public:
    virtual void func() { cout << "A::func 호출" << endl; }
};
class B {
public:
    virtual void func() { cout << "B::func 호출" << endl; }
};
class C : public A, public B {
public:
    // 어떤 함수를 오버라이딩 하는지 모호하다.
    void func() override {} // 컴파일 에러 가능성 있다.

    // 모호성 해결 방법 1 : 각 부모 클래스의 함수를 명시적으로 호출
    void func() override {
        cout << "C::func 호출" << endl;
        A::func(); // A 클래스의 func 호출
        B::func(); // B 클래스의 func 호출
    }
};

// 모호성 해결 방법 2 : using 선언으로 특정 부모 클래스의 함수를 사용
class C2 : public A, public B {
public:
    using A::func; // 클래스의 func 을 사용하겠다고 명시

    // 추가로 override도 가능
    void func() override {
        cout << "C2::func 호출(A의 함수 오버라이드)" << endl;
    }
};

◆  5. final 키워드와의 관계 : final 키워드가 있는 가상 함수는 더 이상 오버라이딩 할 수 없다.

class Parent {
    virtual void func() final {}
};
class Child : public Parent {
public:
    void func() override {} // 컴파일 에러 : final 로 지정된 오버라이딩 불가
};

실제 코드를 사용할 때에는, 가상 함수를 오버라이딩 할때 항상 override 키워드를 사용하는 것이 좋다.

뭔가 진도를 더 넘어간거 같지만, 학교 다닐때 배우던 문법이 이제야 제대로 기억이 난다.
이번 강의에서 제대로 배워야겠다.