업데이트:

태그: ,

카테고리:



REF : 윤성우의 열혈 C++

상속 기초

  • C++의 상속은 단어 그대로 물려받는다는 의미가 강하지만, 단순히 기존 클래스를 재활용하는 목적의 기능이 아니다.
  • 상속을 적절히 이용하면 프로그램의 유연성과 확장성을 확보할 수 있다.

  • A라는 클래스가 B라는 클래스를 상속한다.
    • A는 B클래스의 모든 멤버를 가진 상태로, 자신의 멤버도 가지게된다.
    • 따라서 B클래스의 생성자로 B클래스 멤버를 초기화해야한다.

class B
{
  private:
    int num;
  public:
    B(int num) : num(num)
    {}

};

class A : public B
{
  private:
    int A_Num;
  public:
    A(int a, int b) : B(b), A_Num(a)
    {}
};


유도(자식) 클래스의 객체생성과정

  • 유도클래스의 객체 생성과정에서 기초 클래스의 생성자도 호출된다.
    1. 유도클래스의 생성자가 호출.
    2. 기초 클래스의 생성자 호출을 위해 이니셜라이저를 확인.
      • 이때, 이니셜라이저에 기초클래스의 생성자를 명시하지 않으면 void 생성자가 호출된다.
      • 있으면 해당 기초클래스 생성자 호출
    3. 유도클래스의 멤버 초기화 후, 유도클래스 생성자 실행 완료.


유도(자식) 클래스의 객체소멸과정

  • 생성순서와 소멸순서는 반대이다.
  • 소멸과정에서도 똑같이 유도클래스의 소멸자와 기초클래스의 소멸자 모두 호출된다.
    1. 유도클래스의 소멸자 호출, 실행.
    2. 기초 클래스의 소멸자 호출, 실행.
      • 따라서, 각 클래스의 생성자에서 동적할당한 메모리는 각 클래스의 소멸자에서 해제해야한다.



protected선언과 세가지 형태의 상속

protect로 선언된 멤버

접근제어 지시자 중, public, private는 계속 써왔는데, protected는 이제야 공부한다.

  • 이 세 가지 접근제어 지시자가 허용하는 접근범위는
    public > protected > private
    
  • protected 멤버변수는 상속시에 private와 차이를 나타낸다.
    • protected 멤버변수: 해당 클래스를 상속하는 유도 클래스에서 접근 가능
    • private는 클래스 외부에선 접근이 불가능하나,
    • protected는 유도클래스에게만 제한적으로 접근을 허용한다.


상속의 3가지 형태

  1. protected 상속
    • protected보다 접근 허용 범위가 넓은 멤버(public)는 protected로 변환해 상속해
    • 클래스 외부에서 접근하지 못하도록 한다.
     class A
     {
       private:
         int n1;
       protected:
         int n2;
       public:
         int n3;
     };
        
     class B : protected A
     {
         int n1 <- A private이므로 B에서 접근불가.
         int n2 <- protected
         int n3 <- protected
     };
     int main(void)
     {
       B b;
    
       b.n3 //<-에러 발생
     }
    


  1. private 상속
    • private 접근 허용 범위가 넓은 멤버(public, protected)는 private로 변환해 상속해
    • 클래스 외부에서 접근하지 못하도록 한다.
     class A
     {
       private:
         int n1;
       protected:
         int n2;
       public:
         int n3;
     };
        
     class B : private A
     {
         int n1 <- A private이므로 B에서 접근불가.
         int n2 <- private
         int n3 <- private
     };
     int main(void)
     {
       B b;
    
       b.n3 //<-에러 발생
     }
    
    • private 상속을 한 클래스를 다시 private상속하면, 모든 멤버변수가 접근불가 상태이므로 의미없는 상속이된다.


  1. public 상속
    • private를 제외한 나머지를 그대로 상속한다.



상속조건

  • 상속으로 클래스의 관계를 구성하기 위해서는 조건이 필요하다.
  • IS_A 관계
    • 유도클래스는 기초클래스가 지니는 모든 것을 지니고, 유도클래스만의 추가적인 특성이 더해진다.
    • 예시
      • 사람 - 학생 : 학생은 is a 사람.
      • 사람 - 남자 : 남자 is a 사람.
  • HAS_A 관계
    • 소유관계도 상속으로 표현이 가능하다.
    • 하지만 이런 소유 관계는
      • 소유하지 않을때를 표현하는게 어렵고
      • 다른 클래스도 소유하기 시작하면 표현하기 어려워진다.(다중상속을 할 수 있지만 어려움)
    • 따라서, 소유관계는 상속으로 표현하기에 적합하지 않다.



다형성

객체 포인터의 참조관계

  • 객체 포인터 변수 : 객체의 주소값을 저장하는 포인터 변수
    • 선언시의 클래스의 객체 뿐만아니라, 이를 상속하는 유도 클래스의 객체도 가리킬 수 있다.

      class A
      {};
      class B : public A
      {};
      
      A *ptr = new A();
      A *ptr2 = new B();
      
    • 즉, cpp는 부모클래스의 포인터 변수는 부모 클래스를 직접적으로 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.


함수 오버라이딩

  • 함수 오버로딩은 동일한 이름의 여러 함수가 다양한 매개변수로 선언되어 있을때,
    매개변수에 따라서 함수를 호출하는 것을 의미한다.

  • 반면, 함수 오버라이딩은 기초클래스의 함수를 유도클래스에서 다시 선언해 기초클래스의 함수를 가리고, 유도클래스에서 함수를 덮어씌우는 것을 의미한다.

  • 그리고 유도클래스에서 기초클래스의 이름공간을 명시하면 기초클래스에서 오버라이딩되어 가려진 함수를 호출할 수 있다.



가상함수

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

class A
{
  public:
    void basefunc()
    {}
};

class B : public A
{
  public:
    void derivedfunc()
    {}
};

int main(void)
{
  A *ptr1 = new A();
  A *ptr2 = new B();
  return 0;
}
  • 앞서 이와같은 코드가 오류를 발생시키지 않는다는 것을 공부했다.
    • 컴파일러는 포인터변수를 바라볼 때, 포인터의 자료형을 위주로 보지, 내부는 들여다보지 않는다.


  • 하지만, 아래와같은 코드는 오류를 발생시킨다.
    int main(void)
    {
      A *ptr1 = new A();
      A *ptr2 = new B();
      ptr1->derivedfunc(); //오류
      return 0;
    }
    
    • 실제로 가리키는 대상은 B이므로, B의 메서드가 실행될 수 있다고 생각하지만,
    • 컴파일러는 포인터의 자료형으로 연산가능성을 확인하므로, 컴파일 에러를 일으킨다.


  • 유도클래스의 포인터에 기초클래스의 객체 대입 연산도 컴파일 에러를 일으킨다.
    int main(void)
    {
      A *ptr1 = new B();
      B *ptr2 = ptr1; //에러
    }
    
    • 컴파일러 입장에서는 ptr1에 담긴 객체가 기초클래스인지 유도클래스인지 구분할 수 없으므로, 에러를 일으킨다.


  • 마찬가지로, 컴파일러는 포인터의 자료형으로 연산가능성을 확인하므로, 클래스에 정의된 멤버에만 접근이 가능하다.



가상함수의 필요성

  • 실제로 가리키는 객체가 아닌, 포인터형으로만 호출할 함수를 결정하지말고,
  • 실제 가리키는 객체의 자료형에따라 멤버를 호출하고 싶다면, virtual키워드를 사용해야한다.
    • 가상함수가 선언되고나면, 이 함수를 오버라이딩하는 함수도 가상함수가 된다.
    • 포인터 변수가 실제로 가리키는 객체를 참조해 호출 대상을 결정한다.



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

  • 기초클래스의 의미만 가지고, 객체 생성 목적이 없는 클래스가 존재한다.
    • 이때, 가상함수를 순수가상함수로 선언하면 문제가 해결된다.
      class A
      {
      public:
        virtual void basefunc() const = 0; //순수 가상함수
      };
      
  • 순수 가상함수란 함수의 몸체가 정의되지 않은 함수를 의미한다.
    • 명시적으로 몸체를 정의하지 않겠다는 것을 컴파일러에 알린다.
    • 순수 가상함수를 가진 클래스는 객체를 생성하지 못한다.
  • 이렇게 순수 가상함수를 1개 이상 가진 클래스를 추상 클래스라고 부른다.
    • 객체 생성이 불가능한 클래스라고 한다.



다형성

  • 객체 지향을 이해하는데에 있어서 매우 중요한 요소.
  • 다형성 == 동질이상 == 모습은 같은데, 형태가 다르다 == 문장은 같은데 결과가 다르다.
  • 포인터의 자료형이 아닌, 실제 가리키는 객체의 자료형에 따라 다른 함수를 호출하는 것.
  • 이것이 다형성의 예시



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

  • virtual 선언은 소멸자에게도 올 수 있다.
  • 이를 가리켜 가리켜 가상 소멸자라고 한다.
class A
{
  private :
    char *member1;
  public :
    A(void)
    {
      member1 = new char[20];
    }
    ~A(void)
    {
      delete []member1;
    }
};

class B : public A
{
  private :
    char *member2;
  public:
    B(void) : A()
    {
      member2 = new char[20];
    }
    ~B(void)
    {
      delete []member2;
    }
};

int main(void)
{
  A *ptr1 = new B();
  delete ptr1; //소멸자 호출
  return 0;
}
  • 위 코드의 문제점은, 소멸자를 호출하면 컴파일러는 포인터형만 보고 A의 소멸자만 호출하는 것이다.
  • 결과적으로, B의 멤버변수에서 leak이 발생하게된다.
    • 포인터 변수의 자료형에 상관없이 모든 소멸자가 호출되어야 한다.
  • 기초 클래스의 소멸자에 virtual을 작성해주어야 유도클래스의 소멸자들도 가상소멸자가 되어 가장 밑의 유도클래스부터 기초클래스까지 순차적으로 소멸자가 호출된다.



참조자의 참조 가능성

  • 컴파일러가 포인터를 선언하면, 해당 포인터는 선언된 자료형의 클래스 객체 뿐만아니라, 간접적으로 상속하는 모든 객체를 가리킬 수 있다.
  • 이러한 특성은 참조자에게도 적용된다.
class A
{
  public:
    virtual void simplefunc()
    {
      std::cout<<"A simplefunc"<<std::endl;
    }
};

class B : public class A
{
  public:
    virtual void simplefunc()
    {
      std::cout<<"B simplefunc"<<std::endl;
    }
};

int main(void)
{
  B obj();

  A &ref = obj;
  B &ref = obj;
  A.simplefunc(); //B simplefunc
  B.simplefunc(); //B simplefunc
  return 0;
}
  • 참조자또한 이런 특성을 가지는데, 가상함수또한 같은 원리로 호출되어 실제 자료형으로 찾아가 멤버함수를 호출한다.



댓글남기기