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 |