[열혈 C++] Chap08: 상속과 다형성
1. 객체 포인터의 참조관계
✏️객체 포인터 변수
- 객체의 주소값을 저장하는 포인터 변수
-
A ⬅️ B ⬅️ C
클래스의 상속관계가 이렇게 될 때,
A * p = new B();
,A * p = new C();
,B & p = new C();
이렇게 유도클래스 객체를 가리킬 수 있다.C++에서, AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.(주소 값을 저장할 수 있다)
- IS-A 관계가 성립하기 때문이다.
✏️함수 오버라이딩
- 오버라이딩 된 기초 클래스의 함수는 오버라이딩을 한 유도 클래스의 함수에 가려진다.
기초클래스::함수
형태로 오버라이딩된 기초 클래스의 함수도 호출할 수 있다.
#include <iostream>
#include <cstring>
using namespace std;
class Employee
{
private:
char name[100];
public:
Employee(char * name)
{
strcpy(this->name, name);
}
void ShowYourName() const
{
cout<<"name: "<<name<<endl;
}
};
class PermanentWorker : public Employee
{
private:
int salary;
public:
PermanentWorker(char * name, int money)
: Employee(name), salary(money)
{ }
int GetPay() const
{
return salary;
}
void ShowSalaryInfo() const
{
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
class TemporaryWorker : public Employee
{
private:
int workTime;
int payPerHour;
public:
TemporaryWorker(char * name, int pay)
: Employee(name), workTime(0), payPerHour(pay)
{ }
void AddWorkTime(int time)
{
workTime+=time;
}
int GetPay() const
{
return workTime*payPerHour;
}
void ShowSalaryInfo() const
{
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
class SalesWorker : public PermanentWorker
{
private:
int salesResult; // 월 판매실적
double bonusRatio; // 상여금 비율
public:
SalesWorker(char * name, int money, double ratio)
: PermanentWorker(name, money), salesResult(0), bonusRatio(ratio)
{ }
void AddSalesResult(int value)
{
salesResult+=value;
}
int GetPay() const
{
return PermanentWorker::GetPay() // PermanentWorker의 GetPay 함수 호출
+ (int)(salesResult*bonusRatio);
}
void ShowSalaryInfo() const //위 PermanentWorker의 함수와 동일하지만, this의 GetPay함수가
{ //호출되어야 하므로 따로 정의해주어야 한다
ShowYourName();
cout<<"salary: "<<GetPay()<<endl<<endl;
}
};
class EmployeeHandler
{
private:
Employee* empList[50];
int empNum;
public:
EmployeeHandler() : empNum(0)
{ }
void AddEmployee(Employee* emp)
{
empList[empNum++]=emp;
}
void ShowAllSalaryInfo() const
{
/*
for(int i=0; i<empNum; i++)
empList[i]->ShowSalaryInfo();
*/
}
void ShowTotalSalary() const
{
int sum=0;
/*
for(int i=0; i<empNum; i++)
sum+=empList[i]->GetPay();
*/
cout<<"salary sum: "<<sum<<endl;
}
~EmployeeHandler()
{
for(int i=0; i<empNum; i++)
delete empList[i];
}
};
int main(void)
{
// 직원관리를 목적으로 설계된 컨트롤 클래스의 객체생성
EmployeeHandler handler;
// 정규직 등록
handler.AddEmployee(new PermanentWorker("KIM", 1000));
handler.AddEmployee(new PermanentWorker("LEE", 1500));
// 임시직 등록
TemporaryWorker * alba=new TemporaryWorker("Jung", 700);
alba->AddWorkTime(5); // 5시간 일한결과 등록
handler.AddEmployee(alba);
// 영업직 등록
SalesWorker * seller=new SalesWorker("Hong", 1000, 0.1);
seller->AddSalesResult(7000); // 영업실적 7000
handler.AddEmployee(seller);
// 이번 달에 지불해야 할 급여의 정보
handler.ShowAllSalaryInfo();
// 이번 달에 지불해야 할 급여의 총합
handler.ShowTotalSalary();
return 0;
}
- 유도클래스의 객체를 포인터로 가리킬 수 있고, 함수를 오버라이딩 할 수 있음으로써 새로운 클래스의 객체가 추가된다고 해도 EmployeeHandler클래스는 수정할 필요가 없어진다.
따라서, 코드의 확장성이 매우 좋아진다.
2. 가상함수
✏️기초클래스를 포인터로 참조했을 경우
class Base{
public:
void BaseFnc();
};
class Derived : public Base{
public:
void DerivedFunc();
};
-
일때,
Base * bptr = new Derived();
에서 컴파일러는 문제없이 지나가지만,bptr->DerivedFunc()
에는 에러를 발생시킨다. 왜냐하면, C++ 컴파일러는 포인터 연산의 가능성 여부를 판단 할 때, 포인터의 자료형을 기준으로 판단하며, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다 -
즉 컴파일러는 bptr의 형의 Base임은 알지만, 실제로는 Derived의 객체라는 것을 기억하지 않는다. 따라서,
Derived * dptr = bptr
에서도 bptr이 가리키는 대상이 Base일 수도 있다고 여기고 에러를 발생시킨다. -
포인터로 참조하여 함수에 접근할 때는 포인터 형에 해당하는 클래스의 멤버변수에만 접근할 수 있다.
void AddEmployee(Employee* emp)
{
empList[empNum++]=emp;
}
void ShowAllSalaryInfo() const
{
/*
for(int i=0; i<empNum; i++)
empList[i]->ShowSalaryInfo();
*/
}
void ShowTotalSalary() const
{
int sum=0;
/*
for(int i=0; i<empNum; i++)
sum+=empList[i]->GetPay();
*/
cout<<"salary sum: "<<sum<<endl;
}
- 앞서 제시되었던 코드에서 주석처리된 부분이 에러가 나는 이유는, 기초클래스의 포인터 형 Employee 객체 emp가 ShowSalaryInfo함수에 접근할 때, 기초클래스와 유도클래스들의 어떤 멤버 함수에 접근해야하는지 모르기 때문이다.
✏️가상함수
-
virtual
선언 -
가상함수 선언하면 이 함수를 오버라이딩 하는 함수도 가상함수
-
가상함수로 선언되면, 해당 함수 호출 시, 포인터의 자료형을 기반으로 호출대상을 결정하지 않고, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출 대상 결정
-
앞서 제시한 에러가 발생하는 코드를 정상적으로 고치려면, 포인터의 자료형인 Employee 클래스에 함수를 가상 멤버함수로 추가시키면 된다.
✏️순수 가상함수와 추상클래스
-
클래스 중에는 객체 생성을 목적으로 정의되지 않는 클래스도 존재한다. 위에서의 가장 기초클래스인 Employee 클래스처럼.
- 순수 가상함수 : 함수의 몸체가 정의되지 않은 함수 -> 명시적으로 0을 대입하여 표현
- 추상 클래스 : 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스
✏️다형성
- 가상함수, 추상클래스의 특성 : 모습은 같지만 형태는 다르다
3. 가상소멸자와 참조자의 참조 가능성
✏️가상 소멸자
-
virtual
로 선언된 소멸자 -
유도클래스를 포인터형으로 참조한 기초클래스의 포인터 형 객체가 소멸될 때, 기초 클래스의 소멸자만 실행되고 끝나버린다. 메모리누수(릭)가 발생.
-
따라서 소멸자에도 virtual이 필요하다.
✏️참조자의 참조 가능성
- 클래스 형 포인터 변수의 특성은 참조자에도 적용이 된다. 즉,
AAA형 참조자는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다.
-
가상함수의 개념도 똑같이 적용된다.
#include <iostream> using namespace std; class First { public: void FirstFunc() { cout<<"FirstFunc()"<<endl; } virtual void SimpleFunc() { cout<<"First's SimpleFunc()"<<endl; } }; class Second: public First { public: void SecondFunc() { cout<<"SecondFunc()"<<endl; } virtual void SimpleFunc() { cout<<"Second's SimpleFunc()"<<endl; } }; class Third: public Second { public: void ThirdFunc() { cout<<"ThirdFunc()"<<endl; } virtual void SimpleFunc() { cout<<"Third's SimpleFunc()"<<endl; } }; int main(void) { Third obj; //포인터 변수 자료형이 Third인 것과 같다 obj.FirstFunc(); //Third는 맨 아래 유도클래스이므로 세 클래스의 함수에 모두 접근이 가능하다 obj.SecondFunc(); obj.ThirdFunc(); obj.SimpleFunc(); Second & sref=obj; //포인터 변수 자료형이 Second인 것과 같다 sref.FirstFunc(); //Second는 First의 유도클래스이므로 First와 자기자신의 함수에 접근 가능하다 sref.SecondFunc(); sref.SimpleFunc(); //그러나 실제로 참조하는 것은 Third형이므로 가상함수를 호출할 땐, //Third의 함수를 호출한다 First & fref=obj; //포인터 변수 자료형이 First인 것과 같다 fref.FirstFunc(); //this->FirstFunc() fref.SimpleFunc(); //마찬가지로 실제 참조형은 Third이므로 Third의 함수 호출 return 0; }
- 위 코드의 실행결과는 따라서
FirstFunc() SecondFunc() ThirdFunc() Third's SimpleFunc() FirstFunc() SecondFunc() Third's SimpleFunc() FirstFunc() Third's SimpleFunc()
이렇게 나온다.