[문과 코린이의 IT 기록장] C,C++ - 상속과 다형성 2 : 가상함수 ( 기초 클래스의 포인터로 객체를 참조하기, 함수의 오버라이딩과 포인터 형, 가상함수(Virtual Function), 'OrangeMedia 급여관리 확장성 ..
[문과 코린이의 IT 기록장] C,C++ - 상속과 다형성 2 : 가상함수
( 기초 클래스의 포인터로 객체를 참조하기, 함수의 오버라이딩과 포인터 형, 가상함수(Virtual Function), 'OrangeMedia 급여관리 확장성 문제'의 주석 부분 해결, 순수 가상함수(Pure Virtual Function)와, 추상 클래스(Abstract Class), 다형성)
1. 기초 클래스의 포인터로 객체를 참조하기
ex 1 )
class Base{
public:
void BaseFunc() { cout<<"Base Function"<<endl; }
};
class Derived : public Base{
public:
void DerivedFunc() { cout<<"Derived Function"<<endl; }
};
int main(){
Base * bptr = new Derived(); // 컴파일 가능
bptr->DerivedFunc(); // 컴파일 에러 발생 (맴버선택 연산자, 조건 성립 X 가능성)
Derived * dptr = bptr; // 컴파일 에러 발생 (대입 연산자, 조건 성립 X 가능성)
Base * bbptr = bptr; // 컴파일 가능 (대입 연산자, 조건 성립 O)
[ bptr->DerivedFunc(); ] - bptr이 Base형 포인터이기 때문에, 컴파일이 불가능하다. - 이 부분이 혼란스러운 부분인데, C++ 컴파일러는 포인터 연산의 가능성 여부를 판단 할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다. [ Derived * dptr = bptr; ] - bptr의 포인터 형만을 가지고, 대입의 가능성을 판단하기 때문에 컴파일 에러 발생. - 즉, bptr이 실제로 가리키는 객체가 Derived 객체라는 사실을 기억하지 않고, bptr은 Base형 포인터니까, bptr이 가리키는 대상은 Base 객체가 될 수 있다는 가능성을 파악한다. 그리고 컴파일 에러를 발생시킨다. |
}
ex 2 )
class First{
public:
void FirstFunc() { cout<<"FirstFunc"<<endl; }
};
class Second : public First{
public:
void SecondFunc() { cout<<"SecondFunc"<<endl; }
};
class Third : public Second{
public:
void ThirdFunc() { cout<<"ThirdFunc"<<endl; }
};
int main(){
Third * tptr = new Third();
Second *sptr = tptr;
First * fptr = sptr;
tptr->FirstFunc(); // 컴파일 가능
tptr->SecondFunc(); // 컴파일 가능
tptr->ThirdFunc(); // 컴파일 가능
sptr->FirstFunc(); // 컴파일 가능
sptr->SecondFunc(); // 컴파일 가능
sptr->ThirdFunc(); // 컴파일 오류
fptr->FirstFunc(); // 컴파일 가능
fptr->SecondFunc(); // 컴파일 오류
fptr->ThirdFunc(); // 컴파일 오류
}
- 즉, 포인터 형에 해당하는 클래스에 정의된 맴버에만 접근이 가능하다.
2. 함수의 오버라이딩과 포인터 형
ex )
#include <iostream>
using namespace std;
class First {
public:
void MyFunc() { cout <<"FirstFunc"<< endl; } // MyFunc()로 인한 오버라이딩
};
class Second : public First {
public:
void MyFunc() { cout <<"SecondFunc"<< endl; } // MyFunc()로 인한 오버라이딩
};
class Third : public Second {
public:
void MyFunc() { cout << "ThirdFunc" << endl; } // MyFunc()로 인한 오버라이딩
};
int main() {
Third* tptr = new Third(); // Third형 포인터 변수, tptr
Second* sptr = tptr; // Second형 포인터 변수, sptr
First* fptr = sptr; // First형 포인터 변수, fptr
fptr->MyFunc(); // First형 포인터 변수를이용해, MyFunc() 함수 호출
sptr->MyFunc(); // Second형 포인터 변수를 이용해, MyFunc() 함수 호출
- 즉, sptr이 Second형 포인터 변수이니, 이 포인터가 가리키는 객체는 First의 MyFunc() 함수와, Second의 MyFunc() 함수가 오버라이딩 관계로 존재하게 된다. 따라서 가장 마지막에 오버라이딩을 한, Second의 MyFunc() 함수를 호출해야 한다. |
tptr->MyFunc(); // Third형 포인터 변수를 이용해, MyFunc() 함수 호출
- 즉, tptr이 Third형 포인터 변수이니, 이 포인터가 가리키는 객체는 First의 MyFunc() 함수와, Second의 MyFunc() 함수와, Third의 MyFunc() 함수가 오버라이딩 관계로 존재하게 된다. 따라서 가장 마지막에 오버라이딩을 한, Third의 MyFunc() 함수를 호출해야 한다. |
delete tptr; // 힙 공간 메모리 삭제
return 0;
}
3. 가상함수(Virtual Function)
- 위의 예제에서, 우리는 이러한 생각을 해 볼 수 있다.
: 함수를 오버라이딩 했다는 것은, 해당 객체에서 호출되어야 하는 함수를 바꾼다는 의미인데, 포인터 변수의 자료형에 따라 호출되는 함수의 종류가 달라지는 것은 문제가 있어 보인다.
- 가상함수의 선언은 virtual 키워드의 선언을 통해 이뤄진다.
ex )
#include <iostream>
using namespace std;
class First {
public:
virtual void MyFunc() { cout << "FirstFunc" << endl; } // 가상함수가 선언되고 나면, 이 함수를 오버라이딩 하는 함수도 가상함수가 된다.
};
class Second : public First {
public:
virtual void MyFunc() { cout << "SecondFunc" << endl; } // virtual를 추가하지 않아도 괜찮음.
};
class Third : public Second {
public:
virtual void MyFunc() { cout << "ThirdFunc" << endl; } // virtual를 추가하지 않아도 괜찮음.
};
int main() {
Third* tptr = new Third();
Second* sptr = tptr;
First* fptr = sptr;
fptr->MyFunc();
sptr->MyFunc();
tptr->MyFunc();
// 함수가 가상함수로 선언되면, 해당 함수 호출 시, 포인터의 자료형을 기반으로 호출대상을 결정 X
// 포인터 변수가 실제로 가리키는 객체를 참조하여, 호출의 대상을 결정 O
delete tptr;
return 0;
}
4. 'OrangeMedia 급여관리 확장성 문제'의 주석 부분 해결
- 이제 위의 링크의 마지막 부분에, 존재했던 주석부분을 해제해보기로 한다.
ex )
#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 { // 정보(name) 출력 함수
cout << "name: " << name << endl;
}
virtual int GetPay() const { // 급여 반환 가상함수 (이후 모든 GetPay() 함수는 가상함수가 된다.)
return 0;
}
virtual void ShowSalaryInfo() const{ } // 정보(name, salary) 출력 가상함수 (이후 모든 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 { // 정보(name, salary) 출력 함수 (가상함수)
ShowYourName();
cout << "salary: " << GetPay() << endl << endl; // PermanentWorker 클래스의 GetPay() 함수 출력
}
};
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 { // 정보(name, salary) 출력 함수 (가상함수)
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 { // 급여 반환 함수 (가상함수)
return PermanentWorker::GetPay() + // PermanentWorker의 GetPay() 함수 호출
(int)(salesResult * bonusRatio);
}
void ShowSalaryInfo() const { // 정보(name, salary) 출력 함수 (가상함수)
ShowYourName();
cout <<"salary: "<< GetPay() << endl << endl; // SaleWorker의 GetPay() 함수가 호출됨
}
};
class EmployeeHandler { // 고용인 handler 클래스
private:
Employee* empList[50]; // 고용인 리스트 포인터 변수 (Employee 객체의 주소값을 저장하는 방식으로 객체를 저장. PermanentWorker, TemporaryWorker, SalesWorker 객체 모두, Employee 객체의 일종이므로 저장 가능.)
int empNum; // 고용인 총 개수 변수
public:
EmployeeHandler():empNum(0){ } // 고용인 handler 생성자
void AddEmployee(Employee* emp) { // 고용인 추가 함수
empList[empNum++] = emp
}
void ShowAllSalaryInfo() const { // 고용인 총 정보 출력 함수
for (int i = 0; i < empNum; i++)
empList[i]->ShowSalaryInfo();
- 배열을 구성하는 포인터 변수가 Employee형 이므로, Employee 클래스의 맴버가 아닌 GetPay() 함수와, 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);
handler.AddEmployee(alba);
// 영업직 등록
SalesWorker* seller = new SalesWorker("Hong", 1000, 0.1);
seller->AddSalesResult(7000);
handler.AddEmployee(seller);
// 이번 달에 지급해야할 급여의 정보
handler.ShowAllSalaryInfo();
// 이번 달에 지불해야 할 급여의 총합
handler.ShowTotalSalary();
return 0;
}
[ 상속을 하는 이유 ]
- 상속을 통해 연관된 일련의 클래스에 대해, 공통적인 규약을 정의할 수 있기 때문이다.
: 실제로, EmployeeHandler 클래스는, 저장되는 모든 객체를 Employee 객체로 바라보고 있다.
: 따라서, 새로운 클래스가 추가되어도, EmployeeHandler 클래스는 변경될 필요가 없는 것이다.
: 물론, 객체의 자료형에 따라 행동방식(호출되는 함수)는 차이가 있어야 한다. 그런 경우에는, 가상함수로 해결할 수 있다.
5. 순수 가상함수(Pure Virtual Function)와, 추상 클래스(Abstract Class)
- 위의 예제의 Employee 클래스에서는, 마지막으로 조금 더 개선해야 할 부분이 있다.
ex 1 )
class Employee { // 고용인 클래스 private: char name[100]; // 이름 변수 public: Employee(const char* name) { // 고용인 생성자 strcpy_s(this->name, name); } void ShowYourName() const { // 정보(name) 출력 함수 cout << "name: " << name << endl; } virtual int GetPay() const { // 급여 반환 가상함수 return 0; } virtual void ShowSalaryInfo() const{ } // 정보(name, salary) 출력 가상함수 }; |
- 이 클래스는 기초 클래스로서만 의미를 가질 뿐, 객체의 생성을 목적으로 정의된 클래스가 아니다.
- 이와 같이 클래스 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재한다.
ex ) Employee * emp = new Employee("Lee Dong Sook");
// 이와 같은 문장은 프로그래머의 실수이다.
// 이러한 실수는 문제가 없는 문장이기 때문에, 컴파일러에 의해 발견되지 않는다.
- 따라서, 이러한 문제를 해결하기 위해, 가상함수는 '순수 가상함수'로 선언하여, 객체의 생성을 문법적으로 막는 것이 좋다.
ex 2 ) 순수 가상함수 개념 도입
class Employee { // 고용인 클래스 private: char name[100]; // 이름 변수 public: Employee(const char* name) { // 고용인 생성자 strcpy_s(this->name, name); } void ShowYourName() const { // 정보(name) 출력 함수 cout << "name: " << name << endl; } virtual int GetPay() const = 0; // 순수 가상함수 virtual void ShowSalaryInfo() const = 0; // 순수 가상함수 |
- '순수 가상함수'란 '함수의 몸체가 정의하지 않은 함수'를 의미하다.
(=0을 통해, 명시적으로 몸체를 정의하지 않았음을 컴파일러에게 알린다.)
- 이와 같이, 맴버함수를 순수 가상함수로 선언한 클래스를 가리켜 '추상 클래스(abstract class)'라 한다.
6. 다형성
- 지금까지 설명한 가상함수의 호출관계에서 보인 특성을 가리켜 '다형성'이라고 한다.
- 다형성은, '모습은 같은데 형태는 다르다', 즉 '문장은 같은데 결과는 다르다'라는 뜻을 지닌다.
ex )
class First{ public: virtual void SimpleFunc() { cout<<"First"<<endl; } }; class Second : public First{ public: virtual void SimpleFunc() { cout<<"Second"<<endl;} }; int main(){ First * ptr = new First(); 1) ptr->SimpleFunc(); // 동일한 문장 존재 delete ptr; ptr = new Second(); 2) ptr ->SimpleFunc(); // 동일한 문장 존재 delete ptr; return 0; } |
- 1) 과 2)는 같은 코드이지만, 포인터 변수 ptr이 참조하는 객체의 자료형이 다르기 때문에 실행결과는 다르다. 이는 C++에서의 '다형성'의 대표적인 예이다.
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |