본문 바로가기
C & C++/C++

[C++] 상속

by 거북이 코딩 2024. 8. 5.

Ch 03. 상속

상속의 기본

자식 클래스가 부모 클래스의 필드와 메소드를 포함한 속성을 물려받는 것이 상속이다.

//문법
class 자식클래스 : public 부모클래스 // public 변경 가능
{
	//클래스 명세
}
class Base
{
public:
    int num;
    void func()
    {
        cout << num << endl;
    }
};

class Derived : public Base // num 과 func를 사용할 수 있다.
{
   
};

부모 클래스의 private 에 저장되어 있는 속성은 자식 클래스에서 접근할 수 없다. 그래서 외부에서는 접근이 제한되지만 자식클래스에는 접근을 허용하고 싶다면 protected 키워드를 사용해야 한다.

class Base
{
protected: // 외부에서는 접근 불가
    int num;
    void func()
    {
        cout << num << endl;
    }
};

class Derived : public Base // num 과 func를 사용할 수 있다.
{
   
};

자식 클래스의 객체를 생성하면 우선 부모클래스가 생성되고 그 후 자식클래스가 생성된다. 생성자도 같은 순서로 호출되는데 만약 부모클래스의 특정 생성자를 호출하고 싶다면 자식클래스의 이니셜라이저에서 호출해 주면 된다.

class Base
{
public:
    Base() // 디폴트 생성자
    {
        cout << "Base" << endl;
    }

    Base(int num) : num(num)
    {
        cout << "Base(" << num << ")" << endl;
    }

    int num;

    void func()
    {
        cout << num << endl;
    }
};

class Derived : public Base
{
public:
    Derived() // 부모 생성자를 지정하지 않으면 부모의 디폴트 생성자를 실행해준다.
    {
        cout << "Derived" << endl;
    }

    Derived(int num) : Base(num) // 자식 생성자에서 어떤 부모 생성자를 호출할지 결정해준다
    {
        cout << "Derived(" << num << ")" << endl;
   
};

부모의 생성자를 상속받고 싶다면 using Base::Base; 로 상속받을 수 있다. (Base = 부모클래스)

클래스가 소멸될 때는 자식클래스의 소멸자가 호출된 후 부모클래스의 소멸자가 호출된다.

class Base
{
public:
    Base()
    {
        cout << "Base" << endl;
    }

    Base(int num) : num(num)
    {
        cout << "Base(" << num << ")" << endl;
    }

    virtual ~Base() // 부모 클래스가 될 가능성이 있는 녀석들의 소멸자는 virtual을 추가해준다
    {               // VIRTUAL 함수여야 부모클래스가 가리키는 자식클래스가 올바르게 소멸한다
        cout << "~Base" << endl;
    }

    int num;

    void func()
    {
        cout << num << endl;
    }
};

class Derived : public Base
{
public:
    // 생성자를 상속하고 싶은 경우 using을 이용한다.
    //using Base::Base;

    Derived() // 부모 생성자를 지정하지 않으면 부모의 디폴트 생성자를 실행해준다.
    {
        cout << "Derived" << endl;
    }

    Derived(int num) : Base(num) // 자식 생성자에서 어떤 부모 생성자를 호출할지 결정해준다
    {
        cout << "Derived(" << num << ")" << endl;
    }

    ~Derived()
    {
        cout << "~Derieved" << endl;
    }
};

void func(Base& b)
{

}

int main()
{
    Base b;
    b.num = 10;
    b.func();

    Derived d; // 부모의 생성자가 먼저 호출 된다. 
    d.num = 20;  // 부모의 num
    d.func(); // 부모의 func

    // 부모 클래스는 자식 클래스를 가리킬 수 있다
    Base* base = new Derived;

    // 자식 파괴자가 먼저 호출된다.
    delete base;

    // 부모가 자식을 가리킬 수 있기 때문에 부모 클래스를 타입으로 가지는 파라메터에
    // 자식 객체를 넘겨줄 수 있다
    func(d);
}

가상 함수

자식 클래스에서 부모클래스의 함수와 프로토 타입이 같은 함수를 재정의 하는 것을 오버라이딩 했다고 한다.

class Base
{
public:
	void func()
	{
		cout<<"Base func"<<endl;
	}
};
class Derived : public Base
{
public:
	void func() override // override 는 붙이지 않아도 오류가 나지 않지만 붙이면 가독성이 좋고
	{                    // 오류를 예방할 수 있다.
		cout<<"Derived func"<<endl;
	}
};

함수 앞에 virtual 키워드가 붙은 함수를 가상 함수 라고 한다. 가상 함수는 부모 클래스가 자식 클래스의 객체를 가리킬 때 오버라이딩 된 함수가 호출되게 한다.

class Base
{
public:
	virtual void func() // 자식클래스의 오버라이드 함수도 virtual 화 된다.
	{
		cout<<"Base func"<<endl;
	}
};

class Derived : public Base
{
public:
	virtual void func() override // 관례상 virtual을 붙여주는게 좋다.
	{
		cout<<"Derived func"<<endl;
	}
};

class Derived1 : public Derived
{
public:
	void func() override
	{
		cout<<"Derived1 func"<<endl;
	}
};
void main()
{
	Base& b = new Derived1;
	b.func(); // Derived1 func
}

자식 클래스에서 함수를 오버라이드 했을때 부모 클래스의 함수를 가렸다고 표현한다. hide, shadow.

부모의 함수를 호출하고 싶다면 범위 지정 연산자를 사용한다.

Base::func();

함수의 이름이 같아서 가려진 부모 클래스의 함수를 사용하고 싶다면 using을 사용하면 된다.

class Base
{
public:
	void func()
	{
		cout<<"Base func"<<endl;
	}
};

class Derived : public Base
{
public:
	using Base::func; // 사용할 수 있게 된다.
	void func(int num)
	{
		cout<<"Derived func"<<endl;
	}
};
void main()
{
	Derived d;
	d.func(); // Base::func();
	d.func(10); // Derived::func(10);
}

using으로 부모클래스 속성 범위 수정하기

class Base
{
public:
	int num;
};

class Derived : public Base
{
private:
	using Base::num;
public:
};

정적 결합, 동적 결합

정적 결합(static binding) : 컴파일 타임 중 결정, ex) 오버로딩

void func(int x) // 컴파일 시 함수 이름이 변경 name mangling, 정적 결합
{
}

void func(int x, int y) // 컴파일 시 함수 이름이 변경 name mangling, 정적 결합
{
}

int main(){
	func(10); // 컴 파일 시 어떤 함수가 호출 될지 결정된다.
	func(10, 20); // 정적 결합
}

네임 맹글링은 이름이 같은 함수를 구분하기 위해 컴파일 시 함수의 이름을 함수의 프로토타입에 맞게 재설정해주는 것을 말한다.

동적 결합(dynamic binding) : 런타임 중 결정, ex) 오버라이딩

class Animal
{
public:
    virtual void eat() const
    {
        std::cout << "냠" << std::endl;
    }
};

class Cat : public Animal
{
public:
    virtual void eat() const override
    {
        std::cout << "냥" << std::endl;
    }
};

class Dog : public Animal
{
public:
    virtual void eat() const override
    {
        std::cout << "멍" << std::endl;
    }
};

int main(){
	Animal* a = new Cat;
	Animal* b = new Dog;
	a->eat(); // 런타임 중 실제 타입에 따라 어떤 eat()이 호출될지 결정된다.
	b->eat(); // 동적 결합

가상함수가 있는 클래스는 객체가 만들어지면 가상함수 테이블이 생성된다. 그리고 런타임 중에 가상함수 테이블로 가서 적절한 함수를 호출한다.

추상 클래스, 순수 가상 함수

한 가지 이상의 순수 가상 함수를 가지고 있는 클래스를 추상 클래스라고 한다. 추상 클래스는 인스턴스화(객체 생성) 할 수 없다.

순수 가상 함수는 오버라이딩을 위해 존재하는 함수라고 볼 수 있다. 직접 사용될 수 없다.

virtual 반환형 함수이름(파라미터) = 0; // = 0 을 붙여 순수 가상 함수로 만든다.

추상 클래스를 상속받은 자식 클래스가 순수 가상 함수를 오버라이드하지 않는다면 자식 클래스도 추상클래스가 되어 인스턴스화할 수 없다.

어떤 클래스가 데이터 없이 순수 가상 함수만 포함하고 있다면 인터페이스라고 부른다.

// 추상 클래스
// 순수 가상함수만 존재하기 때문에 인터페이스라고 볼 수 있음
class Shape
{
public:
virtual double getArea() const = 0;
virtual ~Shape() {}
};

class Circle : public Shape
{
private:
double _radius;

public:
Circle(double radius)
: _radius(radius)
{

}

virtual double getArea() const override
{
    return _radius * _radius * 3.14;
}

};

class Rectangle : public Shape
{
private:
double _width;
double _height;

public:
Rectangle(double width, double height)
: _width(width), _height(height)
{

}

virtual double getArea() const override
{
    return _width * _height;
}

};

class AreaAverage
{
private:
double _total = 0;
int _size = 0;

public:
double operator()(Shape& shape)
{
_size += 1;
_total += shape.getArea();
return _total / _size;
}
};

int main()
{
//Shape s;  // 추상 클래스는 생성할 수 없다
Circle c(10);
cout << c.getArea() << endl;

Rectangle r(10, 20); cout << r.getArea() << endl;

// 추상 클래스는 생성할 수 없지만 가리킬 수는 있다. Shape* s0 = &c; Shape& s1 = r;

cout << s0->getArea() << endl; cout << s1.getArea() << endl;

AreaAverage aavg; cout << aavg(c) << endl; cout << aavg(r) << endl;

}

private, protected 상속

private, protected 상속은 자주 사용하지 않기 때문에 신중하게 사용해야 한다.
IS-A 관계 : 도형 - 원, 과일 - 사과
IS-A관계라면 주로 public으로 상속된다.
HAS-A 관계 : 사각형 - 선분
HAS-A관계라면 주로 private이나 protected로 상속된다.
별다른 범위 지정자 없이 상속하면 클래스의 경우 private으로 상속된다. (구조체는 public으로 상속) private으로 상속하게 되면 부모의 public과 protected도 모두 private으로 상속하여 외부에서 사용할 수 없게 된다.

#include <iostream>
#include <deque>
#include <algorithm>

using namespace std;

class Shape
{

};

// Shape - Rectangle : is-a 관계
class Rectangle : public Shape
{

};

// deque 구현이 맞습니다. 강의에 나온 부분은 잘못 됐습니다
// Queue - deque : has-a 관계
class Queue0
{
private:
    deque<int> v;
public:
    void push(int value)
    {
        v.push_back(value);
    }

    void pop()
    {
        v.pop_front();
    }

    int top()
    {
        return v.front();
    }
};

// public 상속은 deque의 모든 인터페이스를 public으로 상속한다.
// private 상속은 deque의 모든 인터페이스를 private으로 상속한다.
// 따라서 Queue1에서는 deque의 인터페이스를 사용할 수 있지만 외부에서는 사용할 수 없다
// 왠만하면 사용하지 말자.
// deque의 protected 멤버 함수를 사용하고 싶은 경우 사용하면 좋다
// 지정자를 생략하면 private 상속(struct의 경우는 public)

class Queue1 : private deque<int>
{
public:
    void push(int value)
    {
        push_back(value);
    }

    void pop()
    {
        pop_front();
    }

    int top()
    {
        return front();
    }
};

// deque의 인터페이스를 Queue2의 자식에게까지 공개하기 위해 protected 상속을 한다
class Queue2 : protected deque<int>
{
public:
    virtual void push(int value)
    {
        push_back(value);
    }

    virtual void pop()
    {
        pop_front();
    }

    virtual int top()
    {
        return front();
    }

    virtual ~Queue2()
    {

    }
};

class PriorityQueue : public Queue2
{
public:
    virtual void push(int value) override
    {
        Queue2::push(value);
        push_heap(begin(), end());
    }

    virtual void pop() override
    {
        pop_heap(begin(), end());
        Queue2::pop_back();
    }

    virtual int top() override
    {
        return front();
    }
};

int main()
{
    PriorityQueue pq;
    pq.push(10);
    pq.push(100);
    pq.push(0);

    cout << pq.top() << endl;
    pq.pop();

    cout << pq.top() << endl;
    pq.pop();

    cout << pq.top() << endl;
    pq.pop();
}

다중 상속

다중 상속은 주의 깊게 결정해야 한다.

여러 클래스로부터 상속받는것을 다중상속이라고 하는데, 이때 서로 다른 부모에게 같은 이름의 속성이 있을 때 문제가 발생할 수 있다. 자식 입장에서 어떤 부모의 속성을 사용해야 할지 모호하기 때문이다.

class BaseA
{
public:
    int m = 0;
    void foo()
    {
        std::cout << "BaseA" << std::endl;
    }
};

class BaseB
{
public:
    int m = 0;
    void foo()
    {
        std::cout << "BaseB" << std::endl;
    }
};

class Derived : public BaseA, public BaseB 
{
};

int main()
{
	Derived d;
	d.m; // BaseA의 m인지 BaseB의 m인지 알 수 없다.
	d.foo();

또한 다이아몬드 상속이 일어나는 경우에도 문제가 생긴다.

                class A
                
class B0 : class A,  class B1 : class A

    class C : class B0, class B1 
// class C에게 두개의 A가 생기게 된다.
class Base
{
public:
    int m = 10;
    Base(int m) : m(m)
    {
        std::cout << "Base(" << m << ")" << std::endl;
    }
};

class BaseA : public Base
{
public:
    BaseA() : Base(10)
    {
        std::cout << "BaseA" << std::endl;
    }
};

class BaseB : public Base
{
public:
    BaseB() : Base(20)
    {
        std::cout << "BaseB" << std::endl;
    }
};

class Derived : public BaseA, public BaseB 
{
public:
    Derived() // Derived에게 Base 조부모가 2개 생기게 된다.
    {
        std::cout << "Derived" << std::endl;
    }
};

이를 해결하기 위해서 virtual 상속을 할 수 있다.

class Base
{
public:
    int m = 10;
    Base(int m) : m(m)
    {
        std::cout << "Base(" << m << ")" << std::endl;
    }
};

class BaseA : virtual public Base // virtual을 써서 Base를 한 개만 만든다
{
public:
    BaseA() : Base(10)
    {
        std::cout << "BaseA" << std::endl;
    }
};

class BaseB : virtual public Base // virtual을 써서 Base를 한 개만 만든다
{
public:
    BaseB() : Base(20)
    {
        std::cout << "BaseB" << std::endl;
    }
};

class Derived : public BaseA, public BaseB
{
public:
    Derived()
        : Base(30) 
        // Base를 virtual 상속을 한다면 Base의 어떤 생성자를 호출할지 지정해줘야 한다
    {
        std::cout << "Derived" << std::endl;
    }
};

가상 상속을 하면 중간의 두 클래스가 공통된 하나의 부모만 가지게 되어 문제를 해결할 수 있다.

'C & C++ > C++' 카테고리의 다른 글

[C++] 예외 처리  (0) 2024.08.08
[C++] 형변환  (0) 2024.08.07
[C++] 연산자 오버로딩  (4) 2024.07.16
[C++] 클래스  (0) 2024.07.13
[C++] 함수, 범위, 공간  (0) 2024.07.08