[열혈 c++ 프로그래밍] ch8

hyng·2023년 3월 19일
0

열혈 c++ 프로그래밍을 보고 요약정리합니다.

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

  • 다음의 요구사항이 추가된다고 가정해보자.
    • 정규직, 영업직, 임시직 모두 고용의 한 형태다.
    • 영업직은 정규직의 일종이다.
    • IS-A 관계를 기반으로 다음과 같이 상속의 관계를 구성할 수 있다.
      • Employee ←PermanentWorker
      • Employee ← TemporaryWorker
      • PermanetWorker ← SalesWorker
         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 EmployeeHandler
         {
         private:
             Employee* empList[50];
             int empNum;
         public:
             EmployeeHandler() : empNum(0)
             {}
             void AddEmployee(Employee* emp)
             {
                 empList[empNum++] = emp;
             }
             void ShowAllSalaryInfo() 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() {
             EmployeeHandler handler;
         
             handler.AddEmployee(new PermanentWorker("KIM", 1000));
             handler.AddEmployee(new PermanentWorker("LEE", 1500));
             handler.AddEmployee(new PermanentWorker("JUN", 2000));
         
             handler.ShowAllSalaryInfo();
         
             handler.ShowAllSalaryInfo();
             return 0;
         }
      • 영업직 급여는 “기본급여(월 기본급여) + 인센티브” 형태

      • 임시직 급여는 “시간당 급여 * 일한 시간” 형태

         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;
             }
         };
        • PermanentWorker 클래스에도 ShowSalaryInfo() 가 정의되어 있는데 PermanentWorker를 상속받은 SalesWorker 클래스에서 ShowSalaryInfo()를 오버라이딩한 이유는
        • 함수 구현에서 동일하게 GetPay()를 호출하나 PermanentWorker.ShowSalaryInfo() 에서의 GetPay()는 PermanentWorker.GetPay()를 호출하나 SalesWorker에서는 SalesWorker.GetPay()가 호출되도록 해야하기 때문이다.
        • 이를 해결할 수 있는 방법이 가상함수이다.

8-2 가상함수

  • c++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.
            class First
            {
            public:
                void MyFunc() const
                {
                    cout << "FirstFunc" << endl;
                }
            };
            
            class Second : public First
            {
            public:
                void MyFunc() const
                {
                    cout << "SecondFunc" << endl;
                }
            };
            
            class Third : public Second
            {
            public:
                void MyFunc() const
                {
                    cout << "ThirdFunc" << endl;
                }
            };
            
            int main() {
                Third * tptr = new Third();
                Second * sptr = tptr;
                First * fptr = sptr;
            
                fptr->MyFunc(); //FirstFunc
                sptr->MyFunc(); //SecondFunc
                tptr->MyFunc(); //ThirdFunc
                delete tptr;
                return 0;
            }
  • 함수를 오버라이딩 했다는것은, 해당 객체에서 호출되어야 하는 함수를 바꾼다는 의미인데 포인터 변수의 자료형에 따라서 호출되는 함수의 종류가 달라지는것은 문제가 있어보인다.
  • 이를 위해 사용할 수 있는 것이 virtual 키워드
                class First
                {
                public:
                    virtual void MyFunc() const
                    {
                        cout << "FirstFunc" << endl;
                    }
                };
                
                class Second : public First
                {
                public:
                    virtual void MyFunc() const
                    {
                        cout << "SecondFunc" << endl;
                    }
                };
                
                class Third : public Second
                {
                public:
                    virtual void MyFunc() const
                    {
                        cout << "ThirdFunc" << endl;
                    }
                };
                
                int main() {
                    Third * tptr = new Third();
                    Second * sptr = tptr;
                    First * fptr = sptr;
                
                    fptr->MyFunc(); //ThirdFunc
                    sptr->MyFunc(); //ThirdFunc
                    tptr->MyFunc(); //ThirdFunc
                    delete tptr;
                    return 0;
                }
  • 가상함수로 선언하면 포인터의 자료형을 기반으로 호출 대상을 결정하지 않고 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.
  • 순수 가상함수와 추상 클래스
    • 위의 예시에서 Employee 클래스는 기초 클래스로만 의미를 가질 뿐, 객체의 생성을 목적으로 정의된 클래스는 아니다.
    • 이렇게 클래스 중에서도 객체 생성을 목적으로 정의되지 않은 클래스도 존재한다.
    • 하지만 Employee 클래스를 생성하더라도 이는 문법적으로 아무 문제가 없기 때문에 컴파일에러가 발생하지 않는다.
      Employee * employee = new Employee();
                  
    • 따라서 이러한 경우에는 가상함수를 순수 가상함수로 선언하여 객체의 생성을 문법적으로 막는 것이 좋다.
        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 = 0; // 순수 가상함수
             virtual int ShowSalaryInfo() const = 0; //순수 가상함수
        };
                    
    • 순수 가상함수란 함수의 몸체가 정의되지 않은 함수를 의미한다.
      • Employee 클래스는 순수 가상함수를 지닌, 완전하지 않은 클래스가 되기 때문에 Employee 객체를 생성하려고 하면 컴파일에러가 발생한다.
      • 이로써 다음의 이점을 얻을 수 있다.
        • 잘못된 객체 생성을 막을 수 있다.
        • Employee클래스의 Getpay(), ShowSalaryInfo()함수는 유도 클래스에 정의된 함수가 호출되게끔 돕는데 의미가 있었을 뿐, 실제로 실행이 되는 함수는 아니었는데 이를 보다 명확히 명시하는 효과도 얻게 되었다.
    • 하나 이상의 멤버 함수를 순수 가상함수로 선언한 클래스를 가리켜 ‘추상 클래스’라 한다.

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

  • virtual 선언은 소멸자에도 올 수 있다.
    • A클래스를 상속받은 B클래스가 있다고 할 때, 다음의 코드를 실행하면 A클래스의 소멸자만 실행된다.
        A * a = new B();
        delete a; //A클래스의 소멸자만 실행됨.
  • 이렇게되면 메모리 누수(leak)가 발생하게 된다. 그래서 객체의 소멸과정에서는 delete 연산자에 사용된 포인터 변수의 자료형에 상관없이 모든 소멸자가 호출되어야 한다.
                class A
                {
                	public:
                		 virtual ~A()
                		 {
                				....
                		 }
                }
profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글