4 minute read

1. 객체 포인터의 참조관계

✏️객체 포인터 변수

  • 객체의 주소값을 저장하는 포인터 변수
  • A ⬅️ B ⬅️ C 클래스의 상속관계가 이렇게 될 때,
    A * p = new B();, A * p = new C();, B & p = new C(); 이렇게 유도클래스 객체를 가리킬 수 있다.

    C++에서, AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.(주소 값을 저장할 수 있다)

  • IS-A 관계가 성립하기 때문이다.

✏️함수 오버라이딩

  • 오버라이딩 된 기초 클래스의 함수는 오버라이딩을 한 유도 클래스의 함수에 가려진다.
  • 기초클래스::함수형태로 오버라이딩된 기초 클래스의 함수도 호출할 수 있다.
#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;
	}
};

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()       // PermanentWorker의 GetPay 함수 호출
					+ (int)(salesResult*bonusRatio);
	}
	void ShowSalaryInfo() const                 //위 PermanentWorker의 함수와 동일하지만, this의 GetPay함수가
	{                                           //호출되어야 하므로 따로 정의해주어야 한다
		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);	// 5시간 일한결과 등록
	handler.AddEmployee(alba);

	// 영업직 등록
	SalesWorker * seller=new SalesWorker("Hong", 1000, 0.1);
	seller->AddSalesResult(7000);	// 영업실적 7000
	handler.AddEmployee(seller);

	// 이번 달에 지불해야 할 급여의 정보
	handler.ShowAllSalaryInfo();	

	// 이번 달에 지불해야 할 급여의 총합
	handler.ShowTotalSalary();
	return 0;
}
  • 유도클래스의 객체를 포인터로 가리킬 수 있고, 함수를 오버라이딩 할 수 있음으로써 새로운 클래스의 객체가 추가된다고 해도 EmployeeHandler클래스는 수정할 필요가 없어진다.
    따라서, 코드의 확장성이 매우 좋아진다.

2. 가상함수

✏️기초클래스를 포인터로 참조했을 경우

  class Base{
    public:
      void BaseFnc();
  };

  class Derived : public Base{
    public:
      void DerivedFunc();
  };
  • 일때, Base * bptr = new Derived(); 에서 컴파일러는 문제없이 지나가지만, bptr->DerivedFunc()에는 에러를 발생시킨다. 왜냐하면, C++ 컴파일러는 포인터 연산의 가능성 여부를 판단 할 때, 포인터의 자료형을 기준으로 판단하며, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다

  • 즉 컴파일러는 bptr의 형의 Base임은 알지만, 실제로는 Derived의 객체라는 것을 기억하지 않는다. 따라서,
    Derived * dptr = bptr에서도 bptr이 가리키는 대상이 Base일 수도 있다고 여기고 에러를 발생시킨다.

  • 포인터로 참조하여 함수에 접근할 때는 포인터 형에 해당하는 클래스의 멤버변수에만 접근할 수 있다.

  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;
	}
  • 앞서 제시되었던 코드에서 주석처리된 부분이 에러가 나는 이유는, 기초클래스의 포인터 형 Employee 객체 emp가 ShowSalaryInfo함수에 접근할 때, 기초클래스와 유도클래스들의 어떤 멤버 함수에 접근해야하는지 모르기 때문이다.

✏️가상함수

  • virtual 선언

  • 가상함수 선언하면 이 함수를 오버라이딩 하는 함수도 가상함수

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

  • 앞서 제시한 에러가 발생하는 코드를 정상적으로 고치려면, 포인터의 자료형인 Employee 클래스에 함수를 가상 멤버함수로 추가시키면 된다.

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

  • 클래스 중에는 객체 생성을 목적으로 정의되지 않는 클래스도 존재한다. 위에서의 가장 기초클래스인 Employee 클래스처럼.

  • 순수 가상함수 : 함수의 몸체가 정의되지 않은 함수 -> 명시적으로 0을 대입하여 표현
  • 추상 클래스 : 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스

✏️다형성

  • 가상함수, 추상클래스의 특성 : 모습은 같지만 형태는 다르다

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

✏️가상 소멸자

  • virtual로 선언된 소멸자

  • 유도클래스를 포인터형으로 참조한 기초클래스의 포인터 형 객체가 소멸될 때, 기초 클래스의 소멸자만 실행되고 끝나버린다. 메모리누수(릭)가 발생.

  • 따라서 소멸자에도 virtual이 필요하다.

✏️참조자의 참조 가능성

  • 클래스 형 포인터 변수의 특성은 참조자에도 적용이 된다. 즉,

    AAA형 참조자는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다.

  • 가상함수의 개념도 똑같이 적용된다.

    #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 SecondFunc()
        {
        	cout<<"SecondFunc()"<<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;          //포인터 변수 자료형이 Third인 것과 같다
    	obj.FirstFunc();    //Third는 맨 아래 유도클래스이므로 세 클래스의 함수에 모두 접근이 가능하다
    	obj.SecondFunc();
    	obj.ThirdFunc();
    	obj.SimpleFunc();   
    
        Second & sref=obj;    //포인터 변수 자료형이 Second인 것과 같다
        sref.FirstFunc();     //Second는 First의 유도클래스이므로 First와 자기자신의 함수에 접근 가능하다
        sref.SecondFunc();
        sref.SimpleFunc();    //그러나 실제로 참조하는 것은 Third형이므로 가상함수를 호출할 땐, 
                            //Third의 함수를 호출한다
    
    	First & fref=obj;     //포인터 변수 자료형이 First인 것과 같다
    	fref.FirstFunc();     //this->FirstFunc()
    	fref.SimpleFunc();    //마찬가지로 실제 참조형은 Third이므로 Third의 함수 호출
    	return 0;
    }
    
    • 위 코드의 실행결과는 따라서
    FirstFunc()
    SecondFunc()
    ThirdFunc()
    Third's SimpleFunc()
    
    FirstFunc()
    SecondFunc()
    Third's SimpleFunc()
    
    FirstFunc()
    Third's SimpleFunc()
    

    이렇게 나온다.

Tags: ,

Categories:

Updated: