[C++] Chapter 08 - 상속과 다형성

Lee Jeong Min·2021년 1월 10일
0

Cpp

목록 보기
8/16
post-thumbnail
post-custom-banner

08-1 객체 포인터의 참조관계

객체 포인터 변수: 객체의 주소 값을 저장하는 포인터 변수

객체의 주소 값 저장을 위해 포인터 변수를 선언할수 있으며 객체를 상속하는 유도 클래스의 객체도 가리킬 수 있다.

#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(void)
{
	Person* ptr1 = new Student();
	Person* ptr2 = new PartTimeStudent();
	Student* ptr3 = new PartTimeStudent();
	ptr1->Sleep();
	ptr2->Sleep();
	ptr3->Study();
	delete ptr1; delete ptr2; delete ptr3;
	return 0;
}

Person형 포인터 변수에서 Perosn은 Student class의 기초 클래스이기 때문에 Person형 포인터 변수가 Studnet객체를 가리킬 수 있음을 나타낸다. Student와 PartimeStudent class 관계도 마찬가지.

급여관리 시스템의 확장성 문제의 1차적 해결과 함수 오버라이딩

전 챕터부터 급여관리 시스템과 관련한 이 책의 문제가 있었는데 여기서 이 문제를 위한 1차적인 해결로 상속을 소개한다.

  • 고용인 Employee
  • 정규직 PermanentWorker
  • 영업직 SalesWorker
  • 임시직 TemporaryWorker

이러한 관계들을 상속으로 구현함
EmployeeManger2.cpp

#include <iostream>
#include <cstring>
using namespace std;

class Employee
{
private:
	char name[100];
public:
	Employee(const char* name)
	{
		strcpy(this->name, name);
	}
	void ShowYourName() const
	{
		cout << "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;
	}
};

class EmployeeHandler
{
private:
	Employee* empList[50];
	int empNum;
public:
	EmployeeHandler() : empNum(0)
	{}
	void AddEmployee(PermanentWorker* 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));
	handler.AddEmployee(new PermanentWorker("JUN", 2000));

	handler.ShowAllSalaryInfo();

	handler.ShowTotalSalary();
	return 0;
}

이 예시코드에서 중요한점은 EmployeeHandler에 Employee클래스 객체 배열이 있는 데 여기에 이 클래스를 상속하는 클래스의 객체도 저장이 가능하다는 점이다.

함수 오버라이딩과 다른 클래스들의 정의

EmployeeManger3.cpp

#include <iostream>
#include <cstring>
using namespace std;

class Employee
{
private:
	char name[100];
public:
	Employee(const char* name)
	{
		strcpy(this->name, name);
	}
	void ShowYourName() const
	{
		cout << "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;
	}
};

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

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() + (int)(salesResult * bonusRatio);
	}
	void ShowSalrayInfo() 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(); // 오류가 나는 이유는 Employee 객체에 ShowSalaryInfo함수가 없기 때문에!
		*/ 
	}
	void ShowTotalSalary() const
	{
		int sum = 0;
		/*
		for (int i = 0; i < empNum; i++)
			sum += empList[i]->GetPay();  // 마찬가지로 empList의 자료형이 Employee로 선언되었는데 이 클래스내에 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);
	handler.AddEmployee(alba);

	SalesWorker* seller = new SalesWorker("Hong", 1000, 0.1);
	seller->AddSalesResult(7000);
	handler.AddEmployee(seller);

	handler.ShowAllSalaryInfo();

	handler.ShowTotalSalary();
	return 0;
}

위의 코드에서는 함수 오버라이딩을 통해 getpay() 함수와 ShowSalaryInfo 함수를 새로운 클래스마다 각각 멤버 함수로 정의를 해주었음. 눈 여겨 보아야 할점은 SalesWorker 클래스와 PermanentWorker 클래스의 ShowSalaryInfo 함수가 동일하지만 굳이 오버라이딩 한 이유가 그 안에서 실행되는 GetPay()함수가 다르기 때문에 별도로 정의를 한 것!


08-2 가상함수(Virtual Function)

기초 클래스의 포인터로 객체를 참조 할 시

C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.
예시 코드

class First ... 
class Second ... // First 클래스 상속
class Third ... // Second 클래스 상속

int main(void)
{
    Third * tptr = new Third();
    Second * sptr = tptr;
    First * fptr = sptr;
    
    
    // 각각 first, second, third 멤버 함수로 가지고 있다고 가정
    tptr->FirstFunc(); (o) 
    tptr->SecondFunc(); (o) 
    tptr->ThirdFunc(); (o) 
    
    sptr->FirstFunc(); (o) 
    sptr->SecondFunc(); (o) 
    sptr->ThirdFunc(); (x) 
    
    fptr->FirstFunc(); (o) 
    fptr->SecondFunc(); (x) 
    fptr->ThirdFunc(); (x)

이를 통해 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능함을 확인할 수 있다.

--> 정리: C++ 컴파일러는 포인터를 이용한 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.

함수의 오버라이딩과 포인터 형 그리고 가상함수

FunctionVirtualOverrid.cpp

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

함수가 가상함수로 선언되면 해당 함수 호출 시, 포인터의 자료형을 기반으로 호출대상 결정을 하지 않고 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정함!

급여관리 시스템 문제의 완전한 해결

가상함수와 함수 오버라이딩 개념을 사용!
EmployeeManager4.cpp

#include <iostream>
#include <cstring>
using namespace std;

class Employee
{
private:
	char name[100];
public:
	Employee(const 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(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;
	}
};

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() + (int)(salesResult * bonusRatio);
	}
	void ShowSalrayInfo() 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 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;
}

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

위의 코드에서 함수 오버라이딩과 가상함수 설정을 위해 Employee 클래스에 의미만 지닌 함수가 2가지가 있다. GetPay()함수와 ShowSalaryInfo() 함수인데 이를 순수 가상함수로 바꾸어 잘못된 객체의 생성을 막을 수 있다.
예시

virtual int GetPay() const = 0;
virtual void ShowSalaryInfo() const = 0;

이렇게 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 가리켜 추상 클래스라고 한다.

다형성

의미 : 모습은 같은데 형태는 다르다. --> C++에서 같은 자료형으로 선언했어도 다른 객체를 참조하는 경우 실행하는 함수가 같아도 결과가 달라질 수 있음.


08-3 가상 소멸자와 참조자의 참조 가능성

가상 소멸자

VirtualDestructor.cpp

#include <iostream>
using namespace std;

class First
{
private:
	char* strOne;
public:
	First(const char* str)
	{
		strOne = new char[strlen(str) + 1];
	}
	virtual ~First()
	{
		cout << "~First()" << endl;
		delete[]strOne;
	}
};

class Second : public First
{
private:
	char* strTwo;
public:
	Second(const char* str1, const 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 소멸자만 실행이 됨! 이러한 경우 메모리의 누수가 발생하므로 객체의 소멸과정에서는 모든 소멸자가 호출되어야하므로 virtual 선언을 추가해서 사용함

참조자의 참조 가능성

앞서 언급한 포인터와 참조 특성이 같음 --> 자료형에 따른 특성

profile
It is possible for ordinary people to choose to be extraordinary.
post-custom-banner

0개의 댓글