6 분 소요

상속에 들어가기에 앞서

상속은 재활용 목적으로 사용되기도 하지만 재활용의 목적으로만 존재하는 문법적 요소가 아니다.



직원 정보를 등록하고 출력해주는 프로그램을 만든다고 할 때, 2개의 클래스로 분리할 수 있다. 직원 정보에 들어가야되는 멤버 변수를 가지고 있는 클래스와 컨트롤 클래스 / 핸들러 클래스

핸들러 클래스에는 기능의 처리를 실제로 담당하는 클래스로, 이 프로그램의 경우 AddEmployee, ShowAllSalaryInfo, ShowTotalSalary 가 구성하고 있다.


직원 정보에 들어가야되는 멤버 변수를 가지고 있는 클래스

class PermanentWorker
{
private:
    char name[100];
    int salary;

public:
    PermanentWorker(char *name, int money) : salary(money)
    {
        strcpy(this->name, name);
    }
    int GetPay() const
    {
        return salary;
    }
    void ShowSalaryInfo() const
    {
        std::cout << "name: " << name << std::endl;
        std::cout << "salary: " << salary << std::endl
                  << std::endl;
    }
};


핸들러 클래스

class EmployeeHandler
{
private:
    PermanentWorker *empList[50];
    int empNum;

public:
    EmployeeHandler() : empNum(0)
    {
    }
    void AddEmployee(PermanentWorker *emp)
    {
        empList[empNum++] = emp;
    }
    void ShowAllSalaryInfo() const
    {
        for (int i = 0; i < empNum; i++)
        {
            empList[i]->ShowSalaryInfo();
        }
    }
    void ShowTotalSalary() const
    {
        int sum = 0;
        for (int i = 0; i < empNum; i++)
        {
            sum += empList[i]->GetPay();
        }
        std::cout << "salary sum: " << sum << std::endl;
    }
    ~EmployeeHandler()
    {
        for (int i = 0; i < empNum; i++)
        {
            delete empList[i];
        }
    }
};

main

int main()
{
    // 직원 관리를 목적으로 설계된 컨트롤 클래스의 객체 생성
    EmployeeHandler handler;

    // 직원 등록
    handler.AddEmployee(new PermanentWorker("KIM", 1000));
    handler.AddEmployee(new PermanentWorker("LEE", 1500));
    handler.AddEmployee(new PermanentWorker("JUN", 2000));

    // 이번 달에 지불해야 할 급여의 정보
    handler.ShowAllSalaryInfo();

    // 이번 달에 지불해야 할 급여의 총합
    handler.ShowTotalSalary();
    return 0;
}


>>>
name: KIM
salary: 1000

name: LEE
salary: 1500

name: JUN
salary: 2000

salary sum: 4500

이를 통해서 알 수 있는 객체 지향 언어의 특징

  1. 요구 사항의 변경에 대응하는 프로그램의 유연성
  2. 기능의 추가에 따른 프로그램의 확장성

문제 제시

직원의 고용 형태가 고용직, 영업직, 임시직으로 나뉘었고, 각각 급여 계산 방식도 다르다.

여기서 문제는 고용직, 영업직, 임시직의 클래스를 만들게 된다면 핸들러 클래스의 메소드들도 모두 변경되어야 한다. 이 방법은 절대 효율적인 방법이 아니다.

그러면 고용직, 영업직, 임시직에 대한 각각의 클래스는 만들면서 동시에 핸들러 클래스의 코드는 변경시키지 않는 방법이 있을까?

상속의 문법적인 이해

상속 방법

Person 클래스 -> UnivStudent 클래스

'''Person 클래스 정의'''

class UnivStudent : public Person
{
    '''UnivStudent 클래스 정의'''
}

상속 결과

Person 클래스를 상속받은 UnivStudent 클래스는 Person 클래스의 멤버 변수 + UnivStudent 클래스의 멤버 변수 모두를 초기화 시켜야되는 의무를 가지고 있다.

그 외에도 UnivStudent 클래스에서 사용될 수 있는 멤버 함수로는 Person 클래스에서 정의되었던 멤버 함수와 UnivStudent 클래스에서 정의되었던 멤버 함수 둘다 사용할 수 있다.

상속받은 클래스의 생성자 정의

Person 클래스의 멤버 변수: age, name

UnivStuendnt 클래스의 멤버 변수: major

UnivStudent 클래스에서 Person 클래스의 멤버 변수를 초기화하기 위해서는 Person 클래스의 생성자를 사용해주어서 초기화해주는 것이 좋다.

// Person class 생성자
Person(int myage, char *myname) : age(myage)
    {
        strcpy(name, myname);
    }

// UnivStudent class 생성자
UnivStudent(char *myname, int myage, char *mymajor) : Person(myage, myname)
    {
        strcpy(major, mymajor);
    }

상속 전체 예시

#include <iostream>
#include <cstring>

class Person
{
private:
    int age;
    char name[50];

public:
    Person(int myage, char *myname) : age(myage)
    {
        strcpy(name, myname);
    }
    void WhatYourName() const
    {
        std::cout << "My name is " << name << std::endl;
    }
    void HowOldAreYou() const
    {
        std::cout << "I'm " << age << " years old" << std::endl;
    }
};

class UnivStudent : public Person
{
private:
    char major[50];

public:
    UnivStudent(char *myname, int myage, char *mymajor) : Person(myage, myname)
    {
        strcpy(major, mymajor);
    }
    void WhoAreYou() const
    {
        WhatYourName();
        HowOldAreYou();
        std::cout << "My major is " << major << std::endl
                  << std::endl;
    }
};

int main()
{
    UnivStudent ustd1("Lee", 22, "Computer eng. ");
    ustd1.WhoAreYou();

    UnivStudent ustd2("Yoon", 21, "Electronic eng. ");
    ustd2.WhoAreYou();

    return 0;
}

>>>
My name is Lee
I'm 22 years old
My major is Computer eng.

My name is Yoon
I'm 21 years old
My major is Electronic eng.

Q: UnivStudent 클래스의 멤버 함수 내에서는 Person 클래스에 private으로 선언된 멤버변수 age와 name에 접근이 가능한가요?

A: private 접근 제한은 객체 기준이 아닌 클래스 기준이므로 직접 접근은 불가능하다. 하지만 Person 클래스의 멤버 변수는 Person의 public 함수를 통해서는 간접적으로 접근이 가능하다. (ex. GetName, GetAge,,,,)

이를 통해서 정보의 은닉은 하나의 객체 내에서도 진행이 된다. (Person 멤버 변수의 정보는 public 함수를 통해서 접근해야되므로)

용어 정리

  • Person 클래스
    • 상위 클래스
    • 기초 클래스
    • 슈퍼 클래스
    • 부모 클래스
  • UnivStudent 클래스
    • 하위 클래스
    • 유도 클래스
    • 서브 클래스
    • 자식 클래스

유도 클래스의 객체 생성과정

// 기초 클래스
class SoBase
{
private:
    int baseNum;

public:
    SoBase() : baseNum(20)
    {
        std::cout << "SoBase()" << std::endl;
    }
    SoBase(int n) : baseNum(n)
    {
        std::cout << "SoBase(int n)" << std::endl;
    }
    void ShowBaseData()
    {
        std::cout << baseNum << std::endl;
    }
};
// 유도 클래스
class SoDerived : public SoBase
{
private:
    int derivNum;

public:
    SoDerived() : derivNum(30)
    {
        std::cout << "SoDerived()" << std::endl;
    }
    SoDerived(int n) : derivNum(n)
    {
        std::cout << "SoDerived(int n)" << std::endl;
    }
    SoDerived(int n1, int n2) : SoBase(n1), derivNum(n2)
    {
        std::cout << "SoDerived(int n1, int n2)" << std::endl;
    }
    void ShowDerivData()
    {
        ShowBaseData();
        std::cout << derivNum << std::endl;
    }
};

case 1. 빈 생성자인 경우

std::cout << "case 1" << std::endl;
SoDerived dr1;
dr1.ShowDerivData();
std::cout << "===============" << std::endl;

>>>
case 1
SoBase()
SoDerived()
20
30
===============

인자가 정해져있지 않기 때문에 SoDerived 클래스가 생성 -> SoBase 클래스를 상속받았으므로 SoBase의 생성자 호출 (SoBase: 20) -> SoDerived 빈 생성자 호출(derivNum: 30) -> ShowDerivData() 호출 -> 20, 30 출력



Case 2. 매개변수가 한 개인 경우

std::cout << "case 2" << std::endl;
SoDerived dr2(12);
dr2.ShowDerivData();
std::cout << "===============" << std::endl;

>>>
case 2
SoBase()
SoDerived(int n)
20
12
===============

인자가 한개이므로 SoDerived 클래스 중 인자 한개를 가지고 있는 생성자 호출 전에 -> SoBase는 인자가 정해져있지 않으므로 baseNum: 20 -> derivNum: 12 -> ShowDerivData 호출 -> 20, 12 출력



Case 3. 인자가 두 개인 경우

std::cout << "case 3" << std::endl;
SoDerived dr3(23, 24);
dr3.ShowDerivData();

>>>
case 3
SoBase(int n)
SoDerived(int n1, int n2)
23
24

인자가 두 갠데 SoBase 클래스에서 인자 하나인 생성자를 호출 (baseNum: 23) -> derivNum: 24 -> ShowBaseData() 호출 -> 23, 24 출력

유도 클래스 객체의 소멸 과정

유도 클래스가 생성될 때, 생성자가 두 번 호출되었으므로 소멸 과정에서도 소멸자가 두 번 호출됨을 유추할 수 있다.


소멸 과정은 유도 클래스의 소멸자가 실행되고 난 다음에 기초 클래스의 소멸자가 실행된다.

protected 선언과 세 가지 형태의 상속

protected로 선언된 멤버가 허용하는 접근의 범위

private < protected < public


protected와 private 둘다 클래스의 외부에서는 접근이 불가능하지만 protected의 경우 클래스 내부에서는 접근이 가능하다.

즉, 상속이 일어난 경우 private는 클래스 간 접근이 안되기 때문에 직접 접근이 불가능하지만 protected의 경우는 유도 클래스에서 접근이 가능하다.

그래도 기초 클래스와 이를 상속하는 유도 클래스 사이에서도 정보 은닉이 지켜지는게 좋다.

protected 상속

위의 예시에서는 상속을 할 때 pubilc 클래스 이름 을 사용했지만 public 자리에 protected 가 오게 되면 protected 보다 접근의 범위가 넓은 멤버는 protected로 변경시켜서 상송하겠다는 뜻이다.

#include <iostream>

class Base
{
private:
    int num1;

protected:
    int num2;

public:
    int num3;

    Base() : num1(1), num2(2), num3(3)
    {
    }
};

class Derived : protected Base
{
};

int main()
{
    Derived drv;
    std::cout << drv.num3 << std::endl;

    return 0;
}

이렇게 되면 컴파일 에러가 뜬다. 왜? public 멤버 변수인 num3 이 protected로 상속받았기 때문에 protected가 되어서 외부에서는 접근이 불가능 하기 때문이다.

private 상속

private 상속 역시 같은 의미이다. private 보다 범위가 넓은 경우는 모두 private로 변경시켜서 상속시킨다. 라는 의미이다.

public 상속

이는 위에서 예시로 사용했기 때문에 더욱 금방 이해할 수 있다. public 보다 범위가 넓은 범위를 public으로 바꾼다는 의미인데, public이 결국 가장 넓은 범위를 가지고 있으므로 작성한 기존 클래스와 같은 의미를 지닌다.

C++의 상속은 public 상속만 있다고 생각하자


사실 위 의미는 public 상속 외에 private 상속과 protected 상속이 거의 사용되지 않아서 이렇게 가르치기도 한다는 말이다.

상속을 위한 조건

상속을 위한 기본 조건인 IS-A 관계의 성립

is a 는 일종의 ~이다 라는 의미로 해석된다.

  • 무선 전화기 is a 전화기
  • 노트북 컴퓨터는 is a 컴퓨터

HAS-A 관계는 상속 관계로 사용하진 않는다.

HAS-A 관계는 소유의 관계로

  • 경찰 has a 총

과 같은 관계이다. 하지만 이 관계를 상속으로 표현하는 것은 가능은 하지만 오히려 복잡해지므로 상속을 사용해서 각각의 객체를 표현하지는 않는다.

댓글남기기