[문과 코린이의 IT 기록장] C,C++ - 상속과 다형성 1 : 객체 포인터의 참조관계
(객체 포인터 변수 : 객체의 주소 값을 저장하는 포인터 변수, OrangeMedia 급여관리 확장성 문제의 1차적 해결과 함수 오버라이딩)
1. 객체 포인터 변수 : 객체의 주소 값을 저장하는 포인터 변수
1) 객체 포인터 변수 이해
- 포인터 변수는 클래스를 기반으로도 작성될 수 있다.
ex) Person : 클래스 이름
Person * ptr; // 포인터 변수 선언
ptr = new Person(); // 포인터 변수의 객체 참조 ( 위 문장이 실행되면, 포인터 변수 ptr은 Person 객체를 가리키게 된다. )
[ 여기서 꼭 알아야할 점 ]
- Person형 포인터는 Person 객체뿐만 아니라, Person을 상속하는 유도 클래스의 객체도 가리킬 수 있다.
ex ) |
- 즉, 정리하자면, C++에서 AAA형 포인터 변수는, AAA객체 혹은 AAA를 직/간접적으로 상속하는 모든 객체를 가리킬 수 있다.( = 객체의 주소값을 저장할 수 있다. )
ex )
#include <iostream>
using namespace std;
class Person {
public:
void Sleep() { cout << "Sleep" << endl; }
};
class Student : public Person {
public:
void Study() { cout << "Study" << endl; }
};
class PartTimeStudent :public Student {
public:
void Work() { cout << "Work" << endl; }
};
int main() {
Person* ptr1 = new Student(); // Student는 Person을 상속하므로, Person형 포인터 변수는 Student 객체를 가리킬 수 있음
Person* ptr2 = new PartTimeStudent(); // PartTimeStudent는 Person을 간접 상속하므로, Person형 포인터 변수는 PartTimeStudent 객체를 가리킬 수 있음.
Student* ptr3 = new PartTimeStudent(); // PartTimeStudent는 Student를 상속하므로, Student형 포인터 변수는 PartTimeStudent 객체를 가리킬 수 있음.
ptr1->Sleep(); // ptr1은 Student 객체
ptr2->Sleep(); // ptr2는 PartTimeStudent 객체
ptr3->Study(); // ptr3는 PartTimeStudent 객체
delete ptr1; delete ptr2; delete ptr3; // 포인터 변수 ptr1, ptr2, ptr3의 동적할당된 메모리 삭제
return 0;
}
2) 유도 클래스의 객체를 가리키는, 객체 포인터 변수
C++에서 AAA형 포인터 변수는, AAA객체 혹은 AAA를 직/간접적으로 상속하는 모든 객체를 가리킬 수 있다. ( = 객체의 주소값을 저장할 수 있다. ) |
- 이 특징은, 상속의 IS-A 관계를 통해 이해할 수 있다.
- 위의 경우, 기본적으로 다음 두 문장이 성립된다.
"BBB는 AAA이다." or "BBB는 AAA의 일종이다."
"CCC는 BBB이다." or "CCC는 BBB의 일종이다."
- 그리고 IS-A관계는 간접 상속의 경우에서도 유지되어야 하므로, 다음 문장도 성립된다.
"CCC는 AAA이다." or "CCC는 AAA의 일종이다."
- 이에 따라,
: AAA형 포인터 변수는 AAA객체 뿐만 아니라, BBB객체와, CCC객체도 가리킬 수 있으며,
: BBB형 포인터 변수로는 BBB 객체뿐만 아니라 CCC 객체도 가리킬 수 있는 것이다.
2. OrangeMedia 급여관리 확장성 문제의 1차적 해결과 함수 오버라이딩.
- OrangeMedia 급여관리 확장성 문제
- 고용인 : Employee
- 정규직 : PermanentWorker
- 영업직 : SalesWorker
- 임시직 : TemporaryWorker
- 각각의 관계
: 정규직, 영업직, 임시직 모두 고용의 한 형태이다. (고용인이다.)
: 영업직은 정규직의 일종이다.
- 이는 EmployeeHandler 클래스에 대한, 확장성 문제를 해결할 수 있다
: EmployeeHandler 클래스가 저장 및 관리하는 대상이, Employee 객체가 되게 하면, 이후에 Employee 클래스를 직/간접적으로 상속하는 클래스가 추가되었을 때, EmployeeHandler 클래스에는 변화가 발생하지 않는다.
ex 1 ) 위 링크의, OrangeMedia 예제와 비교하며 이해해보세요.
#include <iostream>
#include <cstring>
using namespace std;
class Employee { // 고용인 클래스
private:
char name[100]; // 이름 변수
public:
Employee(const char* name) { // 고용인 생성자
strcpy_s(this->name, name);
}
void ShowYourName() const { // 정보(이름) 출력 함수
cout << "name: " << name << endl;
}
};
class PermanentWorker : public Employee { // 정규직 클래스
private:
int salary; // 월급여 변수
public:
PermanentWorker(const char* name, int money) : Employee(name), salary(money) { } // 정규직 생성자
int GetPay() const { // 월급여 반환 함수
return salary;
}
void ShowSalaryInfo() const { // 정보(이름, 월급여) 출력 함수
ShowYourName();
cout << "salary: " << GetPay() << endl << endl;
}
};
class EmployeeHandler { // 고용인 handler 클래스
- 저장의 대상이 PermanentWorker 객체 -> Employee 객체로 변환되었다. - 그러나, PermanentWorker 객체는 Employee 객체의 일종이므로(Employee 객체를 상속하므로) , 저장이 가능하다. |
private:
Employee* empList[50]; // 고용인 리스트 포인터 변수 (Employee 객체의 주소 값을 저장하는 방식으로 객체를 저장. PermanentWorker 객체 역시 Employee 객체의 일종이므로 저장 가능.)
int empNum; // 고용인 총 개수 변수
public:
EmployeeHandler() :empNum(0) { } // 고용인 handler 생성자
void AddEmployee(Employee* emp) { // 고용인 추가 함수 (객체의 주소 값 전달 방식. Employee, PermanentWorker 객체 모두 가능)
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() { // 고용인 handler 소멸자
for (int i = 0; i < empNum; i++) {
delete empList[i];
}
}
};
int main() {
// 직원관리를 목적으로 설계된 컨트롤 클래스의 객체생성
EmployeeHandler handler;
// 직원등록
handler.AddEmployee(new PermanentWorker("Kim", 1000));
handler.AddEmployee(new PermanentWorker("LEE", 1500));
handler.AddEmployee(new PermanentWorker("Jun", 2000));
// 이번달에 지불해야 할 급여의 정보
handler.ShowAllSalaryInfo();
// 이번달에 지불해야 할 급여의 총합
handler.ShowTotalSalary();
return 0;
}
- 영업직, 임시직 관련 클래스 추가 정의 |
ex 2 ) 영업직, 임시직 클래스 추가 정의 예제
#include<iostream>
#include<cstring>
using namespace std;
class Employee { // 고용인 클래스
private:
char name[100]; // 이름 변수
public:
Employee(const char* name) { // 고용인 생성자
strcpy_s(this->name, name);
}
void ShowYourName() const { // 정보(이름) 출력 함수
cout << "name: " << name << endl;
}
};
class PermanentWorker : public Employee { // 정규직 클래스
private:
int salary; // 월급여 변수
public:
PermanentWorker(const 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(const 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; // TemporaryWorker 클래스의 GetPay() 함수가 출력됨.
}
};
class SalesWorker : public PermanentWorker { // 영업직 클래스
private:
int salesResult; // 월 판매 실적
double bonusRatio; // 상여금 비율
public:
SalesWorker(const char* name, int money, double ratio) : PermanentWorker(name, money), salesResult(0), bonusRatio(ratio) { } // 영업직 생성자
void AddSalesResult(int value) { // 월 판매 실적 추가 함수
salesResult += value;
}
int GetPay() const { // 정규직 급여 + (월 판매 실적 x 상여금 비율) 반환 함수
return PermanentWorker::GetPay() + // PermanentWorker 클래스의 GetPay() 함수 호출
(int)(salesResult * bonusRatio);
}
void ShowSalaryInfo() const { // 정보(이름, 급여) 출력 함수
ShowYourName();
cout << "salary: " << GetPay() << endl << endl; // SaleWorker 클래스의 GetPay() 함수가 출력됨
}
[ 함수 오버라이딩 ] - PermanentWorker 클래스에도 GetPay() 함수와, ShowSalayInfo() 함수가 존재하는데, 유도클래스인 SalesWorker 클래스에서도 동일한 이름과 형태로 두 함수를 정의한 것을 볼 수 있다. - 이와 같은 것을 함수의 오버라이딩이라고 하며, 함수가 오버라이딩 되면, 오버라이딩 된 기초 클래스의 함수는, 오버라이딩을 한 유도클래스의 함수에 가려지게 된다. * 즉, SalesWorker 클래스 내에서 GetPay() 함수를 호출하면, SalesWorker 클래스에 정의된 GetPay() 함수가 호출되지, PermanentWorker 내부의 함수가 호출되는 것이 아니라는 의미이다. ex ) int main(){ SalesWorker seller("Hong",1000,0.1); cout<<seller.GetPay()<<endl; // SalesWorker 클래스의 GetPay() 호출 seller.ShowSalaryInfo(); // SalesWorker 클래스의 ShowSalaryInfo() 호출 [ 주의 ] seller.PermanentWorker::ShowSalaryInfo(); // PermanentWorker 클래스의 ShowSalaryInfo() 호출 }; |
};
class EmployeeHandler { // 고용인 handler 클래스
private:
Employee* empList[50]; // 고용인 리스트 포인터 변수(Employee 객체의 주소 값을 저장하는 방식으로 객체를 저장. PermanentWorker 객체 역시 Employee 객체의 일종이므로 저장 가능.)
int empNum; // 고용인 총 개수 변수
public:
EmployeeHandler() :empNum(0) { } // 고용인 handler 생성자
void AddEmployee(Employee* emp) { // 고용인 추가 함수 (객체의 주소 값 전달 방식. Employee, PermanentWorker 객체 모두 가능)
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() { // 고용인 handler 소멸자
for (int i = 0; i < empNum; i++) {
delete empList[i];
}
}
};
int main() {
// 직원관리를 목적으로 설계된 컨트롤 클래스의 객체 생성
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);
handler.AddEmployee(seller);
// 이번 달에 지불해야 할 급여의 정보
handler.ShowAllSalaryInfo();
// 이번 달에 지불해야 할 급여의 총합
handler.ShowTotalSalary();
return 0;
}
- 위의 코드 중, PermanentWorker 클래스와 이를 상속하는 SalesWorker 클래스의, ShowSalaryInfo() 함수를 다시 한 번 살펴보자.
class PermanentWorker : public Employee { // 정규직 클래스 private: int salary; // 월급여 변수 public: PermanentWorker(const char* name, int money) : Employee(name), salary(money) { } // 정규직 생성자 int GetPay() const { // 월급여 반환 함수 return salary; } void ShowSalaryInfo() const { // 정보(이름, 월급여) 출력 함수 ShowYourName(); cout << "salary: " << GetPay() << endl << endl; } }; |
class SalesWorker : public PermanentWorker { // 영업직 클래스 private: int salesResult; // 월 판매 실적 double bonusRatio; // 상여금 비율 public: SalesWorker(const char* name, int money, double ratio) : PermanentWorker(name, money), salesResult(0), bonusRatio(ratio) { } // 영업직 생성자 void AddSalesResult(int value) { // 월 판매 실적 추가 함수 salesResult += value; } int GetPay() const { // 정규직 급여 + (월 판매 실적 x 상여금 비율) 반환 함수 return PermanentWorker::GetPay() + // PermanentWorker 클래스의 GetPay() 함수 호출 (int)(salesResult * bonusRatio); } void ShowSalaryInfo() const { // 정보(이름, 급여) 출력 함수 ShowYourName(); cout << "salary: " << GetPay() << endl << endl; // SaleWorker 클래스의 GetPay() 함수가 출력됨 } }; |
- SalesWorker 클래스의 ShowSalaryInfo() 함수는, PermanentWorker ShowSalaryInfo() 함수와 완전히 코드가 똑같은데, 오버라이딩을 한 이유가 무엇일까?
* PermanentWorker 클래스의 ShowSalaryInfo() 함수 내에서 호출되는, GetPay() 함수는 PermanentWorker 클래스의 GetPay() 함수의 호출로 이어진다.
* SalesWorker 클래스의 ShowSalaryInfo() 함수 내에서 호출되는, GetPAy() 함수는, SalesWorker 클래스의 GetPay() 함수의 호출로 이어진다.
- 주석 부분을 해제할 때 발생하는 컴파일 에러에 관한 사항에 대한 해결책은, 다음 파트의 가상함수(Virtual Function) 부분에서 더욱 자세히 설명하고자 한다.
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |