[C++ 객체지향] 상속 및 추상화
객체지향 프로그래밍 언어에서는 상속이라는 키워드가 존재하는데
이름 그대로 부모 클래스의 멤버 함수와 멤버 변수를 그대로 물려받는것을 의미 한다.
-> 이 떄 물려받은 클래스를 자식 클래스 혹은 파생 클래스라 한다.
장점은 공통되는 특징(멤버들)들을 다시 작성할 필요없이 필요한 기능이 담긴 클래스를 상속받으면 된다.
-> 코드의 재사용성 UP
사용하는 방법은 간단하다
class 클래스 이름 : 접근지정자 부모 클래스명
{
//내용.
};
즉 Human이라는 클래스가 존재하고, Student라는 클래스가 존재한다고 보자.
상속을 정하는 기준에서 유명한 방법인 IS-A, Has-A 방법이 있다. A와 B클래스가 있고 상속관계를 정하려고 해본다.
IS-A : B는 ~A이다. (상속 관계)
Has-A : B는 A를 가진다. (포함 관계)
-> 포함 관계 ex) 인벤토리 클래스를 기사(Knight)클래스가 가진다. -> Has-A
여기서 A를 사람. B를 학생이라고 하면 HAS-A방법이 아닌 IS-A방법이 알맞다.
class Human
{
protected:
Human(int age, int height) : m_age(age),m_height(height) {}
virtual ~Human() {}
public:
void PrintAge() { std::cout << m_age << std::endl; }
public:
int m_age;
float m_height;
//..등등
};
class Student : public Human
{
public:
Student(int mathGrade, int age, int height) : Human(age, height), m_mathGrade(mathGrade)
{
}
public:
int m_mathGrade;
};
이렇게 부모클래스 Human을 상속받은 Student는 생성자가 호출되기 전 부모 생성자를 호출하여 부모의 초기화부터
진행한다.
그리고 부모의 멤버에 접근할 수 있는지 확인해보면
부모의 멤버를 모두 물려받았기에 접근이 가능하다.
-> 물론 접근 지정자가 public으로 열려있기에 가능.
그런데 부모 클래스를 자식 클래스가 상속받을 때 사용되는 접근 지정자는 무슨 의미일까.
class Student : public Human
그것은 위의 그림과 같이
public 으로 상속받은 자식 클래스는 부모의 public은 그대로 public, protected도 그대로 proteced.private도 그대로 private이다.
즉 부모 클래스의 멤버에서 사용한 접근 지정자가 온전히 그대로 물려받는것.
다만 protected와 private 상속은 다르다.
protected상속은 private 지정자인 멤버를 제외한 모두 protected로 변하게 된다. 그렇기에 외부에서는 호출할 수 없고. 자식 클래스내에서만 호출이 가능.
priviate상속은 모두 다 private 상속이다. 즉 부모 클래스에서 public,protected의 지정자를 가진 멤버들이라 할지라도
자식 혹은 외부에서는 접근이 불가능하다.
다중 상속
C++는 다중 상속을 지원한다. (2개 이상의 부모 클래스를 상속할 수 있음)
다중 상속은 편리해보이지만 당연히 여러가지 문제점이 있다.
1. 여러 부모 클래스중 멤버 이름이 겹치는 경우.
class A
{
protected:
int GetID() { return m_id; }
private:
int m_id = 10;
};
class B
{
protected:
int GetID() { return m_id; }
protected:
int m_id = 20;
};
class C : public A, public B
{
};
위의 코드는 오류가 나지않는다. 같은 이름의 멤버 함수,변수라고 할지라도 어떤 클래스의 함수인지는 명확하기 떄문이다.
다만 사용자가 함수를 호출할때는 조심해야 한다.
-> 에러를 바로 잡아주긴 한다.
이렇게 GetID라는 함수가 모호하다고 뜬다. 즉 부모 클래스의 A,B중 어느 함수를 호출해야할지 컴파일러 입장에선
알 수가 없는 것.
그렇기에 어떤 부모의 함수인지를 명확히 명시해야 한다.
2. 다이아몬드 상속의 문제
사실 이게 제일 문제.
위와 같이 A클래스를 상속받은 B와 C클래스가 있다고 보자.
그리고 D클래스는 B와 C를 상속받는데 여기서 발생하는 문제점.
이 경우는 A라는 클래스를 부모로 가진 B와 C클래스가 D에게 상속된다는게 문제다.
class A
{
protected:
int GetID() { return m_id; }
public:
int m_id = 10;
};
class B : public A
{
};
class C : public A
{
};
class D : public B,public C
{
public:
};
위의 그림과 같은 클래스의 상속구조 코드를 보자.
이 상태에서 D클래스의 객체를 생성하여 m_id에 접근해보면
컴파일러 입장에서는 누구의 m_id를 가리키는건지 알수가없어 에러를 준다.
위의 문제는 부모 클래스에 접근 지정자를 사용하면 해결은 가능하다.
다만 이걸 해결했다고는 할 수 없는게 문제는 의미가 같은데 메모리가 더 붙어서 올라간다는 것.
실제로 D클래스의 크기와 따로 K클래스를 정의해 B만 상속받게 하여 서로 크기를 비교해보면
class D : public B,public C
{
public:
};
class K : public B
{
};
int main()
{
D d;
K k;
std::cout << sizeof(d) << std::endl;
std::cout << sizeof(k) << std::endl;
return 0;
}
D객체는 8바이트이고 k객체는 4바이트인것을 알 수 있다.
인터페이스(Interface)
다중 상속은 단점만 존재하는것 같다.
하지만 장점도 존재하는데 C++에서는 C#과 같은 인터페이스 문법이 존재하지 않는다.
여기서 C#에서는 안되는 다중상속을 C++에서 인터페이스와 비슷하게 사용할 수 있다. 바로 다중상속과 순수 가상함수를 이용하는 방법이다.
class Human
{
protected:
int m_hp = 100;
int m_mana = 50;
};
class IFlyable
{
public:
virtual void Fly() abstract;
};
class Knight : public Human, public IFlyable
{
public:
void Fly() override
{
std::cout << "기사 날다~" << std::endl;
}
};
위와 같이 단순히 인터페이스의 문법과 거의 유사하게 사용할 수 있다.
여기서 인터페이스와 같은 역할을 하는 클래스는 이름앞에 대문자로 I를 붙이는것도 좋은 방법이다.
-> 인터페이스라는 것을 이름에서 명시.
혹은 define 전처리기를 이용하여 인터페이스를 더욱 확실히 명시하는것도 좋은 방법.
#define Interface class
class Human
{
protected:
int m_hp = 100;
int m_mana = 50;
};
Interface IFlyable
{
public:
virtual void Fly() abstract;
};
class Knight : public Human, public IFlyable
{
public:
void Fly() override
{
std::cout << "기사 날다~" << std::endl;
}
};
추상화
간단히 말해 공통의 속성이나 기능을 묶어 이름을 붙이는 것이다.
- 객체지향에서 추상화라는 개념은 객체의 공통된 속성과 행위를 추출하는 것을 의미한다.
예로들어 SM5, 아우디, 봉고차, K5등등..(차 종류를 잘모른다..)
위의 각각의 차 객체들을 하나로 묶을려고 할 때 우리는 '자동차'라는 개념으로 추상화할 수 있다.
즉 코드로 표현해보면 Car라는 클래스가 존재하고 모든 차들이 가지고있는 차 엔진,페달등등.. 공통된 속성과 기능들을
묶어서 하나의 추상화 클래스를 만들 수 있다.
class Car
{
protected:
virtual void StartEngine() abstract;
};
class HyundaiCar : public Car
{
public:
void StartEngine() override
{
std::cout << "시동키기" << std::endl;
}
};
class Granger : public HyundaiCar
{
public:
void StartEngine() override
{
std::cout << "그랜져 시동 키기" << std::endl;
}
};
차의 공통된 기능들이 잘 생각나지 않아 시동을 켜는 함수만을 넣었는데.
위와 같이 Car가 가지는 공통된 기능이 있을 것이고 현대차는 그 기능이 존재하지만 차 브랜드마다 그 기능이 조금씩 다를 것이며(맞나?) 현대차의 그랜져도 세부적으로는 그 기능이 다를것이다.
중요한것은 '자동차'라는 기능을 한데 묶어 그것을 상속함으로서 공통된 기능을 가져갈 수 있으며, 각 차들이 가지는 기능을 재정의하여 다형성까지도 가져갈 수 있는 것.