본문 바로가기
서적 정리/C++ 기초 플러스

74.간단한 기초 클래스부터 시작하자

by 민돌이2 2022. 7. 12.

tabtenn0.zip
0.00MB

객체 지향 프로그래밍(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를 초기화하는 과정에서 컴파일러가 기본 복사 생성자를 사용한다.

728x90

'서적 정리 > 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

댓글