C++ Chapter 11.1 : 상속의 기본

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!


chapter 11. 상속 : 상속의 기본

Is-A 관계

  • 상속받는 방법 👉 클래스 이름 : 부모클래스 이름


🔔 재사용

공통적으로 가지는 특성과 기능부모 클래스로 한 곳에 묶고 이를 상속받아 코드를 재사용한다.

  • Student, Teacher의 공통적인 특성
    • 👉 Person 둘 다 사람이라는 데에서 나오는 공통적인 특성
    • Student, Teacher에서 부모클래스 Person상속받으면 둘의 공통적인 Person타입의 특성들에 대해선 코드를 또 쓸 필요가 없다.
      • Person의 코드들을 그대로 상속받기 때문이다.
  • 📜 Person
    #include <string>
    #include <iostream>
    
    class Person
    {
    private:
    	  std::string m_name; 
    
    public:
        Person(const std::string & name_in = "No Name")  // 생성자. 디폴트 값이 있으므로 인수 없이 호출해도 된다.
            :m_name(name_in)
        {}
    
        void setName(const std::string & name_in)
        {
            m_name = name_in;
        }
    
        std::string getName() const
        {
            return m_name;
        }
    
        void doSomething()
        {
            cout << "Person!" << endl;
        }
    };
    
  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
        // 빈 클래스
    };
    
    • Person 클래스를 상속받는다.
      • class Student : public Person
      • #include “Person.h”
  • 📜 main
    int main()
    {
        Person person;
        person.setName("사람");
        cout << person.getName() << endl;
    
        Student student;
        student.setName("사람");
        cout << student.getName() << endl;
    
        cout << sizeof(student) << endl;
    }
    

상속으로 인하여 Student 클래스는 Person의 코드들을 다시 써 줄 필요가 없다.

  • Student 클래스는 빈 클래스임에도 불구하고 부모 클래스인 Person의 멤버들을 모두 물려받아 가지고 있다.
    • Student의 멤버 변수 & 멤버 함수들
      • m_name
      • void setName(const std::string & name_in)
      • std::string getName() const
      • void doSomething()
      • 전부 Person로부터 물려 받았다.
  • Student 클래스는 빈 클래스임에도 불구하고 student객체의 사이즈를 sizeof(student) 출력하면 8니 나온다.
    • Person의 멤버 변수인 m_name의 크기.
      • 물려받아 student의 멤버 변수가 되었음.
    • private한 멤버 변수도 다 상속된다. 다만 자식클래스에서 접근이 안될 뿐.


using namespace std

부모 클래스가 있는 헤더 파일에선 using namespace std 을 생략하는게 좋다. 눈에 보이지 않아도 전부 자식 클래스에서 상속되기 때문에 자식 클래스에서 중복 선언할 수도 있는 등 나도 모르게 실수를 할 수 있다. 따라서 그냥 헤더 파일에선 일일이 std::를 붙여 사용하자


🔔 오버라이딩

  • 📜 Person
    #include <string>
    #include <iostream>
    
    class Person
    {
    private:
    	  std::string m_name; 
    
    public:
        Person(const std::string & name_in = "No Name")
            :m_name(name_in)
        {}
    
        void setName(const std::string & name_in)
        {
            m_name = name_in;
        }
    
        std::string getName() const
        {
            return m_name;
        }
    
        void doSomething()
        {
            cout << "Person!" << endl;
        }
    };
    
  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    public:
    
        
      void doSomething()  // ⭐오버라이딩⭐
      {
          cout << "Student!" << endl;  // 내용을 바꿨다.
      }
    };
    
  • 📜 main
    int main()
    {
        Person person;
        Student student;
    
        person.doSomething();    // Person! 출력
        student.doSomething();  // Studenet! 출력
    }
    

오버라이딩 : 부모로부터 상속받은 멤버 함수를 내용을 새롭게 다시 정의하는 것.

  • 함수 프로토타입은 동일하지만 내용은 다르게 새롭게 구현함.

C++은 포인터 혹은 참조하는 변수가 어떤 타입이냐를 중요하게 따진다.

  • Person클래스에 doSomething() 멤버가 있으므로 Student 클래스에도 doSomething() 멤버 함수가 상속된다.
  • 이 상태에서 Student 클래스 내에서 동일한 함수를 재정의 한다면
    • student객체는 Student타입이므로 student.doSomething()Student에서 재정의(오버라이딩)doSomething()이 호출된다.
      • C++은 포인터 혹은 참조하는 변수가 어떤 타입이냐에 따라 호출할 멤버를 결정한다.


🔔 접근 지정자

멤버 변수가 private 일 때

  • 📜 Person
    #include <string>
    #include <iostream>
    
    class Person
    {
    private:
    	  std::string m_name; 
    
    public:
        Person(const std::string & name_in = "No Name")
            :m_name(name_in)
        {}
    
        void setName(const std::string & name_in)
        {
            m_name = name_in;
        }
    
        std::string getName() const
        {
            return m_name;
        }
    
        void doSomething()
        {
            cout << m_name << endl;
        }
    };
    
  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    public:
      void doSomething()  
      {
          cout << m_name << endl;  // ❌에러❌
      }
    };
    
  • Person의 멤버 변수 m_nameprivate인 멤버 변수이기 때문에 자식 클래스인 Student상속이 되긴 하지만 자식 클래스에서 접근할 수 없다.

따라서 상속 받은 멤버 중 private한 멤버들을 접근하려면 부모 클래스의 접근 함수를 호출하여 간접 접근해야 한다.

  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    public:
      void doSomething()  
      {
          cout << Person::getName() << endl;  // 문제 없음
      }
    };
    
    • Personprivate 멤버 변수 m_name 을 리턴 받을 수 있는 상속 받은 멤버 함수 *getName()* 을 호출한다.
      • 호출시 Person::붙는 이유는 만약 자식이 *getName()*을 오버라이딩 한 상태라면 자식의 *getName()*이 호출되므로 이를 막기 위해!
        • private 멤버 변수 m_namePerson클래스 내에서만 접근할 수 있으므로 명확히 PersongetName()을 호출해달라는 의미.


부모 클래스의 멤버 변수는 private인게 좋다.

  • 부모의 멤버 변수들은 자식 클래스들에게 상속되기 때문에 자식 클래스에서 멤버 값을 직접 접근할 수 있게 된다. 예를 들어 m_name 이라고 직접 명시하여 접근하는게 가능해진다.
    • 만약 이 멤버를 삭제하거나 수정해야하는 일이 생긴다면 자식클래스들에서 직접 명시하여 사용했던 부분들까지 전부 수정을 해주어야 하는 불상사가 생긴다.
    • 따라서, 상위 클래스의 멤버 변수들은 private으로 두고, 이에 대한 접근 함수들을 상위 클래스에 함께 구현해놓는 것이 좋다.


멤버 변수가 protected 일 때

protected자식클래스들에겐 멤버 접근을 허용하지만 외부에선 못하게 막는 것이므로 상속에 있어선 public 과 같다. 자식 클래스내에선 부모로부터 상속받은 멤버를 마음껏 접근해도 됨.


🔔 생성자

자식 객체가 생성될 때, 늘 부모 생성자도 같이 호출되는데, 더 먼저 호출된다. 부모로부터 물려받은 멤버들을 정의하고 초기화해야 하니까!

생성자 호출 및 처리 순서

  1. 생성자의 인수를 받는 부분
  2. 생성자 초기화 리스트 부분
    • 메모리를 할당 받는다.
    • 부모 생성자를 호출한다. 👉 부모로부터 상속받은 멤버들을 초기화
  3. 자신의 생성자 {} 중괄호 부분 처리
    • 자신만의 멤버들을 초기화한다.


1️⃣ 메모리 할당 ( 생성자 초기화 리스트에 있는 자기 자신 멤버들은 쓰레기값만 들어있는 상태)

2️⃣ 부모 생성자 호출 (물려 받은 멤버는 할당 + 초기화까지 완료)

3️⃣ 초기화 (생성자 초기화 리스트에 있는 자기 자신 멤버들 초기화)

4️⃣ 자신의 생성자 내부 {}중괄호 부분 처리


생성자 초기화 리스트에서 부모 멤버를 초기화 시킬 순 없다.

  • 📜 Person
    #include <string>
    #include <iostream>
    
    class Person
    {
    private:
    	  std::string m_name; 
    
    public:
        Person(const std::string & name_in = "No Name")
            :m_name(name_in)
        {
            cout << "부모 생성자 호출" << endl;
        }
    };
    
  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    private:
        int m_intel;
    
    public:
    
      Student(const std::string & name_in = "No Name", const int & intel_in = 0)
          :m_intel(intel_in),  m_name(name_in) // 👉❌에러❌
      {
          cout << "자식 생성자 호출" << endl;
      }
    };
    
  • 📜 main
    int main()
    {
        Student student;
    }
    
💎출력💎

부모 생성자 호출
자식 생성자 호출
  • m_name(name_in) 👉 Student 자식 클래스의 생성자 초기화 리스트에서 바로 부모의 멤버 변수 m_name을 초기화할 순 없다.
    • 이땐 아직 부모 생성자가 호출되기 전이기 때문이다.
      • 아직 부모 멤버들이 메모리에 정의가 되지 않은 상태.
      • private이라서가 아니라 단순히 아직 부모 생성자가 호출이 되지 않아 부모 멤버들이 메모리로서 할당이 안되 m_name이 무슨 변수인지 몰라서 그런 것.
  • Student student;
    1. 생성자 초기화 리스트가 처리되고
    2. Student클래스의 부모 클래스인 Person 클래스의 생성자가 호출된다.
      • 부모로 부터 상속받은 멤버들이 정의된다.
      • “부모 생성자 호출” 출력
    3. 자신의 생성자 {} 중괄호 부분 처리
      • “자식 생성자 호출” 출력


해결법 1 : 부모의 접근 함수 호출

  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    private:
        int m_intel;
    
    public:
    
      Student(const std::string & name_in = "No Name", const int & intel_in = 0)
          :m_intel(intel_in)
      {
          Person::setName(name_in);
          cout << "자식 생성자 호출" << endl;
      }
    };
    
💎출력💎

부모 생성자 호출
자식 생성자 호출
  • Person::setName(name_in)
    • 중괄호 부분에서 상속받은 멤버 값들을 접근하여 설정할 수 있게 해주는 부모의 setter 멤버 함수를 호출해주면 된다.


해결법 2 : 초기화 리스트에서 부모 생성자 직접 호출

  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    private:
        int m_intel;
    
    public:
    
      Student(const std::string & name_in = "No Name", const int & intel_in = 0)
          :m_intel(intel_in), Person(name_in)
      {
          cout << "자식 생성자 호출" << endl;
      }
    };
    
💎출력💎

부모 생성자 호출
자식 생성자 호출
  • Person(name_in)
    • 생성자 초기화 리스트 안에서 직접 부모 생성자를 호출하여 상속받은 멤버들을 초기화 할 수 있다.
    • 위임 생성자를 사용하는 것.


주의 사항 : 부모의 디폴트 생성자가 없을 경우

위의 예시 코드들에선 📜Person의 생성자 프로토타입을 Person(const std::string & name_in = “No Name”)로 디폴트 값을 정의했기 때문에 인수를 1개 받는 생성자지만 인수가 없는 디폴트 생성자로도 역할을 할 수 있어 문제가 없었다.

  • 📜 Student
    #pragma once
    #include "Person.h"
    
    class Student : public Person
    {
    private:
        int m_intel;
    
    public:
    
      Student(const std::string & name_in = "No Name", const int & intel_in = 0)
      {
          cout << "자식 생성자 호출" << endl;
      }
    };
    

자식 객체가 생성될 때, 즉 자식 생성자가 호출될때 상속 받은 멤버들을 정의하기 위해 부모 생성자를 자동으로 호출되는데, 이 때 직접 인수 넘기는 호출을 해주는게 아니라면 컴파일러는 자동으로 부모의 디폴트 생성자를 호출한다. 부모 클래스에서 아예 생성자 정의가 안되있다면 컴파일러가 자동으로 디폴트 생성자를 만들어주므로 괜찮지만 매개 변수들이 있는 생성자만 있고 디폴트 생성자는 없는 상태로 정의가 되어 있다면 문제가 생긴다. 위 코드와 같이 자식 생성자에서 부모 생성자를 직접 명시해주지 않아 컴파일러가 부모의 디폴트 생성자를 호출해야 할 때, 부모 클래스에서 매개 변수들이 있는 생성자만 있고 디폴트 생성자는 없는 상태라면 에러가 난다. no matching function for call to ‘Person::Person()’




🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기


Cpp 카테고리 내 다른 글 보러가기

댓글 남기기