본문 바로가기
Programming/C++

가상함수

by OKOK 2017. 8. 1.

가상함수의 선언은 virtual 키워드의 선언을 통해서 이뤄집니다. 이 함수를 오버라이딩 하는 함수도 가상함수가 됩니다. 


#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; }

};


class Third : public Second

{

public:

virtual void MyFunc() { cout << "ThirdFunc" << endl; }

};


int main(void)

{

Third * tptr = new Third();

Second * sptr = tptr;

First* fptr = sptr;


fptr->MyFunc();

sptr->MyFunc();

tptr->MyFunc();

delete tptr;

return 0;


virtual 선언을넣어서 함수가 가상함수임을 알리는 것이 좋습니다. 함수가 가상함수로 선언되면, 해당 함수호출 시, 포인터의 자료형을 기반으로 호출대상을 결정하지 않고, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다. 배열을 구성하는 포인터 변수가 Employee형 포인터 변수이므로, Employee 클래스의 멤버가 아닌 GetPay 함수와 ShowSalaryInfo 함수의 호출부분에서 컴파일 에러가 발생해서 주석처리 한 것입니다. 


#pragma warning(disable:4996)

#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;

}


virtual int GetPay() const

{

return 0;

}

virtual void ShowSalaryInfo() const

{ }

};


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()

+ (int)(salesResult*bonusRatio);

}

void ShowSalaryInfo() const

{

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 SalesWorker("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;



ShowSalaryInfo 함수와 GePay 함수는 가상함수이므로 가장 마지막에 오버라이딩을 진행한 함수가 호출됩니다. 상속을 통해 연관된 일련의 클래스에 대해 공통적인 규약을 정의할 수 있습니다. 상속을 통해 연관된 일련의 클래스 PermanentWorker, TemporaryWorker, SalesWorker에 고통 적인 규약을 정의할 수 있습니다. 따라서 EmployeeHandler 클래스는 저장되는 모든 객체를 Employee 객체로 바라볼 수 있는 것입니다. 


순수 가상 함수와 추상 클래스

클르새 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재합니다. 순수 가상 함수란 함수의 몸체가 정의되지 않는 함수를 의미합니다. 그리고 이를 표현하기 위해서, 0의 대입을 표시합니다. 이것은 0의 대입을 의미하는 것이 아니고, 명시적으로 몸체를 정의하지 않았음을 컴파일러에게 알리는 것입니다. 하나는 잘못된 객체의 생성을 막을 수 있는 것이다. 또 하나는 Employee 클래스의 GetPay 함수와 ShowSalaryInfo 함수는 유도 클래스에 정의된 함수가 호출되게끔 돕는데 의미가 있었을 뿐, 실제로 실해이 되는 함수가 아니었는데, 이를 보다 명확히 명시하는 효과도 얻게 되었습니다. 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 가리켜 추상 클래스라 합니다. 이는 완전하지 않은, 그래서 객체생성이 불가능한 클래스라는 의미를 지닙니다. 


다형성

지금까지 설명한 가상함수의 호출관계에서 보인 특성을 가리켜 다형성이라고 합니다. 다형성이란 동질이상을 의미합니다. 모습은 같은데 형태는 다르다. 문장은 같은데 결과는 다르다. ptr은 동일한 포인터변수입니다. 그럼에도 불구하고 실행결과는 다릅니다. 포인터 변수 ptr이  참조하는 객체의 자료형이 다르기 때문입니다. 


가상 소멸자와 참조자의 참조 가능성

가상함수 말고 virtual 키워드를 붙여줘야 할 대상이 하나 더 있습니다. 그건 바로 소멸자입니다. 즉, virtual 선언은 소멸자에게도 올 수 있습니다. 


가상 소멸자. virtual로 선언된 소멸자를 가리켜 가상 소멸자라 하는데, 이것이 필요한 이유의 설명을 위해서 예제를 하나 제시하겠습니다. 실행결과도 함께 참조하여 이 예제의 문제점을 지적해봅시다.


#pragma warning(disable:4996)


#include <iostream>

using namespace std;


class First

{

private:

char * strOne;

public:

First(char * str)

{

strOne = new char[strlen(str) + 1];

}

~First()

{

cout << "~First()" << endl;

delete[]strOne;

}

};


class Second : public First

{

private:

char * strTwo;

public:

Second(char * str1, char * str2) : First(str1)

{

strTwo = new char[strlen(str2) + 1];

}

~Second()

{

cout << "~Second()" << endl;

delete[]strTwo;

}

};


int main(void)

{

First * ptr = new Second("simple", "complex");

delete ptr;

return 0;


First 클래스의 생성자에서 동적 할당이 있었기 때문에 소멸자가 적절히 정의되었습니다. First 클래스의 소멸자와 Second 클래스의 소멸자가 동시에 호출되어야 합니다. 


참조자의 참조 가능성

C++에서, AAA형 포인터 변수는 AAA객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다. 객체의 주소 값을 저장할 수 있다. C++ 에서, AAA형 참조자는 AAA객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다. First 형 참조자를 이용하면 First 클래스에 정의된 MyFunc 함수가 호출되고, Second 형 참조자를 이용하면 Second 클래스에 정의된 MyFunc 함수가 호출되고, Third형 참조자를 이용하면 Third형 클래스에 정의된 MyFunc 함수가 호출됩니다. 



#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 secndFunc() { cout << "sSecondFunc()" << 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;

obj.FirstFunc();

obj.secndFunc();

obj.ThirdFunc();

obj.SimpleFunc();


Second & sref = obj;

sref.FirstFunc();

sref.secndFunc();

sref.SimpleFunc();


First & fref = obj;

fref.FirstFunc();

fref.SimpleFunc();

return 0;

}


obj는 Second를 상속하는 Third 객체이므로, Second 형 참조자로 참조가 가능합니다. 컴파일러는 참조자의 자료형을 가지고 함수의 호출 가능성을 판단하기 때문에 First 클래스에 정의된 FirstFunc 함수와 Second 클래스에 정의된 SecondFunc 함수는 호출이 가능하지만, Third 클래스에 정의된 ThirdFunc 함수는 호출이 불가능합니다. FIrst 객체 또는 First 를 직접 혹은 간접적으로 상속하는 클래스의 객체가 인자의 대상이 되는구나. 인자로 전달되는 객체의 실제 자료형에 상관없이 함수 내에서는 First 클래스에 정의된 함수만 호출 할 수 있겠구나.