[C++] 상속 : 응용

Vaughan·2022년 8월 23일
0

C++

목록 보기
6/6
post-thumbnail

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

01-객체 포인터 변수란?

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

  • 포인터는 어떤 변수의 주소값을 저장하는 변수이다.
  • 따라서 클래스를 기반으로 생성된 객체의 주소값 저장을 위해-객체를 가리키기 위하여 해당 클래스의 타입으로 포인터 변수를 선언할 수 있다.
  • 객체 포인터 변수의 선언 및 참조
    Person* ptr;          //Person class의 객체를 가리키기 위한 ptr 포인터변수
    ptr = new Person();   //포인터 변수의 객체 참조

02-객체 포인터 변수의 특성

C++에서 객체 포인터 변수는 가리키는 객체 뿐만 아니라, 그 클래스를 직접/간접 상속하는 자식 클래스의 객체도 가리킬 수 있다.

//Person을 직접 상속하는 Student
class Student : public Person {
    ...
};

//Person을 간접 상속하는 High_Student
class High_Student : public Student {
		...
};

int main(){
    Person* ptr;          //Person class의 객체를 가리키는 ptr 포인터변수
    ptr = new Student(); 
		ptr = new High_Student();  
}

Q.이런 특성을 갖게 되는 이유는 무엇일까?

그 해답은 상속의 의미로부터 찾을 수 있다.

상속의 관계는 IS-A(~는 ~이다.)의 관계로 표현되는데, 따라서 Person을 상속받는 Student에서 상속의 관계는 “Student는 Person이다.”로 표현되는데, 이는 클래스와 해당 클래스로 만들어진 객체간의 관계와 유사하다.

  • 클래스-객체 관계 : Vaughan(객체)은 Person(클래스)이다.
  • 부모클래스-자식클래스 관계 : Student(자식)은 Person(클래스)이다.
    → 두 관계의 의미가 유사하다.

이러한 상속의 관계때문에 Person의 포인터 변수는 Student객체를 Person 객체의 일종으로 간주하게 된다.


2. 가상함수 : Virtual Function

01-C++ 컴파일러의 포인터 객체 참조 연산의 특성

  • 객체포인터의 특성을 이용하면 우리는 쉽게 하나의 포인터를 이용해 부모/자식 객체에 편하게 접근할 수 있지만, 한가지 단점이 존재한다.
  • C++ 컴파일러는 포인터 연산의 가능 유무를 판단할 때 포인터의 자료형을 기준으로 판단하며, 실제 포인터 변수가 가리키는 객체의 자료형은 고려하지 않는다.

따라서 실제로 객체 포인터가 가리키는 객체의 자료형이 Student 클래스라도, 객체 포인터의 자료형이 Person 클래스라면 자식 클래스인 Student만 가지는 멤버에는 접근할 수 없다 !


02-객체 포인터 변수를 이용해 참조할 때 발생할 수 있는 오류 (예시)

  • Person 클래스 타입으로 정의한 객체 포인터 변수를 사용하여 자식 클래스인 Student의 객체를 사용한다.
  • 이때, 가리키는 객체는 Studnet 타입이라도 포인터 변수가 정의된 자료형은 Person 클래스이기 때문에, 포인터 변수를 이용하여 객체를 참조할 때 마치 Person 클래스의 객체를 참조하는 것처럼 동작한다.
    • Student만이 가지는 멤버 변수/함수에 접근할 수 없다.
    • Student에서 재정의했던 Person의 함수를 호출하지 않고, Person에서 정의했던 기존 함수를 사용
#include<iostream>
using namespace std;

class Person {
		private:
				char name[10];
		public:
				Person(char* name) {
						strcpy(this->name, name);
				}
				void ShowInfo() const {
						cout<<"name: "<<name<<endl;
				}
};

//Person을 상속하는 Student
class Student : public Person {
		private:
				int number;   //Student만이 갖는 멤버 함수
		public:
				Student(char* name, int num) : Person(name), number(num) {}
				// Studnet만이 갖는 멤버 함수
				void GetNumber() const {
						cout<<"number: "<<number<<endl;
				}
				// 함수 오버라이딩
				void ShowInfo() const {	
						Person::ShowInfo();  // 기존 함수
						GetNumber();
				}
};

int main(){
    Person* ptr;          //Person class의 객체를 가리키는 ptr 포인터변수
    ptr = new Student("vaughan", 2022); 
	  //ptr->GetNumber();   //컴파일 오류
    ptr->ShowInfo();      //오버라이딩 이전의, Person 클래스에 정의된 기존 함수를 사용함 
    return 0;
}

03-가상 함수의 의미 및 사용

virtual 키워드를 이용하여 가상함수로 선언된 함수는 포인터 변수를 이용하여 함수를 호출할 때 포인터 변수의 자료형을 기반으로 결정하지 않고 실제로 가리키는 객체를 참조하여 호출대상을 결정한다.

  • 가상함수 또한 함수의 body를 가질 수 있다.
  • 가상함수를 오버라이딩 하는 함수(=자식클래스에서 재정의한 함수)또한 (별도로 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() {
        Third* tptr = new Third;   //선언 자료형 : Third, 실제 가리키는 객체 자료형 : Third
        Second* sptr = tptr;       //선언 자료형 : Second, 실제 가리키는 객체 자료형 : Third
        First* fptr = sptr;        //선언 자료형 : First, 실제 가리키는 객체 자료형 : Third
    
        tptr->MyFunc();
        sptr->MyFunc();
        fptr->MyFunc();
        
        delete tptr;
        return 0;
    }


04-순수 가상함수(pure virtual function)와 추상클래스(abstract class)

  • 추상클래스

    실제로 객체생성은 하지 않고, 상속만을 위해 정의된 클래스

    • 객체 생성을 목적으로 정의되지 않는 클래스
    • 이렇게 아무런 기능을 하지 않는 추상클래스를 실수로 생성했을 때, 문법적으로는 아무런 오류가 발생하지 않기 때문에 생성도지 않도록 막아두는 것이 좋다.
      → 이를 위해 순수 가상함수가 사용된다.
  • 순수 가상함수

    함수의 몸체가 정의되지 않은 함수

    • 순수 가상함수는 =0 으로 표현한다.
      // 추상 클래스
      class Employee {
      		private:
      				char name[100];
      		public:
      				Employ(char* name) {
      						strcpy(this->name, name);
      				}
      				void ShowYourname() const {
      						cout<<"name: "<<name<<endl;
      				}
      				//순수가상함수
      				virtual int GetPay() const = 0;
      				virtual void ShowSalaryInfor() const = 0;
      }
    • 이는 실제로 0을 대입하는 것이 아니라 컴파일러에게 명시적으로 해당함수가 순수 가상함수임을 보여주는 것이다.
    • 이렇게 정의된 1개 이상의 순수 가상함수를 지닌 클래스는 완전하지 않은 클래스가 되어(추상클래스) 객체를 생성하려 하면 컴파일에러가 발생한다.

05-다형성(Polymorphism)

  • C++에서 가상함수의 호출단계에서 보이는 특성을 가리켜, ‘다형성'이라고 한다.
  • 모습은 같은데(=명령문은 동일한데) 형태는(=결과는) 다르다.
    → 동일한 포인터 변수라도, 참조하는 객체의 자료형에따라 그 결과가 달라진다.

06-멤버함수와 가상함수의 동작원리

  • 멤버변수와 멤버변수

    • 어떤 클래스의 객체가 생성되면 멤버변수는 실제로 그 객체 안에 존재한다.
    • 그러나 멤버함수는 메모리의 한 공간에 별도로 위치하고, 해당 클래스로 생성된 모든 객체가 그 멤버함수를 서로 공유하는 형태를 취한다.
  • 가상함수의 동작원리

    • 1개이상의 가상함수를 포함하는 클래스에 대해서는 컴파일러가 가상함수 테이블을 만든다.

    • 가상함수 테이블은 실제 호출되야할 함수의 위치정보(=주소)를 담고 있는 테이블이다.

    • 이때 오버라이딩 된 가상함수의 주소정보는 자식클래스의 테이블에 포함되지 않는다.

      → 따라서 오버라이딩 된 가상함수를 호출하면 자동적으로 가장 마지막에 오버라이딩 한 자식 클래스의 멤버함수가 호출되는 것이다.

    • 가상함수 테이블의 예시
      AAA를 상속받은 자식클래스 BBB의 V-table

      keyvalue
      void BBB::Func1()0x1024
      void AAA::Func2()0x2048
      void BBB::Func3()0x4096

07-가상함수 테이블이 참조되는 방식

  • 가상함수 테이블은 멤버함수 호출에 쓰이는 일종의 데이터이기 때문에, 객체가 생성되지 않더라도 일단 메모리 공간에 할당된다.
  • 각 클래스의 객체에는 해당 클래스의 가상함수 테이블의 주소값이 저장된다.
  • 참조 과정
    1. 객체를 통해 어떤 가상함수가 호출되었다.
    2. 가상함수가 어디에 위치한지 파악하기 위해 가상함수 테이블이 저장된 주소를 이용해 참조된다.
    3. 참조된 가상함수 테이블에서 저장된 가상함수의 주소로 향해 가상함수를 수행한다.

+ 객체 포인터 변수와 가상함수의 활용 (예제)

프로그램 설명 및 상속관계

어떤 회사의 전체 직원에게 제공하는 급여를 계산하고 관리하고자한다.

  • 직원 종류
    • 정규직 : 정해진 월급을 받는다.
    • 임시직(알바) : 일한시간 * 시간당급여 로 계산된 급여를 받는다.
    • 영업직 : 정해진 월급 + 판매실적*상여금비율 로 계산된 급여를 받는다.
  • 클래스의 상속관계
    → 이때 Employee는 실제로 객체생성되지 않고 상속을 위해 정의된 추상클래스이다.

예제 코드

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

        /*Employee형 포인터 변수를 이용해 Employee를 상속받는 자식클래스의 메소드 호출을 위한 가상함수 선언*/
        virtual int GetPay() const = 0;
        virtual void ShowSalaryInfo() const = 0;
};

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 {
            /*PermanentWorker::GetPay() : 현재 자식 클래스에서 재정의된 함수가 아닌 부모 클래스의 원본 함수를 호출*/
            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(){
    //직원관리를 목적으로 설계된 control class 객체 생성
    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;
}

함수 오버라이딩과 함수 오버로딩

  • 함수 오버라이딩 : 부모 클래스와 동일한 이름의 함수를 자식 클래스에서 재정의 하는 것

  • 함수 오버로딩 : 부모 클래스와 동일한 이름의 함수를 자식 클래스에 재정의할 때, 매개변수의 자료형 및 개수가 다른 경우로, 이때는 함수 호출시 전달된 인자에 따라 호출되는 함수가 결정된다.

    → 이는 상속의 관계에서도 구성할 수 있다.


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

01-가상 소멸자(Virtual Destructor)

가상함수 말고도 virtual 키워드를 붙여줘야하는 대상, 소멸자

  • 가상 소멸자의 필요성

    • 부모클래스의 타입으로 선언된 포인터가 자식클래스의 객체를 가리킬 때, 객체 포인터를 이용하여 객체의 소멸을 명시하면 부모클래스의 소멸자만 호출되게된다.
    • 이런 경우에는 메모리의 누수(leak)가 발생한다.
    • 따라서 객체 소멸과정에서는 객체 포인터의 자료형과 관계없이, 실제로 포인터가 가리키는 객체의 자료형에 맞게 모든 소멸자가 호출되게 해주어야한다. → virtual 키워드의 필요성
  • 가상 소멸자의 특징

    • 부모클래스의 소멸자가 virtual로 선언되면, 이를 상속하는 자식클래스의 소멸자들은 (별도로 virtual 선언을 추가하지 않더라도,) 모두 자동으로 가상 소멸자로 선언된다.
    • 객체가 소멸되면서 가상 소멸자가 호출되면, 객체 포인터의 자료형과 관계없이 상속의 계층구조상 가장 아래에 있는 자식클래스의 소멸자가 대힌 호출된다.
      → 차례대로 부모클래스의 소멸자까지 순차적으로 호출됨
  • 가상 소멸자의 사용

    class First {
    		. . .
    		public:
    				virtual ~First() { . . . . }      // 가상 소멸자
    }
    
    //자식클래스
    class Second : public First {
    		. . . 
    		public:
    				virtual ~Second() { . . . . }
    				// ~Second() {} 로 정의해도(키워드없이) 자동으로 가상 소멸자로 정의된다. 
    }

02-참조자의 참조 가능성

C++에서 어떤 클래스의 참조자는 그 클래스를 직접/간접적으로 상속하는 모든 객체를 참조할 수 있다.

  • 객체 포인터를 다룰때 설명한 특성은 참조자에도 적용된다.
  • 또한, 객체 포인터 특성과 가상함수 등의 개념도 참조자에 그대로 적용된다.
  • 참조자의 사용 예시
    • First 클래스 또는 이를 직/간접적으로 상속하는 클래스의 객체가 인자의 대상이 되는 함수

    • 인자로 전달되는 객체의 실제 자료형과는 관계없이, 참조자가 First 클래스 타입이므로 함수 내에서는 First 클래스 내에서 정의된 멤버에 대해서만 접근할 수 있다.

      void GoodFunction(const First &ref) {}

4.다중상속 : Multiple Inheritance

01-다중상속의 의미와 문제점

다중상속은 둘 이상의 클래스를 동시에 상속하는 것을 말한다.

  • 일반적으로 다중상속은 많은 문제를 동반하기 때문에, 가급적 사용하지 않는 것이 좋다.
  • 실제로 다중상속만으로 해결가능한 문제는 존재하지 않으니…

02-다중상속의 기본방법

기본적으로 상속방법은 기존과 동일하다.

  • 다중상속시에는 상속의 대상이 되는 부모클래스를 ,를 이용하여 명시할 수 있다.
  • 또한 상속의 대상이되는 각 부모클래스를 어떤 접근제한 형태로 상속할 지 또한 별도로 지정이 가능하다.
#includ <iostream>
using namespace std;

class BaseOne {
		public:
				void SimpleFuncOne() { cout<<"BaseOne"<<endl; }
};

class BaseTwo {
		public:
				void SimpleFuncTwo() { cout<<"BaseTwo"<<endl; }
};

// 다중상속 받은 클래스
class MultiDerived : public BaseOne, protected BaseTwo {
		public:
				void ComplexFunc() {
						SimpleFuncOne();
						SimpleFuncTwo();
				}
};

03-다중상속의 모호성 (Ambiguous)

  • 다중상속의 대상이 되는 두 부모 클래스에 동일한 이름의 멤버가 존재하는 경우 문제가 발생할 수 있다. → 어떤 부모 클래스의 멤버에 접근하라는 것인지 알 수 없기 때문에 멤버의 이름만으로 접근이 불가능하다.
  • 다중상속의 모호성을 해결하기 위해서는 부모클래스이름::멤버 의 형태로 접근해야한다.
#includ <iostream>
using namespace std;

class BaseOne {
		public:
				void SimpleFunc() { cout<<"BaseOne"<<endl; }
};

class BaseTwo {
		public:
				void SimpleFunc() { cout<<"BaseTwo"<<endl; }  // 동일한 이름의 멤버함수
};

// 다중상속 받은 클래스
class MultiDerived : public BaseOne, protected BaseTwo {
		public:
				void ComplexFunc() {
						//모호성 해결
						BaseOne::SimpleFunc();
						BaseTwo::SimpleFunc();
				}
};

04-가상 상속

  • 간접상속으로 인한 다중상속의 모호성

    • Base 클래스를 상속하는 2개의 자식클래스를 가정해보자.
      • MiddleOne
      • MiddleTwo
    • 만약 Derived 클래스가 MiddleOne, MiddleTwo 클래스를 모두 상속받게 된다면 Derived 클래스는 Base 클래스에 2번 간접 상속하게된다.
    • 이렇듯 하나의 객체 안에 2개의 Base클래스 멤버가 존재하기 때문에, 어떤 Base클래스의 멤버를 호출할지에 대한 구분이 필요하다. [다중상속의 모호성]
      • MiddleOne::Func() : MiddleOne클래스가 상속한 Base클래스의 Func() 함수 호출

      • MiddleTwo::Func() : MiddleTwo클래스가 상속한 Base클래스의 Func() 함수 호출

        → 그러나 이경우, Base클래스의 멤버가 1개씩만 존재하는 것이 타당하기 때문에 해결책이 필요하다.

  • 만약 가상으로 Base 클래스를 상속하는 두 클래스를 다중상속하게 되면, Base클래스의 멤버가 1개씩만 존재하게 할 수 있다..

    → 가상 상속을 하면 Base클래스의 생성자가 1번만 호출된다.


본문은 ⟪열혈 C++ 프로그래밍, 윤성우⟫ 도서에 기반하여 정리한 내용입니다.

profile
우주의 아름다움도 다양한 지식을 접하며 스스로의 생각이 짜여나갈 때 불현듯 나를 덮쳐오리라.

0개의 댓글