C++ Primer 13
학습목표
- is - a 관계로 상속하기
- 다른 클래스로부터
public
으로 파생된 클래스 protected
접근- 생성자 멤버 초기자 리스트
- 업캐스팅과 다운캐스팅
- 가상 멤버 함수
- 초기 (정적) 결합과 말기 (동적) 결합
- 추상화 기초 클래스
- 순수 가상 함수
public
상속은 언제 어떻게
C++은 클래스를 확장하고 수정하기 위해 단순하게 코드를 수정하는 것이 아닌, 클래스 상속(class inheritance)라는 강력한 수단을 제공함
클래스 상속은 기초 클래스(base class)로부터 모든 메소드와 멤버들을 상속받아 새로운 파생 클래스(derived class)를 만들 수 있게 함
- 기존의 클래스에 기능을 추가할 수 있음
- 클래스가 나타내고있는 데이터에 다른 것을 더 추가할 수 있음
- 클래스 메소드가 동작하는 방식을 변경할 수 있음
13.1 간단한 기초 클래스로부터 시작
// tabtenn0.h
#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
class TableTennisPlyaer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlyaer(const string & fn = "none",
const string & ln = "none",
bool ht = false);
void Name() const;
bool HasTable() const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
#endif
// 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;
}
생성자에 멤버 초기자 리스트 구문을 사용하여 string
복사 생성자를 사용해 firstname
을 fn
으로 초기화하는 한 단계를 생략함
// usett0.cpp
#include <iostream>
#include "tabtenn0.h"
int main()
{
using std::cout;
TableTennisPlayer player1("Chunk", "Blizzard", true);
TableTennisPlayer player2("Tara", "Boomdea", false);
player1.Name();
if (player1.HasTable())
cout << " : 탁구대가 있다.\n";
else
cout << " : 탁구대가 없다.\n";
player2.Name();
if (player2.HasTable())
cout << " : 탁구대가 있다.\n";
else
cout << " : 탁구대가 없다.\n";
return 0;
}
표준 string
클래스는 const char*
형의 매개변수를 지닌 생성자를 가지고있음
따라서 string
객체는 const string &
형의 매개변수를 지닌 string
생성자를, C스타일 string
은 const char*
형의 매개변수를 지닌 string
생성자를 불러옴
즉, TableTennisPlyaer
생성자에 대한 매개변수로 string
객체 혹은 C스타일 string
둘 중 어느 것을 사용해도 무방함
클래스 파생시키기
class RatedPlayer : public TableTennisPlayer { ... };
형태로 클래스 파생
:
으로 RatedPlayer
클래스가 TableTennisPlayer
에 기초를 둔 파생 클래스란 것을 나타내고, 이를 public 파생
(public derivation)이라고 함
- 기초 클래스의
public
멤버들이 파생 클래스의public
멤버가 됨 - 기초 클래스의
private
부분도 파생 클래스의 일부가 되지만 기초 클래스의protected
또는public
메소드를 통해서만 접근할 수 있음 - 파생 클래스형의 객체 안에는 기초 클래스형의 데이터 멤버들이 저장됨
- 파생 클래스형의 객체는 기초 클래스형의 메소드들을 사용할 수 있음
- 즉, 파생 클래스는 기초 클래스의 구현과 인터페이스를 상속받음
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating;
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
멤버에 직접 접근할 수 없기 때문에 기초 클래스의 메소드를 통해서 접근해야함
따라서 파생 클래스의 생성자는 기초 클래스의 생성자를 사용해야함
RatedPlayer::RatedPlayer(unsigned int r, const char * fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
파생 클래스의 객체 생성시 먼저 기초 클래스 객체가 생성됨
이를 위해 C++에서는 멤버 초기자 리스트 문법이 사용됨
: TableTennisPlayer(fn, ln, ht)
가 멤버 초기자 리스트이며, 이는TableTennisPlayer
생성자를 호출함- 멤버 초기자 리스트를 생략할시 기초 클래스의 디폴트 생성자가 사용됨
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: TableTennisPlayer(tp), rating(r)
{
}
TableTennisPlayer(tp)
에서 tp
는 TableTennisPlayer &
형이기 때문에 기초 클래스의 복사 생성자를 호출함
이 때 복사 생성자가 정의되어있지 않은 경우 컴파일러는 복사 생성자를 자동으로 생성함
파생 클래스의 멤버들에도 멤버 초기자 리스트 문법을 사용할 수 있음
객체 파괴시 생성과 반대 순서로 파생 클래스 파괴자가 먼저 실행되고, 이후에 기초 클래스 파괴자가 자동으로 호출됨
파생 클래스 사용하기
// tabtenn1.h
#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string & fn = "none",
const string & ln = "none",
bool ht = false);
void Name() const;
bool HasTable() const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating;
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; };
};
#endif
// tabtenn1.cpp
#include "tabtenn1.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;
}
RatedPlayer::RatedPlayer(unsigned int r, const char * fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: TableTennisPlayer(tp), rating(r)
{
}
두 클래스가 서로 연계되어있기 때문에 클래스 선언 및 정의를 한 파일에 묶어두는 것이 더 체계적임
// usett1.cpp
#include <iostream>
#include "tabtenn1.h"
int main()
{
using std::cout;
using std::endl;
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
rplayer1.Name();
if (rplayer1.HasTable())
cout << " : 탁구대가 있다.\n";
else
cout << " : 탁구대가 없다.\n";
player1.Name();
if (player1.HasTable())
cout << " : 탁구대가 있다.\n";
else
cout << " : 탁구대가 없다.\n";
cout << "이름 : ";
rplayer1.Name();
cout << "랭킹 : " << rplayer1.Rating() << endl;
RatedPlayer rplayer2(1212, player1);
cout << "이름 : ";
rplayer2.Name();
cout << "랭킹 : " << rplayer2.Rating() << endl;
return 0;
}
파생 클래스와 기초 클래스의 특별한 관계
기초 클래스는 명시적 데이터형 변환 없이도 파생 클래스 객체를 지시할 수 있음
기초 클래스 참조는 명시적 데이터형 변환 없이도 파생 클래스 객체를 참조할 수 있음
단, 기초 클래스 포인터나 참조는 기초 클래스 메소드만 호출할 수 있음
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer1;
TableTennisPlayer * pt = &rplayer1;
rt.Name();
pt->Name();
TableTennisPlayer player("Betsy", "Bloop", true);
RatedPlayer & rr = player; // 허용되지 않음
RatedPlayer * pr = player; // 허용되지 않음
기초 클래스의 참조형과 포인터형에는 파생 클래스의 객체나 주소를 대입할 수 있으나, 반대는 허용되지 않음
기초 클래스에는 파생 클래스에 있는 멤버 등이 없을 수 있기 때문
따라서 기초 클래스 참조나 포인터를 매개변수로 사용하는 함수는 기초 클래스 객체 및 파생 클래스 객체 모두에 사용할 수 있음
RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer olaf2(olaf 1);
기초 클래스 객체를 파생 클래스 객체로 초기화시 암시적인 복사 생성자가 활용됨
위와 같은 경우 TableTennisPlayer(const TableTennisPlayer &);
형태의 복사 생성자가 사용되며, 이는 RatedPlayer
객체인 olaf1
내부의 TableTennisPLayer
객체로 olaf2
를 초기화함
RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer winner;
winner = olaf1;
파생 클래스 객체를 기초 클래스 객체에 대입시에는 암시적인 오버로딩 대입 연산자가 활용됨
위와 같은 경우 TableTennisPlayer & operator=(const TableTennisPlayer &) const;
형태의 오버로딩 대입 연산자가 사용되며, 기초 클래스의 참조가 파생 클래스 객체를 참조하여 olaf1
의 기초 클래스 부분이 winner
에 복사됨
13.2 상속 : is-a 관계
public
상속의 경우 is-a
관계로서 파생 클래스 객체가 기초 클래스 객체이기도 하다는 것을 뜻함
이 경우엔 기초 클래스 객체를 대상으로 수행할 수 있는 모든 작업을 파생 클래스 객체에 대해서도 수행할 수 있음
public
상속은 has-a
관계 및 is-like-a
관계나 is-implemented-as-a
관계, uses-a
관계를 나타내지 않음
has-a
: 점심이 과일을 가질 수는 있어도 일반적으로 점심은 과일이 아니기 때문에Fruit
클래스로부터Lunch
클래스를 파생시키는 것은 옳지 않음is-like-a
: 변호사는 상어와 같다고 말할 수 있더라도, 변호사는 상어가 아니기 때문에Shark
클래스로부터Lawyer
클래스를 파생시킬 수 없음is-implemented-as-a
: 스택을 배열을 사용하여 구현할 수 있더라도, 스택은 배열이 아니며 다른 방법으로도 구현될 수 있기 때문에Array
클래스로부터Stack
클래스를 파생시킬 수 없음uses-a
: 컴퓨터가 레이저프린터를 사용할 수 있더라도Computer
클래스로부터Printer
를 파생시키는 것 혹은 그 반대는 이치에 맞지 않음
13.3 public 다형 상속
파생 클래스에 대한 행동이 기초 클래스에 대한 행동과 다른 메소드가 요구되는 상황이 발생할 수 있음
즉, 호출하는 객체에 따라 메소드의 행동이 달라질 수 있으며 이를 다형(polymorhphic)이라고 부름
public
다형 상속을 구현하기 위한 방법
- 기초 클래스 메소드를 파생 클래스에서 다시 정의
- 가상 메소드를 사용
Brass Account
당좌를 위한 정보 : 고객 이름, 계좌번호, 현재 잔액
해당 계좌에 대해 허용할 연산 : 계좌 개설, 입금, 인출, 계좌 정보 출력
Brass Plus Account
에 추가하고픈 정보 : 당좌 대월의 한도, 당좌 대월의 이자율, 상환할 원리금
Brass와 BrassPlus 클래스의 개발
// brass.h
#ifndef BRASS_H_
#define BRASS_H_
#include <string>
class Brass
{
private:
std::string fullName;
long acctNum;
double balance;
public:
Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt);
double Balance() const;
virtual void ViewAcct() const;
virtual ~Brass() {};
};
class BrassPlus : public Brass
{
private:
double maxLoan;
double rate;
double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0,
double ml = 500, double r = 0.11125);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125);
virtual void ViewAcct() const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; };
void ResetRate(double r) { rate = r; };
void ResetOwes() { owesBank = 0; };
};
#endif
BrassPlus
클래스는 Brass
클래스에서 파생된 클래스임
- 3개의 새로운
private
멤버와public
멤버 함수가 추가됨 - 양 클래스 모두에
ViewAcct()
와Withdraw()
메소드가 선언되었으며, 각 클래스의 객체에 대한 행동이 다른 메소드임 Brass
클래스에서virtual
키워드를 사용해 가상 메소드(virtual method)를 선언함Brass
클래스는 아무 일도 하지않는 가상 파괴자를 선언함
Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct();
b2_ref.ViewAcct();
가상 메소드는 메소드가 객체에 의해 호출되지 않고 참조 또는 포인터에 의해 호출되었을 때 어느 메소드를 사용할 것인지를 객체형에 기초하여 결정함
ViewAcct()
가 가상 메소드가 아닌 경우 : 참조 변수들이Brass
형이기 때문에Brass:ViewAcct()
가 선택됨ViewAcct()
가 가상 메소드인 경우 :b1_ref
는Brass
형 객체를 참조하기 때문에Brass::ViewAcct()
를,b2_ref
는BrassPlus
형 객체를 참조하기 때문에BrassPlus::ViewAcct()
를 선택함- 파생 클래스에서 다시 정의되는 메소드들은 기초 클래스에서 가상으로 선언하는 것이 일반적인 관행이며, 이 때 파생 클래스에서는 자동으로 가상 메소드가 되지만
virtual
키워드를 사용하여 명시하는 것이 권장됨
// brass.cpp
#include "brass.h"
#include <iostream>
using std::cout;
using std::endl;
using std::string;
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);
Brass::Brass(const string & s, long an, double bal)
{
fullName = s;
acctNum = an;
balance = bal;
}
void Brass::Deposit(double amt)
{
if (amt < 0)
cout << "마이너스 입금은 허용되지 않습니다.\n"
<< "그래서 입금이 취소되었습니다.\n";
else
balance += amt;
}
void Brass::Withdraw(double amt)
{
format initialState = setFormat();
precis prec = cout.precision(2);
if (amt < 0)
cout << "마이너스 인출은 허용되지 않습니다.\n"
<< "그래서 인출이 취소되었습니다.\n";
else if (amt <= balance)
balance -= amt;
else
cout << "인출을 요구한 금액 $" << amt
<< "가 현재 잔액을 초과합니다.\n"
>> "그래서 인출이 취소되었습니다.\n";
restore(initialState, prec);
}
double Brass::Balance() const
{
return balance;
}
void Brass::ViewAcct() const
{
format initialState = setFormat();
precis prec = cout.precision(2);
cout << "고객 이름 : " << fullName << endl;
cout << "계좌 번호 : " << acctNum << endl;
cout << "현재 잔액 : $" << balance << endl;
restore(initialState, prec);
}
BrassPlus::BrassPlus(const string & s, long an, double bal,
double ml, double r) : Brass(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
BrassPlus::BrassPlus(const Brass & ba, double ml, double r)
: Brass(ba)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
void BrassPlus::ViewAcct() const
{
format initialState = setFormat();
precis prec = cout.precision(2);
Brass::ViewAcct();
cout << "당좌 대월 한도액 : $" << maxLoan << endl;
cout << "상환할 원리금 : $" << owesBank << endl;
cout.precision(3);
cout << "당좌 대월 이자율 : " << 100 * rate << "%\n";
restore(initialState, prec);
}
void BrassPlus::Withdraw(double amt)
{
format initialState = setFormat();
precis prec = cout.precision(2);
double bal = Balance();
if (amt <= bal)
Brass::Withdraw(amt);
else if (amt <= bal + maxLoan - owesBank)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);
cout << "당좌 대월 금액 : $" << advance << endl;
cout << "당좌 대월 이자 : $" << advance * rate << endl;
Deposit(advance);
Brass::Withdraw(amt);
}
else
cout << "당좌 대월 한도가 초과되어 거래가 취소되었습니다.\n";
restore(initialState, prec);
}
format setFormat()
{
return cout.setf(std::ios_base::fixed,
std::ios_base::floatfield);
}
void restore(format f, precis p)
{
cout.setf(f, std::ios_base::floatfield);
cout.precision(p);
}
BrassPlus
의 생성자들은 멤버 초기자 리스트 문법을 이용하여 기초 클래스의 private
데이터를 초기화함
ViewAcct()
함수를 BrassPlus
버전에 재정의함으로써 서로 다른 동작을 하도록 만듬
BrassPlus::ViewAcct()
에서Brass::ViewAcct()
메소드를 호출할 때 사용 범위 연산자를 사용하는 것이 중요함
Withdraw()
함수 역시 양 클래스에서 다르게 동작함
BrassPlus::Withdraw()
에서 쓰이는Balance()
함수는 파생 클래스에서 재정의되지 않기 때문에 사용 범위 결정 연산자가 불필요함
// usebrass1.cpp
#include <iostream>
#include "brass.h"
int main()
{
using std::cout;
using std::endl;
Brass Piggy("Porcelot Pigg", 381299, 4000.00);
BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);
Piggy.ViewAcct();
cout << endl;
Hoggy.ViewAcct();
cout << endl;
cout << "Hogg씨의 계좌에 $1000 입금 : \n";
Hoggy.Deposit(1000.00);
cout << "Hogg씨의 현재 잔액 : $" << Hoggy.Balance() << endl;
cout << "Pigg씨의 계좌에서 $4200 인출 :\n";
Piggy.Withdraw(4200.00);
cout << "Pigg씨의 현재 잔액 : $" << Piggy.Balance() << endl;
cout << "Hogg씨의 계좌에서 $4200 인출 :\n";
Hoggy.Withdraw(4200.00);
Hoggy.ViewAcct();
return 0;
}
// usebrass2.cpp
#include <iostream>
#include <string>
#include "brass.h"
const int CLIENTS = 4;
int main()
{
using std::cin;
using std::cout;
using std::endl;
Brass * p_clients[CLIENTS];
std::string temp;
long tempnum;
double tempbal;
char kind;
for (int i = 0; i < CLIENTS; i++)
{
cout << "고객의 이름을 입력하십시오 : ";
getline(cin, temp);
cout << "고객의 계좌 번호를 입력하십시오 : ";
cin >> tempnum;
cout << "계좌 개설 잔액을 입력하십시오 : $";
cin >> tempbal;
cout << "Brass 계좌는 1, "
<< "BrassPlus 계좌는 2를 입력하십시오 : ";
while (cin >> kind && (kind != '1' && kind != '2'))
cout << "1 아니면 2, 둘 중 하나를 입력하십시오 : ";
if (kind == '1')
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
{
double tmax, trate;
cout << "당좌 대월 한도를 입력하십시오 : $";
cin >> tmax;
cout << "당좌 대월 이자율을 소수점 형식으로 "
<< "입력하십시오 : ";
cin >> trate;
p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
}
while (cin.get() != '\n')
continue;
}
cout << endl;
for (int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct();
cout << endl;
}
for (int i = 0; i < CLIENTS; i++)
{
delete p_clients[i];
}
cout << "프로그램을 종료합니다.\n";
return 0;
}
Brass::ViewAcct()
함수가 가상으로 선언되었기 때문에 p_clients
포인터가 지시하는 객체의 종류에 따라 서로 다른 종류의 ViewAcct()
함수가 호출됨
또한 객체형에 해당하는 파괴자를 호출하기 위해 가상 파괴자가 사용됨
13.4 정적 결합과 동적 결합
함수 이름의 결합(binding) : 함수를 호출할 때 특정 블록에 있는 함수 코드를 실행하라는 뜻으로 해석하는 것
- C에서는 각각의 함수가 별개이지만, C++에서는 함수 오버로딩때문에 조금 복잡함
- 함수 이름 및 함수 매개변수의 검사는 컴파일 동안에 수행할 수 있으며 이를 정적 결합(static binding) 또는 초기 결합(early binding)이라고 함
- 가상 함수를 사용할시 컴파일시가 아닌 프로그램 실행시에 결합이 발생하며 이를 동적 결합(dynamic binding) 또는 말기 결합(lately binding)이라고 함
포인터와 참조형의 호환
일반적으로 C++은 포인터형이나 참조형의 불일치시 대입이 허용되지 않음
반면 기초 클래스를 지시하는 포인터나 참조는 파생 클래스 객체를 참조할 수 있음
- 업캐스팅(upcasting) : 파생 클래스 참조 및 포인터를 기초 클래스의 참조 및 포인터로 변환하는 것
public
상속시 명시적인 데이터형 변환 없이도 업캐스팅이 항상 허용됨
- 다운캐스팅(downcasting) : 기초 클래스 참조 및 포인터를 파생 클래스 참조 및 포인터로 변환하는 것
- 다운캐스팅은 명시적인 데이터형 변환 없이는 허용되지 않음
가상 멤버 함수와 동적 결합
BrassPlus ophelia;
Brass * bp;
bp = &ophelia;
bp->ViewAcct();
ViewAcct()
가 가상으로 선언되지 않은 경우 bp->ViewAcct();
는 bp
의 자료형인 Brass *
에 맞추어 Brass::ViewAcct()
를 호출함
이 때는 컴파일시에 함수가 결합되는 정적 결합이 사용됨
ViewAcct()
가 가상으로 선언된 경우 bp
의 객체형이 BrassPlus
이므로 BrassPlus::ViewAcct()
가 호출됨
일반적으로는 프로그램이 실행될 때 객체형이 결정되므로 동적 결합이 사용됨
상속을 위한 기초 클래스로 사용되지 않는 클래스를 설계하거나, 메소드를 재정의하지 않는 파생 클래스의 경우 동적 결합이 필요하지 않음
따라서 기초 클래스 포인터 및 참조가 지시하는 객체를 추적할 필요가 없어 더 효율적인 정적 결합을 사용함
파생 클래스에서 재정의되어서는 안되는 멤버 함수의 경우 가상으로 만들지 않음으로써 효율 및 의도를 드러낼 수 있음
컴파일러가 가상 함수를 다루는 일반적인 방법
- 각각의 객체에 숨겨진 멤버를 하나씩 추가함
- 이 때 숨겨진 멤버는 함수의 주소들로 이루어진 배열을 지시하는 포인터를 저장하고, 이 배열을 가상 함수 테이블(virtual function table)이라고 함
- 가상 함수 테이블에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되어있음
- 파생 클래스가 새로운 가상 함수를 정의하면 가상 함수 테이블에 해당 함수의 주소가 추가됨
가상 함수를 사용시 메모리와 실행 속도 면에서 부담이 발생함
- 각 객체의 크기가 주소 하나를 저장하는데 필요한 양만큼 커짐
- 각각의 클래스에 대해 컴파일러가 가상 함수들의 주소로 이루어진 하나의 배열을 만들어야함
- 각각의 함수 호출에 대해 실행할 함수의 주소를 얻기 위해 테이블에 접근하는 하나의 단계가 추가됨
가상 메소드에 대해 알아야 할 사항
파생 클래스 객체의 생성시 파생 클래스 생성자가 먼저 호출되고, 파생 클래스 생성자가 기초 클래스 생성자를 사용함
이는 상속 메커니즘과 다르기 때문에 생성자는 가상으로 선언할 수 없음
파생 클래스 객체의 사용 종료시 추가된 멤버에 대한 해제가 필요하므로 가상 파괴자를 사용해야함
일반적으로 파괴자가 필요 없는 기초 클래스라 하더라도 가상 파괴자를 제공해야함
프렌드는 멤버 함수가 아니기 때문에 가상 함수가 될 수 없음
단, 프렌드 함수가 내부적으로 가상 멤버 함수를 사용하게하여 문제를 해결할 수 있음
파생 클래스에서 함수가 재정의되지 않은 경우 해당 파생 클래스는 그 함수의 기초 클래스 버전을 사용함
파생 클래스가 길게 이어진 파생 사슬의 일부라면 해당 파생 클래스는 함수의 가장 최근 정의된 버전을 사용함
단, 기초 클래스 버전이 은닉되어있는 경우는 예외임
class Dwelling
{
public:
virtual void showperks(int a) const;
...
};
class Hovel : public Dwelling
{
public:
virtual void showperks() const;
...
};
위 코드를 컴파일시 컴파일러 경고가 발생할 수 있음
showperks()
함수를 매개변수를 가지지 않는 버전으로 재정의했기 때문에 trump.showperks(5);
와 같은 사용은 불가능함
즉, 함수를 파생 클래스에서 재정의할시 동일한 이름을 가진 모든 기초 클래스 메소드들을 은닉함
- 따라서 상속된 메소드를 재정의할 시에는 오리지널의 원형과 정확히 일치시켜야함
- 단, 리턴형이 기초 클래스에 대한 참조 및 포인터인 경우에는 파생 클래스에 대한 참조 및 포인터로 대체될 수 있음
- 이를 리턴형의 공변(covariance)이라고 함
- 기초 클래스 선언이 오버로딩되어있을 경우, 파생 클래스에서 모든 기초 클래스 버전들을 재정의해야함
13.5 접근 제어 : protected
protected
키워드를 사용하여 접근 제어시 바깥 세계에서는 private
와 동일하게 접근이 차단됨
그러나 파생 클래스의 멤버라면 마치 public
멤버를 다루는 것처럼 기초 클래스의 protected
멤버에 직접 접근할 수 있음
단, 클래스에서 변수를 보호하기 위해 마련한 안전장치가 무시될 수 있으므로 설계상의 결점을 가지게 됨
따라서 protected
접근 제어보다는 private
접근 제어를 사용하는 것이 권장됨
파생 클래스가 기초 클래스 데이터에 접근하는 것을 의도한다면 protected
접근 제어 대신 기초 클래스 메소드를 사용할 수 있음
단, public
으로 사용할 수 없는 내부 함수에 파생 클래스가 접근할 수 있으므로 protected
접근 제어가 멤버 함수에 유용할 수도 있음
13.6 추상화 기초 클래스
클래스의 일부만이 공통적일 경우, 추상화 기초 클래스(ABC)를 정의한 후 클래스들을 ABC로부터 파생시킬 수 있음
ABC의 공통 메소드는 필요한 데이터를 멤버를 가지고있지 않은 경우 구현이 불가능함
따라서 이 경우엔 구현되지 않는 메소드를 위한 순수 가상 함수(pure virtual function)을 사용함
class BaseEllipse
{
private:
double x;
double y;
...
public:
BaseEllipse(double x0 = 0, double y0 = 0) : x(x0), y(y0) {};
virtual ~BaseEllipse() {};
void Move(int nx, int ny) { x = nx; y = ny; };
virtual double Area() const = 0;
...
}
위의 Area()
함수가 순수 가상 함수에 해당함
클래스 선언에 순수 가상 함수가 포함되어있을시 해당 클래스의 객체를 생성할 수 없음
어떤 클래스가 ABC가 되기 위해서는 최소 하나 이상의 순수 가상 함수를 보유해야함
ABC 개념의 적용
// acctabc.h
#ifndef ACCTABC_H_
#define ACCTABC_H_
#include <iostream>
#include <string>
class AcctABC
{
private:
std::string fullName;
long acctNum;
double balance;
protected:
struct Formatting
{
std::ios_base::fmtflags flag;
std::streamsize pr;
};
const std::string & FullName() const { return fullName; };
long AcctNum() const { return acctNum; };
Formatting SetFormat() const;
void Restore(Formatting & f) const;
public:
AcctABC(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt) = 0;
double Balance() const { return balance; };
virtual void ViewAcct() const = 0;
virtual ~AcctABC() {};
};
class Brass : public AcctABC
{
public:
Brass(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0) : AcctABC(s, an, bal) {};
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
virtual ~Brass() {};
};
class BrassPlus : public AcctABC
{
private:
double maxLoan;
double rate;
double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0,
double ml = 500, double r = 0.1);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
virtual void ViewAcct() const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; };
void ResetRate(double r) { rate = r; };
void ResetOwes() { owesBank = 0; };
};
#endif
// acctabc.cpp
#include "acctabc.h"
#include <iostream>
using std::cout;
using std::ios_base;
using std::endl;
using std::string;
AcctABC::AcctABC(const string & s, long an, double bal)
{
fullName = s;
acctNum = an;
balance = bal;
}
void AcctABC::Deposit(double amt)
{
if (amt < 0)
cout << "마이너스 입금은 허용되지 않습니다.\n"
<< "그래서 입금이 취소되었습니다.\n";
else
balance += amt;
}
void AcctABC::Withdraw(double amt)
{
balance -= amt;
}
AcctABC::Formatting AcctABC::SetFormat() const
{
Formatting f;
f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
f.pr = cout.precision(2);
return f;
}
void AcctABC::Restore(Formatting & f) const
{
cout.setf(f.flag, ios_base::floatfield);
cout.precision(f.pr);
}
void Brass::Withdraw(double amt)
{
if (amt < 0)
cout << "마이너스 인출은 허용되지 않습니다.\n"
<< "그래서 인출이 취소되었습니다.\n";
else if (amt <= Balance())
AcctABC::Withdraw(amt);
else
cout << "인출을 요구한 금액 $" << amt
<< "가 현재 잔액을 초과합니다.\n"
<< "그래서 인출이 취소되었습니다.\n";
}
void Brass::ViewAcct() const
{
Formatting f = SetFormat();
cout << "Brass 고객 이름 : " << FullName() << endl;
cout << "계좌 번호 : " << AcctNum() << endl;
cout << "현재 잔액 : $" << Balance() << endl;
Restore(f);
}
BrassPlus::BrassPlus(const string & s, long an, double bal,
double ml, double r) : AcctABC(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
BrassPlus::BrassPlus(const Brass & ba, double ml, double r)
: AcctABC(ba)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
void BrassPlus::ViewAcct() const
{
Formatting f = SetFormat();
cout << "BrassPlus 고객 이름 : " << FullName() << endl;
cout << "계좌 번호 : " << AcctNum() << endl;
cout << "현재 잔액 : $" << Balance() << endl;
cout << "당좌 대월 한도 ; $" << maxLoan << endl;
cout << "상환활 원리금 : $" << owesBank << endl;
cout.precision(3);
cout << "당좌 대월 이자율 : " << 100 * rate << "%\n";
Restore(f);
}
void BrassPlus::Withdraw(double amt)
{
Formatting f = SetFormat();
double bal = Balance();
if (amt <= bal)
AcctABC::Withdraw(amt);
else if (amt <= bal + maxLoan - owesBank)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);
cout << "당좌 대월 금액 : $" << advance << endl;
cout << "당좌 대월 이자 : $" << advance * rate << endl;
Deposit(advance);
AcctABC::Withdraw(amt);
}
else
cout << "당좌 대월 한도가 초과되어 거래가 취소되었습니다.\n";
Restore(f);
}
// usebrass3.cpp
#include <iostream>
#include <string>
#include "acctabc.h"
const int CLIENTS = 4;
int main()
{
using std::cin;
using std::cout;
using std::endl;
AcctABC * p_clients[CLIENTS];
std::string temp;
long tempnum;
double tempbal;
char kind;
for (int i = 0; i < CLIENTS; i++)
{
cout << "고객의 이름을 입력하십시오 : ";
getline(cin, temp);
cout << "고객의 계좌 번호를 입력하십시오 : ";
cin >> tempnum;
cout << "계좌 개설 잔액을 입력하십시오 : $";
cin >> tempbal;
cout << "Brass 계좌는 1, "
<< "BrassPlus 계좌는 2를 입력하십시오 : ";
while (cin >> kind && (kind != '1' && kind != '2'))
cout << "1 아니면 2, 둘 중 하나를 입력하십시오 : ";
if (kind == '1')
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
{
double tmax, trate;
cout << "당좌 대월 한도를 입력하십시오 : $";
cin >> tmax;
cout << "당좌 대월 이자율을 소수점 형식으로 "
<< "입력하십시오 : ";
cin >> trate;
p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
}
while (cin.get() != '\n')
continue;
}
cout << endl;
for (int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct();
cout << endl;
}
for (int i = 0; i < CLIENTS; i++)
{
delete p_clients[i];
}
cout << "프로그램을 종료합니다.\n";
return 0;
}
ABC 철학
추상화 기초 클래스를 사용하면 훨씬 더 체계적으로 상속에 접근할 수 있음
추상화 클래스를 설계하기 전
- 프로그래밍 문제를 표현해야하는 데 필요한 클래스들에는 어떤 것들이 있을지 고민
- 해당 클래스들은 서로 어떤 연관이 있을까를 고민
구체화 클래스들이 기초 클래스 역할을 하지 못하게 만들시 설계가 단순해지고 더 깨끗한 코드가 만들어짐
추상화 기초 클래스의 사용은 컴포넌트 설계자가 인터페이스 약정을 만드는 것을 허용함
이를 통해 ABC로부터 파생되는 모든 컴포넌트들이 최소한 ABC가 규정하는 공통 기능을 지원한다는 것을 보장
13.7 상속과 동적 메모리 대입
경우 1 - 파생 클래스가 new를 사용하지 않음
class baseDMA
{
private:
char * label;
int rating;
public:
baseDMA(const char * l = "null", int r = 0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA & rs);
...
};
class lacksDMA : public baseDMA
{
private:
char color[40];
public:
...
};
파생 클래스인 lacksDMA
가 new
를 사용하지 않는 경우
- 디폴트 파괴자에서 기초 클래스의 파괴자를 호출하는 것만으로 충분하기 때문에 명시적 파괴자의 정의가 필요하지 않음
lacksDMA
클래스의 디폴트 복사 생성자는baseDMA
성분을 복사하기 위한 명시적인 복사 생성자를 사용함- 추가로 동적 메모리 대입이 없기 때문에 새로운
lacksDMA
멤버에 대해서는 멤버별 복사를 수행하는 디폴트 복사만으로 충분하고, 따라서 복사 생성자의 정의가 필요하지 않음 - 디폴트 대입 연산자는 기초 클래스의 대입 연산자를 자동적으로 사용하기 때문에 대입 연산자의 정의가 필요하지 않음
경우 2 - 파생 클래스가 new를 사용함
class hasDMA : public baseDMA
{
private:
char * style;
public:
...
};
baseDMA::~baseDMA()
{
delete [] label;
}
hasDMA::~hasDMA()
{
delete [] style;
}
파생 클래스에서 새롭게 동적 대입한 메모리를 해제하기 위해 명시적 파괴자가 필요함
baseDMA::baseDMA(const baseDMA & rs)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}
파생 클래스의 복사 생성자는 파생 클래스의 데이터에만 접근할 수 있으므로 기초 클래스의 복사 생성자도
호출해야함
이 때 멤버 초기자 리스트로 기초 클래스의 생성자를 호출하는데, 기초 클래스 참조가 파생형을 참조할 수 있기 때문에 파생 클래스의 참조형을 매개변수로 전달해도 무방함
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if (this == &rs)
return *this;
delete [] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if (this == &hs)
return *this;
baseDMA::operator=(hs);
delete [] style;
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
파생 클래스의 명시적 대입 연산자는 상속받은 기초 클래스 객체의 대입을 처리하기 위해 기초 클래스 대입 연산자를 명시적으로 호출해야함
baseDMA::operator=(hs);
는 *this = hs;
와 같은 효과를 가져옴
동적 메모리 대입과 프렌드를 사용하는 상속예제
// dma.h
#ifndef DMA_H_
#define DMA_H_
#include <iostream>
class baseDMA
{
private:
char * label;
int rating;
public:
baseDMA(const char * l = "null", int r = 0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const baseDMA & rs);
};
class lacksDMA : public baseDMA
{
private:
enum { COL_LEN = 40 };
char color[COL_LEN];
public:
lacksDMA(const char * c = "blank", const char * l = "null", int r = 0);
lacksDMA(const char * c, const baseDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const lacksDMA & rs);
};
class hasDMA : public baseDMA
{
private:
char * style;
public:
hasDMA(const char * s = "none", const char * l = "null", int r = 0);
hasDMA(const char * s, const baseDMA & rs);
hasDMA(const hasDMA & hs);
~hasDMA();
hasDMA & operator=(const hasDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const hasDMA & rs);
};
#endif
// dma.cpp
#include "DMA.h"
#include <cstring>
baseDMA::baseDMA(const char * l, int r)
{
label = new char[std::strlen(l) + 1];
std::strcpy(label, l);
rating = r;
}
baseDMA::baseDMA(const baseDMA & rs)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}
baseDMA::~baseDMA()
{
delete [] label;
}
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if (this == &rs)
return *this;
delete [] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}
std::ostream & operator<<(std::ostream & os, const baseDMA & rs)
{
os << "상표 : " << rs.label << std::endl;
os << "등급 : " << rs.rating << std::endl;
return os;
}
lacksDMA::lacksDMA(const char * c, const char * l, int r) : baseDMA(l, r)
{
std::strncpy(color, c, 39);
color[39] = '\0';
}
lacksDMA::lacksDMA(const char * c, const baseDMA & rs) : baseDMA(rs)
{
std::strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
}
std::ostream & operator<<(std::ostream & os, const lacksDMA & ls)
{
os << (const baseDMA &) ls;
os << "색상 : " << ls.color << std::endl;
return os;
}
hasDMA::hasDMA(const char * s, const char * l, int r) : baseDMA(l, r)
{
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}
hasDMA::hasDMA(const char * s, const baseDMA & rs) : baseDMA(rs)
{
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}
hasDMA::~hasDMA()
{
delete [] style;
}
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if (this == &hs)
return *this;
baseDMA::operator=(hs);
delete [] style;
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
os << (const baseDMA &) hs;
os << "스타일 : " << hs.style << std::endl;
return os;
}
std::ostream & operator<<(std::ostream & os, const hasDMA & hs);
의 경우 hasDMA
클래스의 프렌드이지만 baseDMA
클래스에 대해서는 프렌드가 아님
따라서 baseDMA
클래스에 대해 프렌드인 operator<<()
함수를 사용하여 label
과 rating
멤버에 접근해야함
단, 프렌드는 멤버 함수가 아니라 사용 범위 결정 연산자를 사용할 수 없기 때문에 (const baseDMA &) hs;
와 같이 강제 데이터형 변환을 사용하여 원형을 일치시킴
// usedma.cpp
#include <iostream>
#include "dma.h"
int main()
{
using std::cout;
using std::endl;
baseDMA shirt("Portabelly", 8);
lacksDMA balloon("red", "Blimpo", 4);
hasDMA map("Mercator", "Buffalo Keys", 5);
cout << "baseDMA 객체를 출력한다 : \n";
cout << shirt << endl;
cout << "lacksDMA 객체를 출력한다 : \n";
cout << balloon << endl;
cout << "hasDMA 객체를 출력한다 : \n";
cout << map << endl;
lacksDMA balloon2(balloon);
cout << "lacksDMA 복사 결과 : \n";
cout << balloon2 << endl;
hasDMA map2;
map2 = map;
cout << "hasDMA 대입결과 : \n";
cout << map2 << endl;
return 0;
}
13.8 클래스 설계 복습
컴파일러가 자동으로 생성하는 멤버 함수
디폴트 생성자 : 매개변수를 사용하지 않거나, 모든 매개변수가 디폴트 매개변수를 사용하는 생성자
- 생성자가 정의되지 않았을시 컴파일러가 자동 디폴트 생성자를 정의함 = 사용자가 어떤 종류든 생성자를 정의했을시 디폴트 생성자를 사용자가 직접 정의해주어야함
- 명시적 초기화가 없는 객체 생성, 객체 배열 생성시에 사용
- 자동 디폴트 생성자는 기초 클래스 및 다른 클래스의 객체인 모든 멤버에 대한 디폴트 생성자를 호출함
- 파생 클래스 생성자 작성시 멤버 초기자 리스트를 사용하지 않은 경우(= 기초 클래스 생성자를 명시적 호출하지 않은 경우)에 컴파일러는 기초 클래스 디폴트 생성자를 사용
- 사용자가 직접 명시적 디폴트 생성자를 제공하는 것이 권장됨
복사 생성자 : 자기 자신의 클래스형 객체를 const
참조 매개변수로 사용하는 생성자, 즉 Name(const Name &);
형태의 원형을 가짐
- 복사 생성자가 사용되는 상황
- 새 객체를 동일한 클래스의 다른 객체로 초기화
- 객체를 함수에 값으로 전달
- 함수가 객체를 값으로 리턴
- 컴파일러가 임시 객체를 생성
- 복사 생성자가 사용되지 않으면 컴파일러는 복사 생성자의 원형만을 제공
- 복사 생성자가 사용된다면 프로그램은 멤버별 초기화를 수행하는 복사 생성자를 정의함
- 새 객체의 멤버들을 오리지널 객체에 해당하는 멤버의 값으로 각각 초기화
- 멤버가 클래스 객체일 경우 해당 클래스에 정의된 복사 생성자를 사용
- 깊은 복사가 요구되는
new
를 사용한 멤버 포인터나 변경이 이루어져야하는static
변수의 경우 사용자가 직접 복사 생성자를 정의해야함
대입 연산자 : 어떤 객체를 동일한 클래스의 다른 객체에 대입하는 것을 처리, Name & Name::operator=(const Name &);
형태의 원형을 가짐
- 새 객체 생성시에는 초기화, 기존 객체의 값을 변경시에는 대입
- 디폴트 대입은 멤버간 대입을 사용하며, 멤버가 클래스 객체일 경우 해당 클래스의 대입 연산자를 사용
- 복사 생성자를 명시적으로 정의해야할 이유를 가지고있다면 대입 연산자도 명시적으로 정의해야함
- 하나의 데이터형을 다른 데이터형에 대입할 경우
- 해당 연산자를 명시적으로 정의
- 변환 함수를 사용하여 데이터형을 클래스 객체로 변환한 후 대입 연산자 함수를 사용
클래스 메소드에 관련된 그 밖의 고려 사항
생성자가 아닌 메소드들은 기존 객체에 의해 호출됨
반면 생성자가 일을 마칠때까지 객체는 존재하지 않기 때문에 생성자는 상속되지 않음
new
에 의해 대입되는 메모리를 해제하기 위해서는 명시적 파괴자를 반드시 정의해야함
어떤 클래스를 기초 클래스로 사용하고 싶다면 해당 클래스가 명시적 파괴자를 요구하지 않더라도 가상 파괴자를 제공해야함
하나의 매개변수를 사용하여 호출 가능한 생성자는 해당 매개변수의 데이터형을 클래스형으로 변환하는 것을 정의함
Star(const char *);
Star north;
north = "polaris";
(char *)
를 Star
에 대입하는 대입 연산자가 정의되지 않았을시
north = "polaris";
구문은 우선Star::Star(const char *)
를 사용하여Star
객체를 생성함- 이후
Star
객체를 매개변수로 사용하여Star::operator=(const Star &)
함수를 호출함 - 이 때
Star(const char *)
앞에explicit
키워드를 붙일 경우 암시적 변환은 사용이 불가능함- 즉,
north = "polaris";
는 허용되지 않음 - 단
north = Star("polaris");
와 같이 명시적 변환은 허용됨
- 즉,
클래스 객체를 다른 데이터형으로 변환시엔 변환 함수를 정의해야함
- 변환 함수는 매개변수를 사용하지 않고 변환 결과 데이터형의 이름을 리턴형으로 선언하지 않는 클래스 멤버 함수임
Star::Star double() const {...}
와 같은 형태로 사용하여Star
클래스 객체를double
형으로 변환함- 변환 함수 사용시 모호한 코드가 작성될 가능성이 높아지므로 주의해야함
객체를 매개변수로 사용하는 함수 작성시 일반적으로 값이 아닌 참조로 전달을 사용해야함
- 객체를 값으로 전달시 복사 생성자 및 파괴자를 호출하는 임시 복사본이 생성되어 효율성이 낮아짐
- 따라서 객체를 변경하지 않는다면 함수의 매개변수를
const
참조로 선언 - 가상 함수를 사용하는 상속의 경우 기초 클래스 참조 매개변수를 사용하도록 정의된 함수는 파생 클래스에 대해서도 성공적으로 사용할 수 있음
반드시 객체를 리턴해야할 이유가 있는 것이 아니라면 참조를 리턴하는 것이 권장됨
- 객체 리턴시 리턴된 객체의 임시 복사본을 생성해 효율성이 낮아짐
- 단, 함수 안에서 생성된 임시 객체에 대한 참조를 리턴하지 않도록 유의해야함
메소드가 매개변수 또는 그것을 호출한 객체를 변경하지 않는다는 확신이 있을시 const
를 사용할 수 있음
함수의 매개변수를 const
에 대한 참조 또는 const
를 지시하는 포인터로 선언시 매개변수를 변경하지 않는다는 보장이 있는 경우에만 해당 매개변수를 다른 함수에 전달할 수 있음
public 상속에 관련된 그 밖의 고려 사항
프로그램에 상속 기능 추가시 is-a
관계로부터 안내를 받아야 함
is-a
관계인 경우 명시적 데이터형 변환 없이 기초 클래스 포인터가 파생 클래스 객체를 지시할 수 있고, 기초 클래스 참조가 파생 클래스 객체를 참조할 수 있으며 역은 성립하지 않음
즉, 예상하는 파생 클래스가 기초 클래스의 특별한 종류가 아닐시 public
파생을 사용하면 안됨
생성자는 상속되지 않으므로 파생 클래스는 일반적으로 멤버 초기자 리스트 문법을 사용하여 기초 클래스 생성자를 호출함
그렇지 않다면 파생 클래스는 기초 클래스의 디폴트 생성자를 사용함
파괴자는 상속되지 않으며, 객체 파괴시 파생 파괴자가 호출된 후 기초 파괴자가 호출됨
디폴트 기초 클래스 파괴자가 있다면 파생 클래스는 디폴트 파생 클래스 파괴자를 생성함
일반적으로 어떤 클래스가 기초 클래스로 쓰인다면 해당 클래스의 파괴자는 가상이어야함
한 객체를 동일한 클래스의 다른 객체로 대입시 컴파일러는 암시적 디폴트 대입 연산자를 통한 멤버별 대입을 사용함
객체가 파생 클래스에 속한다면 해당 객체의 기초 클래스 부분은 기초 클래스 대입 연산자를 사용하여 처리함
어떤 클래스가 다른 클래스의 객체를 멤버로 가진다면 대입시 해당 멤버 클래스의 대입 연산자가 사용됨
new
를 사용한 포인터와 같이 특별한 주의를 요하는 데이터 멤버를 사용한 경우 대입 연산자를 따로 정의해야함
이 때 해당 대입 연산자는 클래스의 모든 멤버들을 제공해야함
파생 클래스 객체를 기초 클래스 객체에 대입시 기초 클래스 멤버들만 대입이 이루어짐
그 반대의 경우 변환 생성자가 존재시 또는 따로 대입 연산자를 정의해야 이루어질 수 있음
파생 클래스는 기초 클래스의 protected
멤버에는 직접 접근할 수 있으나, private
멤버는 기초 클래스 멤버 함수를 통해서만 접근할 수 있음
따라서 protected
는 코드가 간단해지고 접근 속도가 빨라진다는 장점이, private
는 보안성이 강화된다는 장점이 있음
파생 클래스에서 메소드를 재정의할시엔 기초 클래스에서 해당 메소드를 가상으로 정의하여 동적 결합을 가능하게 해야함
그렇지 않다면 가상으로 정의하지 않음으로써 해당 메소드의 재정의를 원하지 않는다는 뜻으로 해석함
void show(const Brass & rba)
{
rba.ViewAcct();
cout << endl;
}
void inadequate(Brass ba)
{
ba.ViewAcct();
cout << endl;
}
BrassPlus buzz("Buzz Parsec", 00001111, 4300);
show(buzz);
inadequate(buzz);
show()
함수는 BrassPluss
객체에 대한 참조를 매개변수로 사용하기 때문에 rba.ViewAcct
는 BrassPlus
버전으로 해석됨
inadequate()
함수는 Brass
객체를 매개변수로 사용하므로 ba
는 Brass(const Brass &)
생성자를 통해 생성된 Brass
객체이며, ba.ViewAcct
는 Brass
버전으로 해석됨
ostream & operator <<(ostream & os, const hasDMA & hs)
{
os << (const baseDMA &) hs;
os << "스타일 : " << hs.style << endl;
return os;
}
파생 클래스의 프렌드 함수를 기초 클래스의 프렌드로 사용하고싶을 경우
- 파생 클래스 참조나 포인터를 기초 클래스에서 그에 대응하는 것으로 데이터형 변환 실시
- 변환된 데이터형의 참조나 포인터형을 사용하여 기초 클래스 프렌드를 호출
- 이 때 데이터형 변환에는
dynamic_cast<>
연산자를 사용할 수 있음
public
파생 클래스 객체가 기초 클래스를 사용하는 방법 정리
- 파생 클래스 객체는 메소드를 재정의하지 않은 경우 상속된 기초 클래스 메소드들을 자동으로 사용함
- 파생 클래스 파괴자는 기초 클래스 파괴자를 자동으로 호출함
- 파생 클래스 생성자는 멤버 초기자 리스트에서 다른 생성자를 지정하지 않는다면 기초 클래스 디폴트 생성자를 자동으로 호출함
- 파생 클래스 생성자는 멤버 초기자 리스트에 지정된 기초 클래스 생성자를 명시적으로 호출함
- 파생 클래스 메소드들은
public
및protected
기초 클래스 메소드들을 호출하기 위해 사용 범위 결정 연산자를 사용할 수 있음 - 파생 클래스의 프렌드는 파생 클래스 참조 또는 포인터를 기초 클래스 참조나 포인터로 데이터형 변환을 실시하고, 변환된 데이터를 사용하여 기초 클래스의 프렌드를 호출할 수 있음
연습문제
-
생성자, 파괴자, 대입 연산자, 프렌드를 제외한 멤버
-
생성자, 파괴자, 대입 연산자, 프렌드
-
baseDMA::operator=()
함수의 리턴형이baseDMA &
가 아닌void
로 정의된 경우 단일 대입은 사용 가능하나 연쇄 대입은 사용 불가능함
리턴형이baseDMA
로 정의된 경우 결과는 같으나 내부적으로 복사가 이루어지며 임시 객체가 생성되어 효율이 낮아짐 -
- 파생 클래스 생성시 : 파생 클래스 생성자 호출 > 기초 클래스 생성자 호출
- 파생 클래스 파괴시 : 기초 클래스 파괴자 호출 > 파생 클래스 파괴자 호출
-
모든 클래스는 자신의 생성자를 요구하기 때문에 필요함
단, 이 경우 생성자는 빈 몸체로 사용됨 -
파생 클래스의 메소드가 호출됨
-
파생 클래스 생성자에서
new
를 사용하여 포인터 멤버를 초기화할시에 파생 클래스는 대입 연산자를 정의해야함 -
전자와 후자 모두 가능하나 후자의 경우 데이터형 변환이 필요함
- 전자와 후자와 모두 가능
- 전자의 경우 파생 클래스에서 새로 추가된 데이터 멤버들은 전달되지 않음
- 후자의 경우 파생 클래스가 변환 연산자를 정의하고 있거나, 기초 클래스 매개변수를 사용하는 대입 연산자를 정의하고 있을 경우에만 가능
-
기초 클래스에 대한 참조는 모든 파생 클래스를 참조할 수 있기 때문
-
복사 과정에서 호출되는 기초 클래스 복사 생성자가 기초 클래스에 대한 참조를 매개변수로 사용함
기초 클래스에 대한 참조는 모든 파생 클래스를 참조할 수 있음 -
값으로 전달할 경우 복사 과정에서 임시 객체의 생성과 파괴가 이루어져 효율이 저하됨
원본 데이터에 대한 보호는 참조를const
형으로 전달하여 값에 의한 전달에서의 장점을 유지할 수 있음
참조로 전달시 가상 함수를 사용할 수 있음 -
a)
Corporation::head()
b)PublicCorporation::head()
-
class Kitchen { private: double kit_sq_ft; public: Kitchen() { kit_sq_ft = 0.0; } virtual double area() const { return kit_sq_ft * kit_sq_ft; } }; class House : public Kitchen { private: double all_sq_ft; public: House() { all_sq_ft += kit_sq_ft; } double area(const char * s) const { cout << s; return all_sq_ft; } };
상황이
is-a
모델에 맞지 않음
기초 클래스의priavte
멤버인kit_sq_ft
를 파생 클래스인House
에서 직접 접근할 수 없음
House
와Kitchen
에 있는area()
의 시그내처가 서로 다르기 때문에Kitchen
에 있는area()
를 가림