프로그래밍 지식/C++

C++문법 / 상속, Inheritance - 1

게임이 더 좋아 2021. 12. 12. 19:11
반응형
728x170

상속은 OOP하면 가장 먼저 생각나는 특성으로

이미 정의한 클래스를 물려받아 새로운 클래스를 정의하는 기법이다.

이는 재사용성을 높이고 

반복을 제거해서 효율성을 높여주며

계층별 다형성을 구현할 수 있게 한다.

참 장점이 많은 아이다.

 

천천히 알아보자

 


 

밑도 끝도 없이 예시부터 보자

 

class Car
{
private:
    char name[12];

public:
    Car(const *aname){
        strcpy(name, aname);
    }
    
    void init(){
        cout << name;
    }
    
};



class Truck : public Car
{
private:
    double length;
    int wheels

public:
    Truck(const char *aname, double alength, int awheels) : Car(aname){
        length = alength;
        wheels = awheels;
    }
    
    
};

 

Car : 부모클래스

Truck : 자식클래스

 

여기서 생성자들을 살펴보면

Truck의 생성자에서 부모클래스의 멤버 변수들을 초기화하는데

중요한 것은 실제로 자식 클래스에서 멤버 변수에 접근가능할 지라도

부모 클래스의 변수는 부모클래스 생성자를 호출해서 초기화하는 것이 일반적이다.

 

하지만 생성한다고해서

윗 부분을

 

Truck(const char *aname, double alength, int awheels) : Car(aname){
        length = alength;
        wheels = awheels;
    }
    
    /// 아래처럼 바꾸면 안된다.

Truck(const char *aname, double alength, int awheels){
        Car(aname)
        length = alength;
        wheels = awheels;
    }

 

저렇게 수정한다면 그냥 이름모를 임시 객체를 생성할 뿐이다.

-> 이렇게 하지 말라는 말이다.

 

만약 비워둔다고 해도 상관없다.

비어있으면 부모클래스의 디폴트 생성자가 자동으로 호출되기 때문이다.

**다만 디폴트 생성자를 정의해야 한다.

 

아무튼 상속받은 멤버는 반드시 초기화 리스트에서 부모의 생성자로 전달해서 

초기화하는 것을 부모에게 위임하는 것이 원칙이다.

 


 

부모 클래스에서 모든 멤버는 자식 클래스로 상속된다.

하지만 상속받는 것과 사용하는 것은 다른 문제다.

자식은 부모의 멤버를 소유하지만 부모가 허용하지 않은 멤버를 접근할 수는 없다.

 

즉, 부모클래스에서 private 처리된 멤버를 자식이라고 접근할 수는 없는 것이다.

실제로 상위 클래스에서 private 처리한 것은

부모 클래스에서만 사용하기에 써놓은 것이고

protected라는 접근자를 지정하여

자식 클래스가 접근할 수 있는 멤버를 나눈다.

 

private -> 해당 클래스의 객체만 접근 가능

protected -> 해당 클래스의 객체 또는 자식 클래스의 객체만 접근 가능

 

다시 말해서 상속을 하는 것이랑 실제로 우리가 사용하는 것이랑은 접근자 때문에 달라지는 것이다.

 

조금 더 접근자에 대해 알아볼까?

 

사실 3가지가 있다.

위에 2가지와

public까지 있다.

public -> 모든 클래스 객체에서 접근 가능

 

우리는 멤버변수나 함수에게만 접근 제한자를 붙인다고 생각하지만 

사실 클래스를 선언할 때도 지정한다.

이를 우리는 상속 접근 제한자라고 한다.

 

하지만 해석은 조금 다르게 되는데

 

상속 액세스 지정자가 public 이라면 부모 클래스의 속성이 그대로 유지된다.

즉, public이면 public, private 이면 private, protected 면 protected 그대로

유지되어 상속되게 된다.

 

하지만 private, protected가 상속 액세스 지정자가 된다면

해당 부모 클래스의 모든 멤버가 private 또는 protected로 바뀐다.

 

상속 액세스 지정자의 디폴트 자체는 private이나

일반적으로 상속이란 부모의 접근 속성이 유지되는 public 상속을 뜻한다.

 

class Truck : public Car
// Car 클래스의 멤버들의 접근제한자를 그대로 상속받는다는 뜻이다.

 


 

근데 멤버함수 중에 상속에 따르지 않는 것들이 있다.

 

1. 생성자, 소멸자

2. 대입 연산자

3. 정적 멤버

4. 프렌드 선언

 

왜냐하면 해당 클래스의 고유한 처리를 하기 때문에 상속의 대상에서 제외된다.

즉, 해당 클래스만을 위한 것이기 때문에 굳이 상속할 필요가 없다는 말과 같다.

 

반대로 말하자면

이런 특수한 멤버들 빼고는 자식 클래스는 모두 상속받아야 한다는 말이다.

취사선택할 수 없다.

 

이 때 자식은 필요없는 멤버가 있다면 2가지 결정을 할 수 있다.

 

1. 전혀 필요하지 않다 -> 사용하지 않는다.

2. 필요하지만 다른 용도(다형성)으로 쓰겠다. -> 오버라이딩

 


 

상속에 대해서 더 나아가면

다중 상속, Multiple Inheritance를 지원한다.

즉, 상속을 여러가지 받을 수 있다.

 

다시 말하면 나라는 인간은 엄마의 유전자 + 아빠의 유전자이다.

어머니, 아버지를 상속받아서 내가 생겼다.

즉, 어머니의 특성, 아버지의 특성을 모두 물려받아 자랑스런 아들이 되었다.

 

여러 가지 기능을 만든 뒤에 하나로 통합하면 비용도 절감되고 편의성이 향상된다.

한 번에 모두 집어넣으면 힘들 수도 있지만

기능 별로 구성하여 필요한 기능만 넣어서 새로운 물건을 만들 수 있다.

 

예를 들어보자

날짜와 시간을 만들고

날짜 시간을 상속받는 클래스를 만들었다.

class Date
{
protected:
    int year, month, day;
public:
    Date(int y, int m, int d){year = y; month = m; day = d;}
    void OutDate(){printf("%d%d%d", year, month, day);}
};


class Time
{
protected:
    int hour,min,sec;
public:
    Time(int h, int m, int s){hour=h; min=m; sec=s;}
    void OutTime(){printf("%d%d%d", hour,min,sec);}
};


class DateTime : public Date, public Time
{
private:
    bool bEngMessage;
    int milisec;
public:
    DateTime(int y, int m, int d, int h, int min, int s, int ms, bool b = false)
        :Date(y,m,d), Time(h,min,s){
        milisec = ms;
        bEngMessage = b;
    }
    
    void OutNow(){
        printf(bEngMessage ? "Now is" : "지금은");
        OutDate();
        putch(' ');
        OutTime();
        printf(".%d", milisec);
        puts(bEngMessage ? ".":"입니다.");
    }
};

 

메인함수에서 돌려보면

int main()
{
    DateTime now(2021,12,12,19,30,23,44);
    now.OutNow();
}

 

결과는?

지금은 2021/12/12 19:30:23.44 입니다.

 

잘 나온다.

다중 상속으로 날짜와 시간을 받아서 사용할 수 있었으며 생성자도 부모 클래스에 생성자를 위임하여

현재 클래스를 잘 초기화시켰다.

 

**클래스 선언시 상속 순서대로 생성자도 그 순서로 호출하는게 일반적이다.

 


 

함수에만 virtual이 있는 것이 아니다.

사실 클래스에도 virtual이 선언되는데 알아보자.

 

상속이 좋은 것이 맞지만 클래스끼리 여러 단계로 다중 상속하다 보면

한 클래스를 간접적으로 여러 번 상속받을 때가 있고이렇게 되면

알게 모르게 멤버 이름으로 충돌이 나는 경우가 잦다.

 

예를 들어보자

class A
{
protected:
    int a;
public:
    A(int aa){a = aa;}
};

class B : public A
{
protected:
    int b;
public:
    B(int aa, int ab) : A(aa) { b = ab;}
};

class C: public A
{
protected:
    int c;
public:
    C(int aa, int ac) : A(aa) { c = ac;}
};


class D : public B, public C
{
protected:
    int d;
public:
    D(int aa, int ab, int ac, int ad) : B(aa,ab), C(aa,ac) { d = ad;}
    void fD(){
        b = 1;
        c = 2;
        a = 3; // ambiguous 발생
    }
};


int main()
{
    D d(1,2,3,4);
}

 

A,B,C,D 클래스를 생성하였고

B는 A를

C는 A를

D는 B,C를 상속받는다.

이렇게 되면 간접적으로 A를 2번 상속받는 결과가 나온다.

 

즉, 불필요한 상속이 발생한다. 물론 받은 a는 B::a 와 C::a로 구분할 수 있지만

이름이 같은 것은 피하는 것이 상책이다.

아무튼 상속을 하다보면 모호함이 생기는 경우가 있는데(멤버 a가 그렇다)

클래스는 두 번 상속받더라도 멤버는 하나만 상속받게끔 하면 된다.

 

그렇게 멤버를 한 번만 상속하는 클래스를 가상 기반 클래스, Virtual Base Class라고 한다.

상속문의 기반 클래스 앞에서 virtual 키워드를 쓰면 된다.

 

즉, 아래와 같다.

class B : virtual public A
...

class C : virtual public A
...

 

가상 클래스로 지정했다면 다중 상속하더라도 내부적으로 A의 상속은 한 번만 된다.

즉, 모호함이 사라지는 것이다.

 

가상 기반 클래스로부터 상속받는 중간 클래스는 부모의 생성자를 호출하지 않는다.

다시 말해서 D 생성자의 초기화 리스트에서 B(aa, ab)를 호출하는데

B의 생성자는 A의 생성자를 건너 뛰는 것이다.

중간, 사이에 낀 클래스는 가상 기반 클래스의 멤버인 a를 직접 소유하지 않게 된다.

즉, 상속받은 멤버의 초기화는 최종 클래스가 직접 처리하게 된다.

그래서 D의 생성자에서는 B,C를 건너뛰어서 A의 생성자를 호출할 수 있게 되는 것이다.

 

사실 상속이란 부모 자식 간에만 일어나지만

다중 상속의 virtual 기반 클래스에서는

현실에서 일어나듯이 조상(ancestor)의 생성자를 호출할 수 있게 되었다.

**하지만 다중 상속은 그리 권장할 만한 사항은 아니다.

 


 

???기능을 나누어서 활용해서 좋다고할 때는 언제고???

 

물론 다중 상속으로 완성된 클래스를 많은 곳에 재사용할 수 있다는 점에서는 좋다.

다만 실제로 멤버의 중복문제가 있고 계층이 복잡해지면

부모 타입의 포인터로 자식 객체를 가리킬 수 없는 민감한 문제가 있어서 다형성을 구현하는데 문제가 생긴다.

 

물론 이를 해결할 수는 있고 다중 상속을 이용해서 아름다운 라이브러리도 있지만

사실 전체를 궤뚫고 있지 않은 이상 한 번에 이해하기 어려운게 사실이다.

 

위에선 이중이었지만 삼중, 사중, 다중 상속도 가능하여 클래스 계층이 극단적으로 복잡해지기도 한다.

 

이런 복잡성 >> 실용성의 문제가 제기되어서 다중상속이 추천할 사항이 아니게 된 것이다.

 

어쩔 수 없이 소개할 때는 좋다고 칭찬했지만

실전에서는 그렇게 추천할 사항이 아닌 것이다.

728x90
반응형
그리드형