기초 클래스의 포인터로 객체 참조하기
C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단한다.
실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.
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(); //O
Second* sptr = tptr; //O
First* fptr = sptr; //O
tptr->FirstFunc(); //O
tptr->SecondFunc(); //O
tptr->ThirdFunc(); //O
sptr->FirstFunc(); //O
stpr->SecondFunc();//O
sptr->ThirdFunc();//X
fptr->FirstFunc();//O
fptr->SecondFunc();//X
fptr->ThirdFunc();//X
}
}
위의 예제를 보면 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다는 것을 알 수 있다.
다음 두 가지 경우를 자세히 살펴보자.
1.
Third* tptr = new Third();
Second* sptr = tptr;
tptr은 Third 클래스의 포인터 변수이므로, 이 포인터가 가리키는 객체는 Base 클래스는 직접 또는 간접적으로 상속하는 객체일 것이다.
따라서 Base형 포인터 변수로도 참조가 가능하다.
2.
sptr->ThirdFunc();
sptr은 Second형 포인터이므로, sptr이 가리키는 대상은 Second 객체일 수도 있기 때문에 문장이 성립하지 않을 수도 있다.
따라서 컴파일 에러가 발생한다.
함수의 오버라이딩과 포인터 형
#include <iostream>
using namespace std;
class First {
public:
void MyFunc() { cout << "FirstFunc" << endl; }
};
class Second : public First {
public:
void MyFunc() { cout << "SecondFunc" << endl; }
};
class Third : public Second {
public:
void MyFunc() { cout << "ThirdFunc" << endl; }
};
int main() {
Third* tptr = new Third();
Second* sptr = tptr;
First* fptr = sptr;
fptr->MyFunc();
sptr->MyFunc();
tptr->MyFunc();
delete tptr;
return 0;
}
위 코드에서는 3개의 클래스가 상속되어있으며, 모두 MyFunc 함수를 통해 오버리이딩 관계를 형성하고 있다.
fptr->MyFunc();
이 문장에서는 fptr이 First형 포인터이므로, 이 포인터가 가리키는 객체를 대상으로 First 클래스에 정의된 MyFunc 함수는
무조건 호출할 수 있을것이다.
따라서 First클래스에 정의된 MyFunc 함수가 호출된다.
sptr->MyFunc();
sptr이 Second형 포인터이므로, 이 포인터가 가리키는 객체에는 First의 MyFunc함수과 Second의 MyFunc 함수가 오버라이딩 관계로 존재한다. 따라서 오버라이딩을 한 Second 함수의 MyFunc함수가 호출된다.
tptr->MyFunc();
여기서 컴파일러는 tptr이 Third형 포인터라는 사실을 기억한다. 따라서 이 포인터 변수가 참조하는 객체이는 총 세 개의 MyFunc 함수가 존재하고, 이들은 오버라이딩 관계를 갖기 때문에 가장 마지막에 오버라이딩을 한 Third 클래스의 MyFunc 함수가 호출된다.
가상 함수(Virtual Function)
이전 예제를 보면서, 함수를 오버라이딩했다는 것은 해당 객체에서 호출되어야 하는 함수를 바꾼다는 의미인데,
포인터 변수의 자료형에 따라서 호출되는 함수의 종류가 달라지는 것은 문제가 있어 보인다.
이러한 문제를 해결하기 위해 C++(및 다른 객체 지향 언어)에서는 '가상함수'라는 것을 제공하고 있다.
가상함수의 선언은 다음과 같이 virtual 키워드의 선언을 통해서 이뤄진다.
class First{
public:
virtual void MyFunc() {cout<<"FirstFunc"<<endl;{
};
이렇게 가상함수가 선언되고 나면, 이 함수를 오버라이딩 하는 함수도 가상함수가 된다.
#include <iostream>
using namespace std;
class First {
public:
virtual void MyFunc() { cout << "FirstFunc" << endl; }
};
class Second : public First {
public:
//First 클래스의 MyFunc가 virtual로 선언되었으니
//오버라이딩하는 함수에는 virtual선언을 추가하지 않아도 가상함수가 된다.
virtual void MyFunc() { cout << "SecondFunc" << endl; }
};
class Third : public Second {
public:
//위와 마찬가지이다.
//그러나 virtual 선언을 넣어서 가상함수임을 보여주는 것이 좋다.
virtual void MyFunc() { cout << "ThirdFunc" << endl; }
};
int main() {
Third* tptr = new Third();
Second* sptr = tptr;
First* fptr = sptr;
fptr->MyFunc();
sptr->MyFunc();
tptr->MyFunc();
delete tptr;
return 0;
}
상속을 하는 이유
상속을 하는 이유는 무엇인가?
상속을 통해 연관된 일련의 클래스에 대해 공통적인 규약을 정의할 수 있다.
위의 급여 관리 코드에서는 상속을 통해 일련의 클래스 PermanentWorker, TemporaryWorker,SalesWorker에 공통적인 규약을 정의할 수 있는 것이다.
세 클래스에 적용된 공통 규약은 Employee 클래스이다.
Employee 클래스를 상속하는 모든 클래스의 객체는 Employee 객체로 바라볼 수 있게 된것이다.
따라서 새로운 클래스가 추가되어도 EmployeeHandler 클래스는 변경될 필요가 없는 것이다.
물론, 객체의 자료형에 따라서 호출되는 함수에는 차이가 있어야 한다.
#define _CRT_SECURE_NO_WARNINGS
#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()//PermanentWorker의 GetPay함수 호출
+ (int)(salesResult * bonusRatio);
}
void ShowSalaryInfo() const {
ShowYourName();
cout << "salary: " << GetPay() << endl << endl;//SalesWorker의 GetPay함수가 호출됨
}
};
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;
}
순수 가상 함수(Pure Virtual Function)
급여 관리 시스템의 Employee 클래스는 기초 클래스로서만 의미를 가질 뿐,
객체의 생성을 목적으로 정의된 클래스는 아니다.
이렇듯 클래스 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재한다.
class Employee
{
private:
char name[100];
public:
Employee(char* name) { ... }
void ShowYourName() const { .... }
virtual int GetPay() const = 0; //순수 가상함수
virtual void ShowSalaryInfo() const = 0; //순수 가상함수
'순수 가상함수'란 '함수의 몸체가 정의되지 않은 함수'를 의미'한다.
위에서 보듯이 =0 로 표시하지만 이것은 0의 대입을 의미하는 게 아니고,
'명시적으로 몸체를 정의하지 않았음'을 컴파일러에게 알리는 것이다.
만약 다음과 같이 객체를 생성하려고 하면 컴파일 에러가 발생한다.
Employee* emp = new Employee("Lee Dong Sook");
순수 가상함수의 장점
1. 잘못된 객체의 생성을 막을 수 있다.
2. GetPay와 ShowSalaryInfo함수는 유도 클래스에 정의된 함수가 호출되게끔 돕는데 의미가 있었을 뿐, 실제로 실행이 되는 함수는 아니었는데, 이를 보다 명확히 명시하게 되었다.
이렇듯 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 가리켜
'추상 클래스(abstract class)'라 한다.
이는 완전하지 않은, 그래서 객체 생성이 불가능한 클래스라는 의미를 지닌다.
다형성(Polymorphism)
지금까지 설명한 가상함수의 호출관계에서 보인 특성을 가리켜 '다형성'이라 한다.
다형성은 모습은 같은데 형태는 다르다 즉, "문장은 같은데 결과는 다르다"
라는 의미를 담고 있다.
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();
ptr->SimpleFunc(); //동일한 문장 존재
delete ptr;
ptr = new Second();
ptr->SimpleFunc(); //동일한 문장 존재
delete ptr;
return 0;
}
위의 main함수에는 동일한 문장이 두 번 등장한다.
여기서 ptr은 동일한 포인터 변수이지만, ptr이 참조하는 객체의 자료형이 다르기 때문에 실행 결과는 다르다.
이것이 바로 C++에서의 '다형성'이다.
'공부 > C++' 카테고리의 다른 글
[C++] 08-4. OOP 단계별 프로젝트 06단계 (0) | 2024.11.18 |
---|---|
[C++] 8-3.가상 소멸자와 참조자의 참조 가능성 (0) | 2024.11.15 |
[C++] 8-1. 객체 포인터의 참조 관계 (0) | 2024.11.11 |
[C++] 7-5. OOP 단계별 프로젝트 05단계 (1) | 2024.11.10 |
[C++] 7-4. 상속을 위한 조건 (0) | 2024.11.08 |