2025. 5. 4. 15:40ㆍProgramming Language/C++
1. 상속이란
기존 클래스(부모 클래스, 기본 클래스)의 속성(멤버 변수)와 기능(멤버 함수)을 새로운 클래스(자식 클래스, 파생 클래스)가 물려받아 사용할 수 있게 하는 객체지향 프로그래밍(OOP)의 핵심 개념 중 하나이다.
2. 상속의 기본 방법
상속의 기본적인 문법은
class 부모클래스 {
public:
void hello() {
std::cout << "Hello from Parent" << std::endl;
}
};
class 자식클래스 : public 부모클래스 {
// 부모의 hello()를 상속받음
};
와 같이 부모 클래스의 이름을 자식 클래스의 우측에 : 연산자를 사용해서 public 키워드와 함께 작성해주면 상속이 된다.
3. 상속의 종류(접근 지정자를 기준으로 해서)
상속은 접근 지정자를 기준으로 하여 3가지로 나뉠 수 있다.
class 자식 : public 부모 {}; // public 상속 → 부모의 public은 자식에서도 public
class 자식 : protected 부모 {}; // protected 상속 → 부모의 public/protected는 자식에서는 protected
class 자식 : private 부모 {}; // private 상속 → 부모의 public/protected는 자식에서는 private
4. 상속을 코드로
상속을 코드로 한번 확인해보자
먼저 부모 클래스가 될 클래스와 상속을 받을 자식 클래스를 생성해주자.
그리고 자식 클래스에서 부모 클래스를 상속해보자.
1) 부모의 상태와 기능을 물려 받는 자식 클래스
상속을 하게 되면 자식 클래스에서 부모 클래스의 멤버 변수와 멤버 함수의 사용이 가능하게 된다.
달리 말해 부모의 상태와 기능을 그대로 물려받아 사용이 가능하게 된다.
부모에 멤버 변수와 함수를 임의로 하나 생성해주자.
보면 자식 클래스에는 어떤것도 생성하지 않았는데 메인 함수 내부에서 자식 클래스를 하나 생성해서 부모의 멤버변수, 부모의 멤버 함수의 명칭으로 접근하면
이렇게 생성하지 않은 멤버 변수, 멤버 함수의 사용이 가능한 것을 볼 수 있다.
또한 클래스 내부에서도 부모 클래스의 멤버 변수 및 함수를 본인이 가지고 있는 멤버 처럼 사용이 가능하다.
함수를 호출해보면
만약 부모 클래스에서 멤버 변수 혹은 함수를 private로 제한을 주게 된다면
상속을 받는 자식 클래스에서도 접근이 불가능하고, 당연히 외부에서도 접근이 불가능하게 된다.
이러면 사실 쓸모가 없어 지게 되는데 우리가 원하는것은 외부에서 접근은 막으나 자식 클래스에서는 접근할 수 있도록 하는 것이다.
만약 멤버 변수의 직접 접근을 자식에서는 막고 부모의 멤버 함수를 통해서만 접근할 수 있도록 만든다면 멤버 변수는 그대로 두고
이렇게 함수로만 접근을 할 수 있도록 함수에서 접근하는 형태로 만들어두면 은닉성을 높일 수 있다.
이 방식 말고 그냥 멤버 변수의 접근을 자식 클래스로 한정하고 싶다면 부모의 멤버 변수의 접근 지시자를 private가 아니라 protected로 변경해주면 된다.
그러면 이렇게 상속한 자식 클래스에서는 접근이 가능하나 외부에서 해당 멤버 변수에 접근하는 것은 금지 된다.
가장 좋은 설계는 외부에 최대한의 노출을 시킬 때가 가장 좋은 설계라고 한다.
그래서 최대한 private를 사용하고 의도에 따라서 protected를 사용하는것이 좋다고 한다.
2) 한번에 여러 클래스에 상속이 가능
상속은 하나의 클래스가 여러 클래스를 상속 시킬 수 있고 상속된 클래스도 상속 시킬수 있다
이를
객체로 생성해서 디버깅으로 찍어보면
이렇게 상속 받은 객체는 부모 객체를 내부적으로 가지고 있다는 것을 알 수 있다.
3) 부모 객체와 자식 객체의 생성
부모 객체는 자식객체가 생성될때 자식 객체보다 먼저 생성된 후에 자식 객체가 생성된다
또한 자식 객체를 생성할때 원하는 부모 생성자를 선택해줄 수 있다.
자식 생성자의 오른쪽에 원하는 부모 생성자를 입력해주면
작성해둔 생성자를 호출한다.
또한 이렇게 직접 어떤 생성자를 호출할 것인지 자식클래스에서 지정해주지 않는다면 매개변수를 받지 않는 디폴트 생성자를 호출하게 된다.
그런데 부모 클래스에 이미 디폴트 생성자 외의 매개변수를 받는 생성자가 존재한다면
이렇게 컴파일 에러를 발생시킨다.
이럴 때에는 부모클래스에 디폴트 생성자를 생성해주거나
자식 클래스에서 어떤 부모 클래스를 호출 할것인지를 지정해주면 된다.
이건 용도에 따라서 원하는 방식을 선택하면 될듯하다.
그리고 보통 자식 클래스에서 선언되지 않은 멤버변수, 즉 부모 클래스에서 상속 받는 멤버변수를 자식 클래스에서 초기화하는 것은 좋지 않은 방식이다.
그렇기 때문에 생성자를 호출할때 같이 부모 생성자에 그 초기화 값을 전달해주는 방식이 좀 더 좋은 방식이다.
이제 객체를 생성할때 초기화 값을 전달하면서 생성해주면
이렇게 부모 클래스에게 직접 초기화하라고 전달해주는 방식으로 개발이 가능하다.
여기서 의문인 점은 부모 클래스의 모든 멤버 변수, 멤버 함수를 상속하는데 생성자는 상속이 되어야 하는것 아닌가?
근데 자식 클래스의 생성자를 모두 지워보면
C++ 컴파일러의 버전에 따라서 다른거 같긴한데 원래 저기서 부모 생성자를 바라볼 수 없는경우가 있다고 함
그럴 때는 using을 사용해서 범위지정자(클래스명)::생성자명을 넣어주면
Parent의 명칭을 가진 함수, 즉 생성자들을 모두 사용할 수 있도록 해준다.
그리고 특정 생성자는 상속 받기 싫다면 부모 클래스의 생성자와 동일한 형태의 자식 생성자에 delete를 대입해주면
이렇게 접근이 불가능 하게 만들어 줌을 알 수 있다.
4) 부모 객체와 자식 객체의 소멸
이번엔 소멸자를 한번 확인해보자.
생성자의 경우는 부모가 먼저 생성되고 자식이 생성되었는데 소멸의 경우는 이 역순이다.
소멸자를 각각 생성해주고 main함수에서 객체를 생성해주면
main함수가 종료되면서
자식이 소멸된 후에 부모가 소멸된다.
소멸자의 경우에는 조금 조심해야할 부분이 있는데 나중에 가면 업케스팅(Upcasting)이란 방식을 사용하는데
이렇게 부모 클래스를 통해서 자식 객체를 받아주는 형태의 개발이 가능하게 된다.
업케스팅(Upcasting)
자식 클래스의 객체를 부모 클래스 타입으로 변환하는 것으로
class Parent {};
class Child : public Parent {};
Child c;
Parent* p = &c; // ← 업캐스팅
이런 형태를 띈다.
이게 가능한 이유는 자식이 가진것은 모두 부모에서 오기 때문이고 부모 객체에서 가진것을 자식객체는 모두 사용할 수 있기 때문이다.
그렇기 때문에 반대의 경우는 불가능하다
class Parent {};
class Child : public Parent {};
Parent p;
Child* c = &p; // ← 불가능
자식 객체는 부모 클래스의 모든 멤버를 상속받아 포함하고 있으며, 따라서 자식은 부모처럼 행동할 수 있다.
하지만 부모 클래스는 자식 클래스에 어떤 기능이 추가되어 있는지 전혀 모르기 때문에, 부모 포인터나 객체로는 자식 클래스의 고유 기능에 접근할 수 없다.
class Parent {
public:
void foo() { std::cout << "Parent::foo" << std::endl; }
};
class Child : public Parent {
public:
void bar() { std::cout << "Child::bar" << std::endl; }
};
int main() {
Child c;
Parent* p = &c; // 업캐스팅
p->foo(); // ✅ OK: Parent가 알고 있는 함수
p->bar(); // ❌ ERROR: Parent는 bar()를 "모름"
}
즉, 부모 타입에서는 부모가 정의한 기능에만 접근할 수 있다고 이해하면 된다.
왜 업케스팅을 사용하는가
여러 자식 클래스들을 하나의 부모 타입으로 통일해서 다루기 위해서 사용된다.
이는 객체지향의 다형성을 활용한 예로 공통 인터페이스를 통해서 다양한 자식 객체를 통합 처리할 수 있기에 코드의 재사용성이 증가한다는 장점이 있다.
class Animal {
public:
virtual void speak() {
std::cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!\n";
}
};
이렇게 Dog와 Cat이 Animal이란 클래스를 상속받고 speak라는 공통된 함수를 오버라이딩 했다고 했을때 부모 클래스에서 이 함수에 virtural이라는 키워드를 붙여주면 해당 부모 객체를 통해 해당 함수를 불렀을때 자식의 함수를 부를 수 있도록 해준다.
int main() {
Dog d;
Cat c;
Animal* a0 = &d; // 업캐스팅: Dog* → Animal*
Animal* a1 = &c; // 업캐스팅: Cat* → Animal*
a0->speak(); // Dog의 speak
a1->speak(); // Cat의 speak
}
혹은
int main() {
Animal* a0 = new Dog(); // 업캐스팅
Animal* a1 = new Cat(); // 업캐스팅
a0->speak();
a1->speak();
delete a0;
delete a1;
}
이렇게 하나의 클래스를 통해서 여러 상속된 객체를 다를 수 있게 된다
이때 virtual이란 키워드가 붙은 함수의 경우는 자식이 override한 함수를 호출하게 된다는 점을 알아두자.
이렇게 Heap 공간에 할당된 자식 객체의 경우는 직접 delete키워드를 통해서 해제 해줘야 하는데
이렇게 해제를 시켜주면
부모 클래스의 소멸자를 호출하게 된다.
그런데 new를 통해 생성된 것은 자식 객체인데 부모 객체의 소멸자를 호출하면 자식 객체의 소멸자는 호출되지 않았기에 메모리 누수가 발생하게 된다.
그래서 이렇게 업케스팅을 하는 경우 부모객체의 소멸자의 앞에 virtual이라는 키워드를 붙여서 실행해주면
자식객체와 부모객체 모두 소멸자를 호출해주게 된다.
또한 이러한 업케스팅은
기존에 이렇게 함수에 Parent& 참조 타입을 매개변수로 받는다면 함수의 전달인자로 Parent& 타입만을 전달 할 수 있을 텐데 업케스팅이 가능하게 되면서
이러한 전달이 가능하게 되는 것이다
함수 내부에서는 매개변수로 Parent타입을 받을 것이고 내부에서는 그 객체의 멤버 변수 및 함수를 사용할텐데 자식 객체는 이 부모 객체의 멤버 변수 및 함수를 모두 가지고 있기에 에러가 발생하지 않게 개발이 가능하기 때문이다
이는 다음의 가상함수를 볼때 다시 한번 보도록 하자.
'Programming Language > C++' 카테고리의 다른 글
Part2::Ch 03. 상속 - 03. 정적 결합, 동적 결합 (0) | 2025.05.04 |
---|---|
Part2::Ch 03. 상속 - 02. 가상 함수 (0) | 2025.05.04 |
Part2::Ch 02. 연산자 오버로딩- 10. 사용자 정의 리터럴 (0) | 2025.05.03 |
Part2::Ch 02. 연산자 오버로딩- 09. 호출 연산자 오버로딩, 함수 객체 (0) | 2025.05.03 |
Part2::Ch 02. 연산자 오버로딩- 08. 변환 연산자 오버로딩, 변환 생성자, explicit (0) | 2025.05.03 |