devseop08 님의 블로그
[OOP] 상속과 다형성 본문
기초 클래스와 파생 클래스
- 객체지향 언어에서 상속은 계층관계를 사용하여 클래스 간의 속성 및 함수를 공유할 수 있도록 지원하는 매우 중요한 개념이다.
- 객체지향 언어에서는 하나의 클래스를 다른 클래스의 속성을 상속받아 정의하는 기능을 제공한다.
- 한 클래스가 다른 클래스의 한 가지 구체적인 예에 해당할 때 이 클래스 간에 'is-a' 관계가 있다고 말한다.
- 상속하는 클래스들의 공통된 특징을 모두 보유하고 있는 클래스를 기초 클래스라고 한다.
- 기초 클래스보다 더 자세한 속성을 갖는 클래스를 파생 클래스라고 한다.
- 파생 클래스는 기초 클래스의 특성을 모두 포함하고 있으며 자신의 고유 특성을 추가로 갖고 있을 수 있다.
- 파생 클래스를 표현할 때 기초 클래스의 특성들을 반복하여 표현할 필요가 없다.
- 상속은 프로그램의 비슷한 특성이 중복되는 현상을 줄여준다.
파생 클래스의 선언
- 파생 클래스를 선언하는 방법은 다음과 같다.
class DerivedClassName : visibilitySpec BaseClassName {
visibilitySpec_1 :
데이터 멤버 또는 멤버 함수 리스트;
visibilitySpec_2 :
데이터 멤버 또는 멤버 함수 리스트;
};
// visibilitySpec: 가시성 지시어, 가시성 변경자- 파생 클래스는 파생 클래스의 이름 옆에 콜론(:)과 기초 클래스의 이름을 기록하여 선언한다.
- 기초 클래스가 갖는 특성 이외에 새로 추가되는 파생 클래스의 특수한 데이터 멤버나 멤버 함수를
{};안에 기록한다. - 기초 클래스의 이름 앞에 상속의 형태를 표현하는 가시성 지시어가 올 수 있다.
- 이것은 파생 클래스에 상속된 기초 클래스의 멤버가 파생 클래스에서 어떠한 가시성을 갖게 되는가를 제한하는 것으로, public, protected, private 중 하나를 사용할 수 있다.
// Person1.hpp
#ifndef PERSON1_H_INCLUDED
#define PERSON1_H_INCLUDED
#include <iostream>
#include <string>
using namespace std;
class Person {
string name;
public:
void setName(const string& n) { name = n; }
string getName() const { return name; }
void print() const { cout << name; }
};
#endif PERSON1_H_INCLUDED// Student1.hpp
#ifndef STUDENT1_H_INCLUDED
#define STUDENT1_H_INCLUDED
#include <iostream>
#include <string>
#include "Person1.hpp" // 기초 클래스를 상속하려면 반드시 include 전처리
// 파생 클래스 Student를 기초 클래스 Person을 상속받아 선언
class Student: public Person {
string school;
public:
void setSchool(const string& s){ school = s; }
string getSchool() const { return school; }
void print() const { // 기초 클래스 Person의 멤버 함수 print 재정의(override)
Person::print(); // 기초 클래스 Person의 멤버 함수 print 호출
cout << "goes to" << school;
}
};
#endif STUDENT1_H_INCLUDED// StudMain.cpp
#include <iostream>
#include "Person1.hpp"
#include "Student1.hpp"
using namespace std;
int main() {
Person dudley; // 기초 클래스의 객체 선언
dudley.setName("Dudley"); // 기초 클래스의 함수 호출
Student harry; // 파생 클래스의 객체 선언
harry.setName("Harry"); 기초 클래스의 함수 호출
harry.setSchool("Hogwarts"); 파생 클래스의 함수 호출
dudley.print(); // 기초 클래스의 함수 호출
cout << endl;
harry.print(); // 파생 클래스의 함수 호출
cout << endl;
harry.Person::print() // 기초 클래스의 함수 호출
cout << endl;
return 0;
}- 기초 클래스에 선언된 멤버 함수 print()는 파생 클래스에도 선언돼 있다.
- 파생 클래스에서 기초 클래스의 멤버 함수를 중복 선언하여 동일한 이름의 멤버 함수가 파생 클래스에 맞게 동작할 수 있다.
- 이처럼 기초 클래스에서 동일한 이름, 매개 변수, 반환 자료형을 갖는 멤버 함수를 파생 클래스에서 정의하는 것을 멤버 함수의 재정의(override)라고 한다.
- 파생 클래스의 객체에서 재정의한 멤버 함수가 아닌 재정의하기 전의 기초 클래스의 멤버 함수를 호출하기 위해선
Person::print()와 같이 기초 클래스 이름에 범위 연산자(: :)를 사용하여 멤버 함수를 호출해야 한다.
클래스의 계층
- 파생 클래스는 그 자신도 기초 클래스가 될 수 있다.
- 클래스 간의 관계를 클래스 계층이라고 한다.
- 클래스 간의 관계는 트리 구조를 이룰 수도 있고 그래프 관계를 이룰 수도 있는데 그래프 관계를 이루는 경우엔 상속받는 기초 클래스가 2개 이상이어야 한다.(다중 상속)
액세스 제어
가시성에 따른 클래스 멤버의 접근 가능 영역

가시성의 상속

파생 클래스의 생성자 및 소멸자
- 파생 클래스는 생성자가 필요한 경우가 있다. 기초 클래스가 생성자를 가지고 있다면, 파생 클래스의 생성자는 기초 클래스의 생성자를 호출하고 나머지 필요한 부분만 정의한다.
class DerivedClassName: visibilitySpec BaseClassName
{
public:
DerivedClassName(parameterList) : BaseClassName(argsList){
파생 클래스 생성자에서 추가되는 사항
}
~DerivedClassName() {
파생 클래스 소멸자에서 추가되는 사항
}
}생성자와 소멸자의 상속
- 파생 클래스는 기초 클래스의 생성자, 소멸자 및 오버로딩된 대입 연산자를 상속하지 않는다.
- 대신, 기초 클래스의 생성자, 소멸자 및 오버로딩된 대입 연산자를 파생 클래스에서 선택 호출이 가능하다.
상속에서의 생성자
- 파생 클래스는 기초 클래스의 멤버를 포함하므로, 파생 클래스가 초기화 되기 이전에 기초 클래스에서 상속된 부분이 반드시 초기화되어야 한다.
- 파생 클래스의 객체가 생성될 때, 먼저 기초 클래스의 생성자가 최소한 자동으로라도 실행되고 그 이후에 파생 클래스의 생성자가 실행된다.
상속에서의 소멸자
- 소멸자는 생성자와 반대 순서로 호출
- 파생 클래스의 객체가 소멸될 때,
- 먼저 파생 클래스의 소멸자가 실행되고
- 그 이후 기초 클래스의 소멸자가 실행된다.
- 기초 클래스의 소멸자는 자동 호출
기초 클래스의 생성자로 인자 전달
- 파생 클래스의 객체 생성 시, 기초 클래스의 어떤 생성자를 호출할 지 결정해줄 수 있다.
- 호출할 기초 클래스의 생성자를 명시하지 않으면, 기초 클래스의 기본 생성자가 자동 호출된다.
- 파생 클래스의 생성자에 초기화 리스트를 활용해 사용자가 원하는 기초 클래스의 생성자 호출이 가능하다.
class Base {
public:
Base(){};
Base(int val){};
}
Derived::Derived(int x)
: Base{x}, {...} // 어떤 기초 클래스 생성자를 호출할 지 명시하지 않으면,
// 기초 클래스에 대해서는 기본 생성자가 자동으로 호출됨
{
}복사 생성자 상속
- 파생 클래스의 복사 생성자는 파생 클래스의 생성자와 마찬가지로 기초 클래스로부터 상속되지 않는다.
- 기초 클래스의 복사 생성자와 마찬가지로, 컴파일러가 자동 생성하지만, 필요한 경우 직접 구현해줄 수 있다.
- 기초 클래스에서 구현한 복사 생성자를 파생 클래스의 복사 생성자에서 선택적으로 호출할 수 있다.
- 파생 클래스의 이동 생성자도 파생 클래스의 복사 생성자와 마찬가지인 상속의 특성을 갖는다.
class Base {
private:
int value
public:
Base(){};
Base(int val){};
Base(const Base& other)
: value{other.value}
{
}
}
Derived::Derived(const Derived& other)
: Base{other}, {...} // 기초 클래스에서 구현한 복사 생성자 호출
{
}상속에서의 복사 생성자
- 기초 클래스의 복사 생성자를 직접 호출 가능하다.
- 파생 클래스의 복사 생성자에서 기초 클래스의 복사 생성자 호출할 시,
- 파생 클래스 참조 타입의 참조자를 기초 클래스 복사 생성자의 인자로 전달하는데, 이 때 파생 클래스의 참조자에서 기초 클래스의 참조자만 복사되어 전달(이를 슬라이싱(slicing)이라 한다.)
- slicing: 파생 클래스 타입의 객체를 기초 클래스 타입의 인수로 전달할 때 기초 클래스의 멤버만 떼어내 전달.
- 이동 생성자도 동일하게 동작한다.
복사 생성자 구현 가이드
- 파생 클래스에서 파생 클래스의 복사 생성자를 구현하지 않은 경우
- 컴파일러가 자동으로 파생 클래스의 복사 생성자를 생성
- 기초 클래스에 있는, 인자를 받지 않는 기초 클래스의 기본 생성자를 호출(기본 복사 생성자가 아닌 기본 생성자를 호출한다.)
- 파생 클래스에서 파생 클래스의 복사 생성자를 구현한 경우
- 기초 클래스를 위한 복사 생성자 혹은 이동 생성자를 명시하여 선택적으로 호출할 수 있다.
- 기초 클래스의 복사 생성자를 명시하여 선택적으로 호출하지 않으면, 기초 클래스의 인자를 받지 않는 기본 생성자 호출(기본 복사 생성자가 아닌 기본 생성자를 호출한다.)
파생 클래스와 포인터
부모 클래스 포인터와 파생 클래스 포인터
- 기초 클래스의 포인터는 파생 클래스의 객체를 가리킬 수 있다.(참조 가능)
- 그러나 파생 클래스의 포인터는 기초 클래스의 객체를 가리킬 수 없다.(참조 불가능)
Person *pPrsn1, *pPrsn2;
Person dudley{"Dudley"};
Student harry{"Harry", "Hogwarts"};
Student *pStdnt;
pPrsn1 = &dudley; // OK
pPrsn2 = &harry; // OK
pStdnt = &dudley; // Error!기초 클래스의 포인터를 가지고 접근할 수 있는 멤버들은 파생 클래스에 모두 포함되어 있다.
파생 클래스가 그것들을 기초 클래스로부터 상속받았기 때문이다.
파생 클래스의 포인터를 가지고 접근할 수 있는 멤버에는 기초 클래스에 선언되어 있지 않은 파생 클래스의 특수한 멤버들이 포함될 수 있기 때문에 파생 클래스의 포인터는 기초 클래스의 객체를 가리킬 수 없다
기초 클래스의 포인터로 객체를 가리키게 하고, 다형성을 활용하여 일반화된 처리를 하는 예는 객체지향 언어를 사용하는 프로그램에서 흔히 볼 수 있다.(다형성)
객체 포인터의 배열
- 기초 클래스 타입의 객체를 참조하는 포인터들을 하나의 배열 공간에 저장하여 기초 클래스 타입 객체들과 해당 기초 클래스를 상속한 파생 클래스 타입 객체들을 배열로 관리해줄 수 있다.

//Person.h
#include <iostream>
#include <string>
using namespace std;
class Person {
string name;
public:
Person(const string& n): name{n}{}
string getName() const { return name; }
void print() const { cout << name; }
};//Student.h
#include <iostream>
#include <string>
#include "Person.h"
class Student: public Person{
string school;
public:
Student(const string& n, const string& s): Person(n), school{n} {}
string getSchool() const { return school; }
void print() const {
Person::print();
cout << "goes to" << school;
}
}// PArrMain.cpp
#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;
void PrintPerson(const Person* const p[], int n){
for(int i=0; i < n; i++) {
p[i] -> print();
cout << endl;
}
}
int main() {
Person dudley("Dudley");
Student harry("Harry", "Hogwarts");
Student ron("Ron", "Hogwarts");
dudley.print(); // Person 클래스의 print() 호출
cout << endl;
harry.print(); // Student 클래스의 print() 호출
cout << endl << endl;
Person* pPerson[3];
pPerson[0] = &dudley;
pPerson[1] = &harry;
pPerson[2] = &ron;
PrintPerson(pPerson, 3);
return 0;
}Dudley // Person 클래스의 print() 호출
Harry goes to Hogwarts // Student 클래스의 print() 호출
Dudley // Person 클래스의 print() 호출
Harry // Person 클래스의 print() 호출
Ron // Person 클래스의 print() 호출상속에서의 멤버 함수 사용: 멤버 함수 오버라이딩, 정적 바인딩과 동적 바인딩
- 정적 바인딩: 컴파일 타임에 선언한 타입을 기준으로 호출할 함수를 결정
- 동적 바인딩: 런타임에 실제 메모리에 저장된 객체의 타입을 기준으로 호출할 함수를 결정
- 동적 바인딩은 객체지향 프로그램에서 다형성을 실현하기 위한 기반이 된다.
- 기초 클래스의 멤버 함수에 virtual 키워드를 선언해야 동적 바인딩이 일어날 수 있다.
- 즉, 기초 클래스에 가상 함수가 선언되고 기초 클래스 타입의 객체에 대한 포인터 타입이나 참조 타입 변수에 new 연산자를 통한 파생 클래스 타입의 객체가 메모리 동적 할당되는 경우에만 런타임에 객체로부터 호출할 함수가 결정된다.
class Base {
protected:
int value;
public:
Base(int value)
: value{value}
{
}
~Base(){}
Base(const Base& other)
: value{other.value}
{
}
void print(){ // virtual 키워드 미선언!!
std::cout << "Base: " << value << endl;
}
};class Derived : public Base{
private:
double doubleValue;
public:
Derived(int value)
: value{value}
{
}
~Derived(){}
Derived(const Base& other)
: Base{other} // 기초 클래스에서 구현한 복사 생성자 호출
{
}
void print(){ // 멤버 함수 오버라이드
std::cout << "Derived: " << value << endl;
}
};int main() {
Base base{1}; // 정적 바인딩, 컴파일 타임에 객체의 상태와 행동 결정
Derived derived{2}; // 정적 바인딩, 컴파일 타임에 객체의 상태와 행동 결정
base.print(); // Base: 1
derived.print(); // Derived: 2, 멤버 함수 print 오버라이딩
base = derived; // 정적 바인딩, base에 저장된 Base 타입 객체에서 복사 생성자 호출
base.print(); // Base: 2
Base* bp = new Derived(3); // 정적 바인딩, 컴파일타임에 호출 함수 결정
(*bp).print(); // Base: 3
const Base &b = derived; // 정적 바인딩, 컴파일타임에 호출 함수 결정
b.print(); // Base: 3
}가상 함수: 동적 바인딩을 위한 키워드 virtual
정적 연결(static binding)
- 포인터 혹은 참조에 의하여 함수가 호출될 때, 일반적으로 포인터 또는 참조의 유형(type)에 따라 호출되는 멤버 함수가 결정된다.
- 이러한 방식을 정적 연결이라고 하며, 프로그램이 컴파일될 때 실행할 멤버 함수가 결정된다.
#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;
int main() {
Person* p1 = new Person("Dudley");
p1 -> print(); // Person::print() 호출
cout << endl;
Person* p2 = new Student("Harry", "Hogwarts");
p2 -> print(); cout << endl; // Person::print() 호출
((Student *) p2) -> print(); // Student::print() 호출
cout << endl;
return 0;
}동적 연결과 가상 함수
- 프로그램이 실행될 때 실제 객체에 따라 멤버함수를 결정하는 방법을 동적 연결(dynamic binding)이라고 한다.
- 정적 연결에서는 컴파일할 때 포인터의 타입에 따라 미리 결정된 클래스의 멤버함수가 수행되는 반면, 동적 연결에서는 실행 중에 포인터가 가리키는 실제 객체의 클래스가 보유하고 있는 멤버함수가 선택되어 수행된다.
- C++ 언어에서 동적 연결을 구현하는 방법이 가상함수이다.
- 가상함수는 기초 클래스의 함수 앞에 예약어 virtual를 붙여 표현한다.
virtual ReturnType functionName(fParameterList);- C++에서 동적 바인딩, 런타임 다형성 구현을 위한 세 가지 조건
- 상속
- 기본 클래스의 포인터 또는 참조자 선언
- 가상 함수 선언
class Base {
protected:
int value;
public:
Base(int value)
: value{value}
{
}
~Base(){}
Base(const Base& other)
: value{other.value}
{
}
virtual void print(){ // virtual 키워드 선언: 가상 함수 선언!!
std::cout << "Base: " << value << endl;
}
};class Derived : public Base{
private:
double doubleValue;
public:
Derived(int value)
: value{value}
{
}
~Derived(){}
Derived(const Base& other)
: Base{other} // 기초 클래스에서 구현한 복사 생성자 호출
{
}
virtaul void print(){ // 멤버 함수 오버라이드
std::cout << "Derived: " << value << endl;
}
};int main() {
Base base{1}; // 정적 바인딩, 컴파일 타임에 객체의 상태와 행동 결정
Derived derived{2}; // 정적 바인딩, 컴파일 타임에 객체의 상태와 행동 결정
base.print(); // Base: 1
derived.print(); // Derived: 2, 멤버 함수 print 오버라이딩
base = derived; // 정적 바인딩, base에 저장된 Base 타입 객체에서 복사 생성자 호출
base.print(); // Base: 2
Base* bp = new Derived(3); // 동적 바인딩, 런타임에 호출 함수 결정
(*bp).print(); // Base: 3
const Base &b = derived; // 동적 바인딩, 런타임에 호출 함수 결정
b.print(); // Base: 3
}- 기초 클래스의 가상 함수를 오버라이딩한 파생 클래스의 멤버 함수에는 virtual 키워드를 선언해주지 않아도 되지만,
- 파생 클래스의 자식 클래스에 대한 동적 바인딩을 막아야 하는 경우가 아니라면, 혿동을 피하기 위해 virtual 키워드를 기초 클래스의 멤버 함수와 마찬가지로 선언해주는 것을 권고한다.
기초 클래스의 소멸자를 동적 바인딩: 가상 소멸자
- 기초 클래스 타입의 객체를 소멸시키는 소멸자가 virtual 선언되지 않으면, 파생 클래스 타입의 객체를 소멸시키는 소멸자는 호출되지 않는다.
- 클래스가 가상 함수를 가지면, 반드시 항상 가상 소멸자를 함께 정의해야함
- 기초 클래스의 소멸자가 가상 소멸자면, 파생 클래스의 소멸자도 가상소멸자로 선언
class Base {
int* ptB;
public:
Base(int n = 10) {
ptB = new int[n];
}
~Base(){
delete[] ptB;
}
...
}
class Derived: public Base {
int* ptD;
public:
Derived(int n1=10, int n2=10): Base(n1) {
ptD = new int[n2];
}
~Derived() {
delete[] ptD;
}
...
}Base* pB1 = new Base(5);
Base* pB2 = new Derived(10, 15);
...
delete pB1;
delete pB2; delete pB1은 Base 클래스의 소멸자를 호출하여 정상적으로 pB1이 가리키는 Base 타입 객체의 소멸자를 실행할 것이다.- Base 클래스의 소멸자가 virtual 키워드로 선언되지 않았기 때문에
delete pB2또한 Base 클래스의 소멸자를 호출할 것인데 이 경우 pB2가 가리키는 Derived 타입 객체의 소멸자는 호출되지 않기 때문에 결국 Derived 타입 객체가 정상적으로 제거되지 못하게 된다. - 그렇기 때문에 언제가 상속이 될 기초 클래스의 소멸자는 virtual로 선언해두어야 한다
- 그래야지만 해당 기초 클래스를 상속한 파생 클래스 타입의 객체를 정상적으로 소멸시킬 수 있다.
virtual ~Base(){
delete[] ptB;
}override와 final
- 기초 클래스의 가상 함수를 오버라이드한 파생 클래스의 멤버 함수에 override 키워드를 붙여주면 멤버 함수가 기초 클래스의 가상 함수의 시그니처와 일치하게 오버라이드하는지 컴파일 타임에 미리 확인이 가능하다.
class A
{
virtual void print() const {
...
}
}
class B : A
{
virtual void print() override { // override 키워드를 사용하여 기초 클래스 print
... // 에 선언된 const 키워드 누락됨을 컴파일러가 알림
}
}
int main()
{
A* a = new A();
a->print();
B* b = new B();
a = b;
a->print();
}- final로 선언된 클래스는 상속이 불가능하다.
class Base final {
}class Derived: public Base { // 불가
}클래스의 업캐스팅과 다운캐스팅
Person* pPrsn1 = new Person("Dudley");
Student* pStdnt1 = new Student("Harry", "Hogwarts");
Person* pPrsn2 = pStdnt1; // OK
Student* pStdnt2 = pPrsn1; // Error파생 클래스의 포인터를 기초 클래스의 포인터로 업캐스팅하는 것은 묵시적 형변환이 가능하다.
하지만 기초 클래스의 포인터를 파생 클래스의 포인터로 다운캐스팅하는 것은 묵시적 형변화이 불가능하고 명시적 형변환을 하여 컴파일을 해줄 수는 있다.
기초 클래스의 포인터를 파생 클래스의 포인터로 다운캐스팅하는 명시적 형변환
- static_cast 이용: 컴파일은 통과하지만 런타임 안전성을 보장할 수 없다.
Student* pStdnt2 = static_cast<student*>(pPrsn1);- dynamic_cast 이용: 컴파일도 통과되고 만약 런타임에 형변환 대상이 기초 클래스의 포인터라면 null을 반환하므로 이후에 null 검사가 필요하다.
Student* pStdnt2 = dynamic_cast<student*>(pPrsn1);
순수 가상 함수와 추상 클래스
추상 클래스와 상세 클래스
- 기초 클래스에서 가상 함수를 만들 때 몸체가 없이 선언만 할 수도 있다.
- 이처럼 몸체가 없는 가상 함수를 순수 가상 함수라고 한다.
- 순수 가상 함수 선언 형식
virtual ReturnType functionName(fParameterList) = 0;- 어떤 클래스가 순수 가상 함수를 포함하고 있다면, 그 클래스는 객체를 만들 수 없다.
- 아직 정의되지 않은 멤버 함수가 있기 때문이다.
- 이러한 클래스를 추상 클래스라고 한다.
- 추상 클래스를 기초로 하여 파생 클래스를 선언할 때 기초 클래스에 있는 순수 가상 함수를 그 파생 클래스에 맞는 동작을 할 수 있도록 반드시 재정의 해줘야한다.
- 만약 파생 클래스가 기초 클래스의 순수 가상 함수를 재정의해주지 않거나 재정의 해주더라도 파생 클래스에서 새로운 순수 가상 함수를 선언할 시 해당 파생 클래스 또한 추상 클래스이다.
- 모든 순수 가상 함수가 재정의되어 더 이상 순수 가상 함수가 포함되지 않은 파생 클래스는 상세 클래스라고 한다.
class A {
public:
virtual void vf() const = 0;
void f1() const {
cout << "Abstract" <<endl;
}
}
class B: public A{
public:
virtual void vf() const override {
cout << "순수 가상 함수 구현" << endl;
}
void f2() const {
cout << "Concreate" << endl;
}
}인터페이스
순수 가상 함수만을 멤버로 갖는 클래스 = 인터페이스
파생 클래스가 꼭 가져야하는 기능들을 명시해놓기 위해서 인터페이스를 사용한다.
다중 상속
- 파생 클래스는 1개 이상의 기초 클래스를 가질 수 있다.
- 2개 이상의 기초 클래스를 상속받는 것을 다중 상속이라고 한다.
다중 상속 시의 메서드 이름 중복으로 인한 모호성 해결
class Student {
//...
void print() const { cout << school << endl;}
}
class Employee {
//...
void print() const { cout << company << endl; }
}
class PartTime: public Student, public Employee{
//...
}PartTime chulsoo("ABC Univ.", "DEF Co.");
chulsoo.print(); // error : ambigous chulsoo.Student::print() // OK
chulsoo.Employee::print() // OK공통 기초 클래스의 중복 상속 문제
- 1개 이상의 기초 클래스를 상속받을 수 있기 때문에 1개의 기초 클래스가 두 번 이상 상속되는 경우가 발생할 수 있다.
- Person 클래스 하나만을 기초 클래스로 하여 이를 상속하는 클래스가 2개이고 이 클래스들을 각각 Student 클래스와 Employee 클래스라 하자.
- 그리고 Student 클래스와 Employee 클래스 둘 다 상속하는, 즉 다중 상속하는 PartTime 클래스가 있다고 한다.
class Person {
string name;
public:
Person(const string& n): name{n} {}
void print() const {
cout << name;
}
}class Student: public Person {
string school;
public:
Student(const string& n, const string& s)
:Person(n), school{s} {}
void print() const {
Person::print();
cout << "goes to " << school << endl;
}
}class Employee: public Person {
string company;
public:
Employee(const string& n, const string& c)
:Person(n), company{c} {}
void print() const {
Person::print();
cout << "is employeed by" << company << endl;
}
}class PartTime: public Student, public Employee {
public:
PartTime(const string& n, const string& s, const string& c)
: Student(n, s), Employee(n, c) {}
}int main() {
PartTime chulsoo("Chulsoo", "ABC Univ.", "DEF Co."); // Person 객체가 2개 생성
chulsoo.Student::print(); // 상속하는 메서드 모호성 해결을 위한 :: 연산자 사용
chulsoo.Employee::print(); // 상속하는 메서드 모호성 해결을 위한 :: 연산자 사용
return 0;
}// 결과
Chulsoo goes to ABC Univ.
Chulsoo is employeed by DEF Co.PartTime chulsoo("Chulsoo", "ABC Univ.", "DEF Co.");=> Person 객체가 2개 생성된다.- Person의 print 메서드에서 객체의 멤버인 name 값의 저장 주소를 출력하도록 바꿔보면
Student::print()가 출력하는 name 값의 저장 주소와Employee::print()가 출력하는 name 값의 저장 주소가 다른 것을 확인 가능하다.
// Person.h
virtual void print() const {
cout << &name;
}// 결과
005AFAD0 goes to ABC Univ.
005AFB0C is employeed by DEF Co.공통 기초 클래스의 중복 상속으로 인해 공통 기초 클래스 타입 객체가 중복 생성되는 것을 막기 위한 가상 기초 클래스 사용
- 공통 기초 클래스(Person)를 직접 상속하는 파생 클래스(Student, Employee)에서 공통 기초 클래스를 상속할 때, 공통 기초 클래스(Student) 앞에 virtual 키워드를 붙여주고
- 파생 클래스(Student, Employee)를 다중 상속하는 클래스(PartTime)에서 자신이 직접 상속하는 클래스(Student, Employee)의 생성자 뿐만 아니라 공통 기초 클래스(Person)의 생성자도 호출시켜준다.
class Person {
string name;
public:
Person(const string& n): name{n} {}
void print() const {
cout << name;
}
}class Student: virtual public Person {
string school;
public:
Student(const string& n, const string& s)
:Person(n), school{s} {}
void print() const {
Person::print();
cout << "goes to " << school << endl;
}
}class Employee: virtual public Person {
string company;
public:
Employee(const string& n, const string& c)
:Person(n), company{c} {}
void print() const {
Person::print();
cout << "is employeed by" << company << endl;
}
}class PartTime: public Student, public Employee {
public:
PartTime(const string& n, const string& s, const string& c)
: Person(n), Student(n, s), Employee(n, c) {}
void print() const {
Student::print();
Employee::print();
}
}'Language > C++' 카테고리의 다른 글
| [Basic] 연산자와 흐름 제어 구문 (6) | 2025.07.16 |
|---|---|
| [Basic] 구조체와 열거형 (1) | 2025.07.15 |
| [Basic] 상수와 리터럴 (0) | 2025.07.07 |
| [OOP] 생성자와 소멸자 (1) | 2025.06.08 |
| [Basic] 타입과 변수 (0) | 2025.06.07 |