본문 바로가기

C++/윤성우의 열혈 프로그래밍 C++

윤성우의 열혈 C++ chpt 11.

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

 

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

class Base
{
private:
    int num1;
protected:
    int num2;
public:
    int num3;
    void ShowData()
    {
        cout << num1 << ", " << num2 << ", " << num3;
    }
};
class Derived : public Base
{
public:
    void ShowBaseMember()
    {
        cout << num1;	// 컴파일 에러
        cout << num2;	// 컴파일 ok
        cout << num3;	// 컴파일 ok
    }
};

 

protected 는 private 와는 달리 상속관계에서의 접근을 허용해준다.

 

[ 세 가지 형태의 상속 ]

class Derived : public Base
{
	...
}

public 상속

 

접근 제어 권한을 그대로 상속한다.

단, private 은 접근불가로 상속한다.

class Derived : protected Base
{
	...
}

protected 상속

 

protected 보다 접급의 범위가 넓은 멤버는 protected 로 상속한다.

단. private 은 접근불가로 상속한다.

class Derived : private Base
{
	...
}

private 상속

 

private 보다 접근의 넓믄 멤버는 protected 로 상속한다.

단, private 은 접근불가로 상속한다.

 

 

접근법위 : 접근 제어 수준(Access Control Level)

 

보다 넓은 법위 == 얼마나 개방적인지를 의미

 

접근범위 비교

publid > protected > private

 

 

이 내용을 조금 더 설명해보자면

 

부모 객체에서 외부나 상속된 자식으로 접근이 가능하던 자원들도,

접근 레벨이 오른 채로 상속을 받게되면, 외부에서 접근 가능하던 부모의 자원이

상속 받은 자식 객체는 같은 자원이었어도 접근이 불가능하게 되며, 

나아가 자식 객체가 상속받아서 접근 가능했던 부분들도,

접근 레벨이 오른 채 상속을 받는다면 자식 마저 접근을 할 수 없게 될 수 있다는 내용이다.

 

상속 시 접근 레벨이 더 제한적으로 바뀌면 상황이 달라진다는 것이다.

 

부모에서는 외부 접근이 가능했던 자원이라도,
자식 클래스에서는 접근이 불가능해질 수 있다.
이는 같은 자원을 상속받았더라도 접근 권한이 더 낮아지기 때문이다.

더 나아가,
자식 클래스 내부에서 접근 가능했던 멤버라도 그 자식 클래스가 다시 더 제한적인 방식으로 상속된다면,

그 다음 자식(즉, 손자 클래스)에서는 접근 자체가 막히는 상황도 만들어진다.

 

(즉, 접근 권한이 더 좁아진 채 상속되면, 이전에 접근 가능하던 자원도 더 이상 접근할 수 없게 된다)

 

 

이 그림의 예시처럼 기존의 멤버의 접근 권한 설정이 상속에 따라서 바뀐 것을 볼 수 있다.

 

(일반적으로 상속은 public 이 주 이기 때문에, 보통의 상속을 논의할 땐 public 을 기준으로 하면 된다)


상속을 위한 (최소한)조건

상속의 기본 조건 IS-A 관계 성립

 

부모 A -(상속)-> 자식 B

== B is a A

== B 는 A 이다.

 

 

HAS-A 관계의 상속

class Gun
{
private:
    int bullet;	// 장전된 총알의 수
public:
    Cun(int bnum) : bullet(bnum)
    {}
    void Shuw()
    {
        cout << "BBANG!"
        bullet--;
    }
};
class Police : public Gun
{
private:
    int handcuffs;	// 소유한 수갑의 수
public:
    Police(int bnum, int bcuff)
        : Gun(bnum), handcuffs(bcuff)
    {}
    void PutHandcuff()
    {
        cout << "SNAP!" << endl;
        handcuffs--;
    }
};

 

총의 기능을 경찰이 상속을 받아야 그 총의 기능을 사용할 수 있으니, 경찰이 총을 소유하는 입장이라 has a 관계가 성립이 되지만,

 

has a 관계로 상속을 받으면, 경찰 클래스엔 무조건 적으로 총을 포함시키기 때문에

 

총을 소유하지 않은 경찰이나, 다른 무기를 소유하는 경찰을 표현하기가 쉽지 않아진다.

 

두 클래스 간의 의존도가 높아짐으로써 틀이 잡혀진 것을 유연적으로 사용하기는 어렵기 때문이다

 

 

그래서 HAS-A 관계는 다음과 같이 멤버로써 활용하는 것이 사용함에 있어서 더 효율적일 수 있다.

class Police
{
private:
    int handcuffs;	// 소유한 수갑의 수
    Gun* pistol;	// 소융하고 있는 권총
public:
    Police(int bnum, int bcuff)
        : handcuffs(bcuff)
    {
        if (bnum > 0)
            pistol = new Gun(bnum);
        else
            pistol = NULL;
    }
    void PutHandcuff()
    {
        cout << "SNAP!" << endl;
        handcuffs--;
    }
    void Shut()
    {
        if (pistol == NULL)
            cout << "Hut BBANG!" << endl;
        else
            pistol->Shut();
    }
    ~Police()
    {
        if (pistol != NULL)
            delete pistol;
    }
};

 

이처럼 has a 관계를 포함의 형태로 표현하면, 두 클래스간 연관성은 낮아지며, 변경 및 확장이 용이해진다.

이를 통해 아까 표현하기 어려웠던 총이 없는 경찰과, 총이 아닌 다른 무기를 추가하거나 대체하기 쉬워진다.


[ 상속과 다형성 ]

객체 포인터와 참조 관계

 

"C++ 에서, AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다."

(객체의 주소 값을 저장할 수 있다)

class Student : public Person
{
	...
};
class PratTimeStudent : public Student
{
	...
};

Person <- student <- PratTimeStudent

Person * ptr = new Student();

Person * ptr = new PartTimeStudent();

Student * ptr = new PartTimeStudent();

 

Student 클래스는 Person 클래스를 상속 받았기 때문에, Student 와 Person 의 포인터로 접근이 가능하지만,

PartTimeStudent 의 포인터로는 접근 할 수 없다.

 

반대로

 

PartTimeStudent 클래스는 Person 클래스를 상속받은 Student 클래스이기 때문에

PartTimeStudent 의 포인터를 포함해서 Student 와 그 위인 Person 의 포인터로도 접근이 가능하다.

 

 

이전 챕터에서 잠깐 언급된 고용 형태에 따른 클래스들의 상속과 컨트롤 클래스의 개선에도 적용이 가능하다

https://code-jh.tistory.com/100

class Employee
{		// 여러 고용 형태를 한 번에 다루기 위한 Employee 클래스 제작
private:
    char name[100];
public:
    Employee(char* name)
    {
        strcpy(this->name, name);
    }
    void ShowYourName() const
    {
        cout << "name: " << name << endl;
    }
};
class PermanentWorker : public Employee // Employee 객체 상속
{
private:
    int salary;	// 월 급여
public:
    PermanentWorker(char* name, int money)
        : Employee(name), salary(money)
    {}
    int Getpay() const
    {
        return salary;
    }
    void ShowSalaryInfo() const
    {
        ShowYourName();
        cout << "salary: " << GetPay() << endl << endl;
    }
};
class EmployeeHandler
{
private:
    Employee* empList[50];
    int empNum;
public:
    EmployeeHandler() : empNum(0)
    {}
    void AddEmployee(Employee* emp)
    {
        empList[empNum++] = emp;
    }
    void ShowAllSalaryInfo() const
    {
    /*	for(int i = 0; i < empNum++; i++)	// Employee 객체이기 떄문에 ShowSalaryInfo 에 접근할 수 없다.
            empList[i]->ShowSalaryInfo(); */
    }
    void ShowTotalSalary() const
    {
        int sum = 0;
    /*  for(int i = 0; i < empNum; i++)		// Employee 객체이기 떄문에 ShowTotalSalary 에 접근할 수 없다.
            sum+= empList[i]->GetPay(); */
        cout << "salary sum: " << sum << endl;
    }
    ~EmployeeHandler()
    {
        for(int i = 0; i < empNum; i++)
            delete empList[i];
    }
};

 

 

AddEmployee() 에서 Employee 객체 리스트를 생성했지만, 

인자로 PermanenteWorker 객체도 받을 수 있다.

이는 PermanentWorker 객체가 Employee 를 상속 받았기 때문이고,

해당 리스트가 주소로 이루어졌기 때문에 가능하다(포인터 또는 참조)

 

이렇게 부모 객체의 주소임에도 불구하고, 자식 객체가 허용이 되는 이유

상속은 받은 객체의 메모리 레이아웃시작 주소에는 항상 상속받은 부모 부분의 주소가 존재해서

암시적으로 형변환이 일어나는 것이다

그리고 이걸 업캐스팅이라고 한다.

(반대로 부모 객체를 자식 객체의 주소로 형변환 시킨는 다운캐스팅도 있는데,

다운캐스팅은 dynamic_cast 또는 static_cast 키워드와 같이 명시해야 유효하며, 그 외는 컴파일 에러가 발생한다)

 

부모 모임에 참석하고 싶은 자식 객체가 등에 업힌 부모를 앞장세워 모임에 가는 것으로,

안타까운 점은 이 모임에서 자식 객체는 자의로 할 수 있는 것은 아무것도 없어,

오로지 등에 업힌 부모만 행동을 할 수 있다.

불효자식의 인과응보 메타로 볼 수 있다.

 

그럼 주소가 아닌 경우엔 어떻게 되는 것인가?

 

주소가 아닌 부모 객체에 자식 객체를 대입하려는 경우 이는 슬라이싱이라고 한다.

이 떄는 자식이 가지고 있던 값은 잘려나간 채 부모의 값만 전달이 되기 때문에, 자식 클래스의 정체성이 사라진다.

 

이는 부모 모임에 참석하려다 부모님만 들어가고, 불효 자식은 입구컷 받아버리는 상황인 셈이다.

// 임시직: TemporaryWorker

class TemporaryWorker : public Employee
{
private:
    int workTime;	// 이 달에 일한 시간의 합계
    int payPerHour;	// 시간당 급여
public:
    TemporaryWorker(char* name, int pay)
        : Employee(name), workTime(0), payPerHour(pay)
    {}
    void AddWorkTime(int time)	// 일한 시간의 추가
    {
        workTime += time;
    }
    int GetPay() const	// 이 달의 급여
    {
        return workTime * payPerHour;
    }
    void ShowSalaryInfo() const
    {
        ShowYourName();
        cout << "salary: " << GetPay() << endl << endl;
    }
};

이렇게 임시직 클래스인 Temporary 또한 Employee 를 상속 받음으로써 위와 같은 매커니즘이 적용된다.

class SaleWorker : public PermanentWorker
{
private:
    int saleResult;	// 월 판매실적
    double bonusRatio;	// 상여금 비율
public:
    SaleWorker(char* name, int money, double ratio)
        : PermanentWorker(name, money), saleResult(0), bonusRatio(ratio)
    {}
    void AddSalesResult(int value)
    {
        salesResult += value;
    }
    int Getpay() const
    {
        return PermanentWorker::GetPay() + (int)(saleResult * bonusRatio);
                // PermanenetWorker 의 Getpay 함수 호출
                // 함수 오버라이딩
    }
    void ShowSalaryInfo() const
    {
        ShowYourName();
        cout << "salary: " << GetPay() << endl << endl;	//SalaWorker 의 GetPay 함수가 호출
                             	  			 // 함수 오버라이딩
    }
};

 

오버라이딩은 상속받은 자식 클래스부모 클래스의 메서드를 본인 내부에 재정의하는 것이다.

자식 클래스를 통해서 부모 클래스의 메서드를 호출할 때, 항상 부모에게 의존해서 호출해야 된다.

하지만 오버라이딩을 통애 자식 클래스에서 재정의를 한다면,

자식 클래스를 통해서 부모 클래스와 동일한 메서드를 호출하더라도, 우선적으로 자식인 오버라이딩 한 함수가 호출된다.