객체 지향 프로그래밍(OOP : Object Oriented Programming)의 중요 목적 중의 하나는 재활용할 수 있는 코드를 제공하는 것이다. C++는 클래스를 확장하고 수정하기 위해 단순한 코드 수정보다 더 강력한 클래스 상속(class inheritance)을 제공한다. 기초 클래스(base class)라 부르는 클래스로부터 모든 메소드와 멤버들을 상속받아, 새로운 파생 클래스(derived class)를 만들 수 있게 한다.
예를 들어 탁구 동호회의 회원 정보 클래스 TableTennisPlayer를 설계해보자.
//tabtenn0.h
#include <string>
using std::string;
class TableTennisPlayer
{
public:
TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false);
void Name() const;
bool HasTable() { return hastable; }
void ResetTable(bool v) { hastable = v; }
private:
string firstName;
string lastName;
bool hastable;
};
//tabtenn0.cpp
#include "tabtenn0.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer(const string & fn, const string & ln, bool ht)
: firstName(fn), lastName(ln), hastable(ht)
{ }
void TableTennisPlayer::Name() const
{
std::cout << lastName << ", " << firstName << std::endl;
}
이때 테니스 동호회 회원의 일부가 지역 탁구 대회에 선수로 참가하여 대회에서 거둔 랭킹을 포함하는 클래스를 요구한다고 가정해보자. 이 경우 처음부터 클래스를 작성하지 않고, TableTennisPlayer로부터 파생시킬 수 있다.
클래스 파생시키기
어떤 기초 클래스로 부터 파생된 파생 클래스는 다음과 같은 특성을 갖는다.
- 파생 클래스형의 객체 안에는 기초 클래스형의 데이터 멤버들이 저장된다.
- 파생 클래스형의 객체는 기초 클래스형의 메소드들을 사용할 수 있다.'
즉, 파생 클래스는 기초 클래스의 포함관계라고 생각하면 된다.
//tabtenn0.h
class RatedPlayer : public TableTennisPlayer
{
public:
RatedPlayer(unsigned int r = 0, const char* fn = "none", const string& ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer& tp);
unsigned int Rating() { return rating; }
void ResetRating(unsigned int r) { rating = r; }
private:
unsigned int rating;
};
위 코드는 RatedPlayer 클래스가 TableTennisPlayer 클래스에 기초를 두고 있다는 것을 나타낸다. 또한 public 키워드를 사용함으로써 public 파생(public derivation)이라 한다. public 파생에서는 기초 클래스의 public, private, protected 키워드 그대로 반영된다.
파생 클래스 | 클래스 외부 | |
public | 사용 가능 | 사용 가능 |
private | 사용 불가 | 사용 불가 |
protected | 사용 가능 | 사용 불가 |
생성자: 접근에 대하여
위 코드에서 한가지 이상한 점이 있다. 기초 클래스 TableTennisPlayer의 lastName과 firstName는 private 멤버이고, 저장하는 곳이 TableTennisPlayer의 생성자 뿐이다. 파생 클래스인 RatedPlayer에서는 접근이 불가함으로 lastName, firstName는 RatedPlayer에서 직접 설정할 수 없다. 기초 클래스의 private 멤버에 접근하려면, 기초 클래스의 public 메소드를 사용해야 하고, 특히 파생 클래스의 생성자는 기초 클래스의 생성자를 사용해야 한다.
//tabtenn0.cpp
RatedPlayer::RatedPlayer(unsigned int r, const char * fn, const string & ln, bool ht)
: rating(r), TableTennisPlayer(fn, ln, ht)
{ }
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: rating(r), TableTennisPlayer(tp)
{ }
멤버 초기화 리스트를 사용하여 TableTennisPlayer 생성자를 호출했다. 만약 멤버 초기화 리스트에 기초 클래스의 생성자를 선언하지 않았다면 어떻게 될까? 파생 클래스를 생성할 때 컴파일러는 기초 클래스부터 생성한다. 즉, 기초 생성자가 틀림없이 먼저 생성되므로, 기초 클래스 생성자를 생략하면 컴파일러는 기본 생성자를 사용한다.
//tabtenn0.cpp
RatedPlayer::RatedPlayer(unsigned int r, const char * fn, const string & ln, bool ht)
: rating(r), TableTennisPlayer()
{ }
기본 생성자로 들어가게 되면, 더이상 private 멤버인 lastName, firstName은 설정할 수 없으므로 예상치 못한 상황이 발생할 수 있다. 참고로 두번째 생성자는 기초 클래스의 복사 생성자를 호출한다.
파생 클래스 생성자의 요점은 다음과 같다.
- 파생 클래스 생성자가 호출되면 기초 클래스 객체가 먼저 생성된다.
- 파생 클래스 생성자가 멤버 초기화 리스트를 통해 기초 클래스 생성자에 기초 클래스 정보를 제공해야 한다.
- 파생 클래스 생성자는 파생 클래스에 새로 추가된 데이터 멤버들을 초기화해야 한다.
파생 클래스와 기초 클래스의 특별한 관계
파생 클래스는 기초 클래스와 몇 가지 특별한 관계를 갖는다.
- 파생 클래스 객체는 기초 클래스 메소드들이 private이 아니면 사용할 수 있다.
- 기초 클래스 포인터는 명시적 데이터형 변환 없이도 파생 클래스 객체를 지시할 수 있다.
- 기초 클래스 참조는 명시적 데이터형 변환 없어도 파생 클래스 객체를 참조할 수 있다.
두 번째, 세 번째가 이해가 안갈 수 있는데, 쉽게 설명해서 파생 클래스의 데이터를 별도로 캐스팅하지 않고 기초 클래스가 사용할 수 있다는 뜻이다.
RatedPlayer player(1140, "Kim", "Minju", true);
TableTennisPlayer& rt = player; //참조
TableTennisPlayer* pt = &player; //포인터
rt.Name(); //문제 없음
pt->Name(); //문제 없음
여기서 봐야할 점은, rt, pt 모두 기초 클래스이기 때문에, 파생 클래스의 메소드는 사용할 수 없다.
하지만, 반대로 기초 클래스로 부터 파생 클래스로의 변환은 불가능하다.
TableTennisPlayer player("Kim", "Minju", true);
RatedPlayer& rr = player; //불가
RatedPlayer* pr = player; //불가
간단하게 생각해서 player가 캐스팅 되는 시점에서 멤버 데이터나 메소드를 버릴 순 있어도 추가할 순 없다고 생각하면 된다. 기초 클래스인 player는 파생 클래스의 추가된 rating 멤버나 ResetRating 등을 갖고 있지 않지만, 파생 클래스인 player는 추가된 rating 멤버 등을 버리면 기본 클래스와 다를 게 없다.
기초 클래스 참조와 포인터가 파생 클래스 객체를 참조할 수 있다는 점을 활용하여 유용하게 사용할 수 있다.
- 기초 클래스 참조와 포인터를 매개변수로 사용하여, 기초 클래스와 파생 클래스 객체 모두 사용할 수 있다.
- 기초 클래스 객체를 파생 클래스 객체로 초기화 할 수 있다.
기초 클래스의 참조와 포인터를 매개변수로 하면 템플릿처럼 활용할 수 있다.
void Show(const TableTennisPlayer& rt) {}
void Wohs(const TableTennisPlayer* pt) {}
TableTennisPlayer tPlayer("Tara", "Boomdea", false);
RatedPlayer rPlayer(1140, "Mallory", "Duck", true);
Show(tPlayer); //가능
Show(rPlayer); //가능
Wohs(tPlayer); //가능
Wohs(rPlayer); //가능
참조 호환성은 기초 클래스 객체를 파생 클래스 객체로 초기화하는 것도 간접적으로 허용한다.
RatedPlayer olaf1(1840, "Olaf", "Loaf" true);
TableTennisPlayer olaf2(olaf1); //가능
olaf2를 초기화하는 과정에서 컴파일러가 기본 복사 생성자를 사용한다.
'서적 정리 > C++ 기초 플러스' 카테고리의 다른 글
81.클래스 설계 복습 (0) | 2022.07.19 |
---|---|
80.상속과 동적 메모리 대입 (0) | 2022.07.18 |
79.추상화 기초 클래스 (0) | 2022.07.14 |
78.접근제어: protected (0) | 2022.07.13 |
77.정적 결합과 동적 결합 (0) | 2022.07.13 |
76.public 다형 상속 (0) | 2022.07.12 |
75.상속: is-a 관계 (0) | 2022.07.12 |
차례 (0) | 2022.07.12 |
댓글