[Rookiss C++] 클래스 - 상속

황교선·2023년 4월 5일
0

cpp

목록 보기
18/19

상속은 ‘뒤를 잇다’라는 뜻을 가지고 있는 단어이다. 클래스에서도 상속을 통해 자신의 것을 물려줄 수 있다. 한 클래스를 잘 정의해놓고 이를 물려받게 하면 코드의 중복을 막을 수 있고, 이미 검증된 코드를 통해서 안정성을 확보할 수 있고, 이런 시간들을 아낄 수 있기에 시간 또한 확보할 수 있다. 항상 부모와 동일한 동작을 원하는 것은 아니기에 부모의 기능에 수정을 하거나 변경을 할 수도 있는 다양성 또한 가능하다.

  • 코드를 재활용하기 위해 나온 개념
  • 부모가 되는 클래스를 부모, 기반, 기초, 베이스 클래스 등이라 부름
  • 자식이 되는 클래스는 자식, 파생 클래스라고 부름

상속 관계의 클래스 만들기

부모 클래스

  • 부모 클래스를 정의할 때에는 접근 지정자에 유의해서 작성해야함
    접근 지정자자신의 클래스파생 클래스클래스 외부
    privateOXX
    protectedOOX
    publicOOO
    • protected : 외부로의 데이터 은닉을 하면서도 상속도 가능함

자식 클래스

class 자식클래스이름 : 접근지정자 부모클래스
{
    멤버변수;
    멤버함수;
}
  • 자식 클래스 뒤에 콜론, 접근 지정자, 부모클래스명을 차례대로 기재함
  • 부모 클래스 앞에 접근 지정자는 대부분 public으로 기술
  • 부모 클래스 앞의 접근 지정자는 이 자식 클래스까지만 잘 사용하고 이후로는 부모 클래스의 멤버에 그 접근지정자를 붙이겠다는 의미

상속 관계에서의 생성자와 소멸자

  • 생성자는 멤버 함수이지만 상속할 수 없음
  • 자식 객체가 생성되어도 부모 클래스의 생성자까지 자동 호출됨

생성자의 호출 순서

class Parent
{
public:
    Parent() { cout << "Parent" << endl; }
};

class Child : public Parent
{
public:
    Child() { cout << "Child" << endl; }
};

int main()
{
    Child a;
}
// 출력 결과
// Parent
// Child

소멸자의 호출 순서

class Parent
{
public:
    ~Parent() { cout << "~Parent" << endl; }
};

class Child : public Parent
{
public:
    ~Child() { cout << "~Child" << endl; }
};

int main()
{
    Child a;
}
// 출력 결과
// ~Child
// ~Parent

상속 관계에서의 생성자 문제

  • 자식 객체에서는 부모 생성자를 호출한 후 자신의 생성자를 호출함
  • 만약 부모 클래스에 매개변수 있는 생성자만 있다면, 기본 생성자가 컴파일러에 의해 만들어지지 않음
  • 자식 클래스에서 부모의 매개변수 있는 생성자를 명시적으로 호출하는 것이 아니면 컴파일 에러가 남
class Parent
{
public:
    // Parent() { cout << "Parent()" << endl; } // 이 생성자가 없고
    Parent(int a) { cout << "Parent(int)" << endl; } // 매개변수 있는 생성자가 있다면, 기본 생성자가 만들어지지 않음
    // 그럼 자식 클래스에서 있지도 않은 매개변수가 없는 생성자를 호출하려 하기 때문에 컴파일 에러
    // 자식 클래스에서 명시적으로 매개변수 있는 생성자를 호출하거나, 부모 클래스에 매개변수 없는 생성자를 만들 것
    ~Parent() { cout << "~Parent()" << endl; }
};

class Child : public Parent
{
public:
    Child() { cout << "Child()" << endl; }
    Child(int a) { cout << "Child(int)" << endl; }
    ~Child() { cout << "~Child()" << endl; }
};

해결 방법

  1. 자식 클래스에서 명시적으로 매개변수 있는 생성자를 호출

    Child() : Parent(0)
    {
    }
    Child(int n) : Parent(n)
    {
    }
    // Child(int n) : Parent(int n) // 잘못된 표현
  2. 부모 클래스에 매개변수 없는 생성자를 만들 것

  3. 부모 클래스에 매개변수 있는 생성자를 지울 것 → 컴파일러에 의해 기본 생성자가 만들어짐

오버라이딩

이전에 오버로딩이라는 하나의 함수 이름을 갖고 시그니처를 통해 원하는 함수를 맞춰부르도록 하는 개념을 배웠었다. 같은 함수 이름을 사용하는 것에서 오버로딩이랑 동일하지만 개념이 다르다.

부모 클래스에 정의되어 있는 함수와 동일한 형태로 자식 클래스에서 다시 정의하는 것

정의는 다음과 같고 특징으로는 아래와 같다.

  • 부모 클래스에 정의되어 있는 함수의 원형과 동일한 형태로 정의해야함
    • 매개변수의 자료형과 개수가 일치해야함
  • 오버라이딩한 함수를 갖고 있는 자식 클래스의 객체는 부모의 함수를 호출할 수 없음
  • 오버라이딩되어 은폐된 부모 함수가 필요하다면 :: 연산자를 통해 명시적으로 호출해야함
class Parent
{
public:
    void Print() { cout << "Parent Print()" << endl; }
};

class Child : public Parent
{
public:
    void Print() { cout << "Child Print()" << endl; }
};

int main()
{
    Parent p;
    Child c;

    p.Print();
    c.Print();
}
// 출력 결과
// Parent Print()
// Child Print()

상속 관계에서의 캐스팅

클래스를 기본 자료형 혹은 다른 클래스와의 형변환을 할 때와, 부모와 자식 사이의 형변환은 차이가 있다. 거기에 대해서 알아보도록 하자.

업캐스팅

부모 클래스의 포인터 변수가 자식 클래스의 객체를 갖게 될 때

Child c;
Parent* p = &c; // c 앞에 (Parent)를 붙이지 않아도 자동 형변환이 됨
  • 컴파일러에 의해 자동으로 형변환이 됨
  • 부모로부터 상속받은 부분만 사용하겠다는 의미
    • 참조 가능한 영역을 축소하겠다는 의미
  • 기능이 축소되므로 유용하지 않을 것 같지만, 다형성에서 잘 활용됨

다운캐스팅

자식 클래스의 포인터 변수가 부모 클래스의 객체를 갖게 될 때

  • 정확히는 부모 클래스의 객체가 아닌, 이전에 업캐스팅된 자식객체를 다시 자식 객체처럼 사용하기 위해 사용
  • 위와 같은 이유로 사용할 때 주의해야함
  • 사용할 때 주의하겠다는 의미로 명시적으로 형변환해야함
  • 다운캐스팅 이후 다시 자식 클래스 쪽의 멤버까지 활용할 수 있음
Child c;
Parent* pp = &c; // c 앞에 (Parent)를 붙이지 않아도 자동 형변환이 됨
Child* pc = (Child*)pp; // 명시적으로 형변환을 해주어야함

업캐스팅과 오버라이딩된 함수

자식 객체를 업캐스팅하여 오버라이딩된 함수를 호출하면 부모의 함수가 호출된다

int main()
{
    Child c;

    Parent* pp = &c; // 업캐스팅
    Child* pc = (Child*)pp; // 다운캐스팅

    c.Print();
    pp->Print(); // Parent* 였기 때문에 Parent 함수가 호출됨
    pc->Print(); // Child* 였기 때문에 Child에 오버라이딩된 함수가 호출됨
}
// 출력 결과
// Child Print()
// Parent Print()
// Child Print()
profile
성장과 성공, 그 사이 어딘가

0개의 댓글