오늘은 클래스에서 굉장히 중요한 상속에 대해 자세히 다뤄볼 생각이다. 바로 가보자.
상속이란?
상속이란 말 그대로 물려받는다는 의미다. 여기서 상속해주는 클래스를 부모 클래스, 기본 클래스, 슈퍼 클래스라 부르고 상속받는 클래스를 유도된 클래스, 자식클래스, 서브클래스로 부른다. 자식 클래스는 부모의 멤버를 사용 가능하다. 초간단 예시를 하나 살펴보자.
// 부모
class Base {
public: // Base type
void f() { cout << "Base::f()" << endl; }
};
// 자식
class Derived : public Base { // 상속 type
public:
void g() { cout << "Derived::g()" << endl; }
void f() { Base::f(); } // 부모의 함수 사용 가능
};
자식의 f() 멤버함수 안에서 부모의 멤버함수를 사용하고 있다. 메인 함수에서 Derived 객체를 선언한 뒤에 f() 함수를 호출하면 "Base::f()"가 출력될 것이다.
상속은 Base 타입과 상속타입에 따라 물려받을 수 있는 멤버의 범위가 달라진다. Base 타입이란 부모 클래스 멤버들의 타입(private, protected, public)이다. 상속 타입이란 상속 받을때의 타입으로, 자식 클래스를 정의할 때 첫번째 줄의 ': public Base' 부분을 말한다.
private vs protected vs public
1) Base type이 private
: 상속 타입과 상관없이 Derived에서 접근 불가
2) Base type이 protected
: 상속 타입과 상관없이 Derived에서 private로 선언
3) Base type이 public
: 상속타입 private/protected -> Derived에서 private
: 상속타입 public -> Derived에서 public
Private로 선언된 멤버는 외부에서 어떤 경우라도 사용할 수 없다.
Protected로 선언된 멤버는 상속받은 경우 자식 클래스에서 private처럼 사용 가능하다.
Public으로 선언된 멤버는 외부에서(상속 포함) public처럼 사용 가능하다.
간단한 예시를 하나 보자. 오류가 나는 경우 위 설명을 보면서 왜 오류가 나는지 잘 생각해보자. 처음에는 좀 헷갈리는 부분이다. Protected와 public의 차이점은 상속을 받지 않은 경우(아래 예시 코드에서 Unrelated 클래스)를 보면 알 수 있을 것이다.
// 부모 클래스
class Base{
private: int priv; // base type is private
protected: int prot; // base type is protected
public: int publ; // base type is public
};
class Derive1 : private Base{
void someMemberFunction(){
cout << priv << endl; // 오류 : private 멤버이므로
cout << prot << endl;
cout << publ << endl;
}
};
class Derive2 : protected Base{
void someMemberFunction(){
cout << priv << endl; // 오류 : private 멤버이므로
cout << prot << endl;
cout << publ << endl;
}
};
class Derive3 : public Base{
void someMemberFunction(){
cout << priv << endl; // 오류 : private 멤버이므로
cout << prot << endl;
cout << publ << endl;
}
};
// 상속 받지 않은 경우
class Unrelated{
Base X;
void anotherMemberFunction(){
cout << X.priv; // 오류 : private 멤버이므로
cout << X.prot; // 오류 : protected 멤버이므로
cout << X.publ;
}
};
다형성
다형성이란 한마디로 '부모클래스 포인터로 자식클래스 객체를 모두 사용 가능함'을 의미한다. Person 클래스와 Student클래스로 예시를 들어보자. 간단한 정의는 아래와 같다.
class Person {
private:
string name; // 이름
string idNum; // 학번
public:
// .. 생략
void print(); // 정보 출력
string getName();
};
class Student : public Person {
private:
string major; // 전공
int gradYear; // 졸업년도
public:
// .. 생략
void print(); // 부모와 동일. 정보 출력
void changeMajor(const string& newMajor); // 전공 변경
};
메인 함수는 아래와 같다.
Person* pp[100]; // Person 100명의 포인터 배열
pp[0] = new Person(...); // Person 1명 추가
pp[1] = new Student(...); // Student 1명 추가
cout << pp[1]->getName();
pp[0]->print(); // Person::print() 호출
pp[1]->print(); // Person::print() 호출
pp[1]->changeMajor("English"); // 오류
pp[1]은 Student로 초기화했기 때문에 마지막 두 줄이 정상적으로 실행돼야 할 것 같은데 Student는 Person처럼 동작하고 있다. 그 이유는 멤버 함수가 정적 바인딩으로 호출되었기 때문이다. pp[1]이 Person을 가리키는 포인터로 선언되었기 때문에, Person의 멤버들이 사용된 것이다.
C++는 유도된 클래스를 호출하는 멤버 함수를 결정할 때 기본적으로 정적 바인딩을 사용한다. 그런데 만약에 멤버 함수에 'virtual' 키워드가 있으면 얘기가 달라진다. virtual 키워드가 있는 함수에 대해서는 호출 시점이 실행시간에서 이루어지는데, 이를 동적 바인딩이라고 한다. 즉, print()를 아래와 같이 재정의하면 된다.
class Person {
virtual void print() {...}
};
class Student : public Person {
virtual void print() {...}
// void print() override {...} 이 방식도 가능
};
위와 같이 자식 클래스에서 가상함수를 재정의해주는 것을 '오버라이딩(overriding)'이라고 한다.
동적 바인딩에서 주의해야할 점은, 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근 가능하다는 점이다. 이게 무슨 말이냐 하면, 위 예시에서 배열의 포인터형이 Person이기 때문에 Person에 있는 멤버 함수에 오버라이딩된 함수에만 접근 가능하다는 뜻이다. 따라서 Person의 print 함수는 Student의 print 함수로 동적 바인딩 될 수 있으나, Student 클래스의 changeMajor 함수는 Person에 없으므로 사용할 수 없다. 즉, 위 예시에서 아래 코드는 여전히 오류가 난다.
pp[1]->changeMajor("English"); // 오류
이렇게 하나 이상의 가상 함수를 가지는 클래스 객체를 가리키는 포인터 변수를 다형화(polymoriphic) 되었다고 한다.
추상 클래스
추상 클래스는 다형성에서 인터페이스만 제공하는 클래스를 말한다. 따라서 추상 클래스는 순수하게 부모 포인터라는 '껍데기'만 제공한다. 만약에 클래스 안에 순수가상함수가 적어도 하나 있다면 이를 추상 클래스라고 한다. 그렇다면 순수가상함수는 무엇일까?
순수가상함수는 함수 본체 대신 "=0" 형태를 사용하는 특수한 함수로, 인터페이스만 제공한다. 아무런 기능이 없기 때문에 객체 생성이 불가능하며 반드시 자식 클래스에서 재정의(override) 해줘야한다. 추상 클래스로 만든 스택 인터페이스를 보면 아래와 같다.
class Stack { // 추상 클래스로 만든 스택 인터페이스
public:
virtual bool isEmpty() const = 0; // 스택이 비엇는가?
virtual void push(int x) = 0; // 스택에 x 삽입
virtual int pop() = 0; // 스택에서 삭제하고 결과로 반환
};
위 인터페이스를 토대로 자식 클래스에서 그대로 구현하면 된다. 예시를 보면 아래와 같다.
class ConcreteStack : Stack { // 스택 구현
public:
virtual bool isEmpty() {...}
virtual void push(int x) {...};
virtual int pop() {...};
private:
//...
};
순수 가상함수는 아래 세 가지만 기억하면 된다.
1) 순수 가상함수는 반드시 부모 클래스에 포함되어야 한다.
2) 반드시 자식 클래스에서 override 해줘야한다.
3) 하나라도 순수 가상함수가 있으면 main함수에서 객체 생성 못함. 자식 클래스를 통해서만 가능.
'CS > 자료구조' 카테고리의 다른 글
[ 자료구조 ] 연결 리스트 : 이중 연결 리스트 (0) | 2022.03.04 |
---|---|
[ 자료구조 ] 연결 리스트(Linked List) : 단일 연결 리스트 (0) | 2022.03.04 |
[ 자료구조 ] 템플릿, 예외처리 (0) | 2022.03.03 |
[ 자료구조 ] 복사 생성자 & 배정 연산자, 얕은 복사 & 깊은 복사, 프랜드 함수 & 클래스, 중복된 헤더 피하기 (0) | 2022.03.01 |
[ 자료구조 ] 자료구조 포스팅을 시작하며 (0) | 2022.03.01 |