문과 코린이의, [C. C++] 기록/C++ 이론

[문과 코린이의 IT 기록장] C,C++ - 상속과 다형성 2 : 가상함수 ( 기초 클래스의 포인터로 객체를 참조하기, 함수의 오버라이딩과 포인터 형, 가상함수(Virtual Function), 'OrangeMedia 급여관리 확장성 ..

벼리네 2021. 3. 7. 12:43
반응형

[문과 코린이의 IT 기록장] C,C++ - 상속과 다형성 2 : 가상함수 

( 기초 클래스의 포인터로 객체를 참조하기, 함수의 오버라이딩과 포인터 형, 가상함수(Virtual Function),  'OrangeMedia 급여관리 확장성 문제'의 주석 부분 해결,  순수 가상함수(Pure Virtual Function)와, 추상 클래스(Abstract Class), 다형성)


' 상속과 다형성 1 ' 에 대한 내용을 보고 싶다면?


 

 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++에서의 '다형성'의 대표적인 예이다.

 

 

 


* 유의사항
- 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다.
- 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다.
- 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :)

 

 

반응형