상속(inheritance)
상속(inheritance)은 추상화, 캡슐화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나입니다.
상속은 사용자에게 높은 수준의 코드 재활용성을 제공하며, 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련합니다.
클래스 상속(class inheritance)
C++에서 클래스 상속이란 기존에 정의되어 있는 클래스의 모든 멤버 변수와 멤버 함수를 물려받아, 새로운 클래스를 작성하는 것을 의미합니다.
이때 기존에 정의되어 있던 클래스를 기초 클래스(base class) 또는 부모 클래스(parent class), 상위 클래스(super class)라고도합니다.
그리고 상속을 통해 새롭게 작성되는 클래스를 파생 클래스(derived class) 또는 자식 클래스(child class), 하위 클래스(sub class)라고도 합니다.
이와 같은 클래스의 상속은 다음과 같은 장점을 가집니다.
1. 기존에 작성된 클래스를 재활용할 수 있습니다.
2. 공통적인 부분은 기초 클래스에 미리 작성하여, 파생 클래스에서 중복되는 부분을 제거할 수 있습니다.
파생 클래스(derived class)
파생 클래스(derived class)란 기초 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 가리킵니다.
C++에서 파생 클래스는 다음과 같은 문법을 통해 선언합니다.
문법
class 파생클래스이름 : 접근제어지시자 기초클래스이름[, 접근제어지시자 기초클래스이름, ...]
{
// 파생 클래스 멤버 리스트
}
접근 제어 지시자는 기초 클래스의 멤버를 사용할 수 있는 파생 클래스의 접근 제어 권한을 설정합니다.
이때 접근 제어 지시자를 생략하면, 파생 클래스의 접근 제어 권한은 private로 기본 설정됩니다.
또한, 쉼표(,)를 사용하여 상속받을 기초 클래스를 여러 개 명시할 수 있습니다.
이때 파생 클래스가 상속받는 기초 클래스가 하나이면 단일 상속(single inheritance)이라 합니다.
만약 여러 개의 기초 클래스를 상속받으면 다중 상속(multiple inheritance)이라고 합니다.
파생 클래스의 특징
C++에서 파생 클래스는 다음과 같은 특징을 가지고 있습니다.
1. 파생 클래스는 반드시 자신만의 생성자를 작성해야 합니다.
2. 파생 클래스에는 기초 클래스의 접근할 수 있는 모든 멤버 변수들이 저장됩니다.
3. 파생 클래스는 기초 클래스의 접근할 수 있는 모든 멤버 함수를 사용할 수 있습니다.
4. 파생 클래스에는 필요한 만큼 멤버를 추가할 수 있습니다.
우선 다음과 같은 Person 클래스가 미리 작성되어 있다고 가정합니다.
Person 클래스
class Person
{
private:
string name_;
int age_;
public:
Person(const string& name, int age); // 기초 클래스 생성자의 선언
void ShowPersonInfo();
};
...
Person::Person(const string& name, int age) // 기초 클래스 생성자의 정의
{
name_ = name;
age_ = age;
}
다음은 기존에 작성된 Person 클래스를 상속받는 Student 클래스를 작성하는 예제입니다.
Student 클래스
class Student : public Person {
private:
int student_id_;
public:
Student(int sid, const string& name, int age); // 파생 클래스 생성자의 선언
};
...
Student::Student(int sid, const string& name, int age) : Person(name, age) // 파생 클래스 생성자의 선언
{
student_id_ = sid;
}
위의 예제에서 보듯이 파생 클래스의 생성자는 기초 클래스의 생성자를 사용하고 있습니다.
그 이유는 파생 클래스의 생성자가 기초 클래스의 private 멤버에 접근할 수 없기 때문입니다.
따라서 기초 클래스의 생성자를 사용해야만 기초 클래스의 private 멤버를 초기화할 수 있습니다.
이때 기초 클래스의 생성자를 명시하지 않으면, 프로그램은 기초 클래스의 디폴트 생성자를 사용하게 됩니다.
예제
class Person
{
private:
string name_;
int age_;
public:
Person(const string& name, int age); // 기초 클래스 생성자의 선언
void ShowPersonInfo();
};
class Student : public Person
{
private:
int student_id_;
public:
Student(int sid, const string& name, int age); // 파생 클래스 생성자의 선언
};
파생 클래스의 객체 생성 순서
C++에서 파생 클래스의 객체가 생성되는 순서는 다음과 같습니다.
1. 파생 클래스의 객체를 생성하면, 프로그램은 제일 먼저 기초 클래스의 생성자를 호출합니다.
이때 기초 클래스 생성자는 상속받은 멤버 변수의 초기화를 진행합니다.
2. 그리고서 파생 클래스의 생성자가 호출됩니다.
3. 반대로 파생 클래스의 수명이 다하면, 먼저 파생 클래스의 소멸자가 호출되고, 그 후에 기초 클래스의 소멸자가 호출됩니다.
오버라이딩(overriding)
앞서 배운 함수 오버로딩(overloading)이란 서로 다른 시그니처를 갖는 여러 함수를 같은 이름으로 정의하는 것이었습니다.
함수 오버라이딩(overriding)이란 이미 정의된 함수를 무시하고, 같은 이름의 함수를 새롭게 정의하는 것이라고 할 수 있습니다.
멤버 함수 오버라이딩
C++에서 파생 클래스는 상속을 받을 때 명시한 접근 제어 권한에 맞는 기초 클래스의 모든 멤버를 상속받습니다.
이렇게 상속받은 멤버 함수는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용할 수도 있습니다.
오버라이딩이란 멤버 함수의 동작만을 재정의하는 것이므로, 함수의 원형은 기존 멤버 함수의 원형과 같아야 합니다.
C++에서는 다음과 같은 방법을 통해 멤버 함수를 오버라이딩할 수 있습니다.
1. 파생 클래스에서 직접 오버라이딩하는 방법
2. 가상 함수를 이용해 오버라이딩하는 방법
파생 클래스에서의 오버라이딩
C++에서는 파생 클래스에서 상속받은 기초 클래스의 멤버 함수를 직접 재정의할 수 있습니다.
예제
void Person::ShowPersonInfo()
{
cout << name_ << "의 나이는 " << age_ << "살입니다." << endl;
}
...
void Student::ShowPersonInfo()
{
cout << "이 학생의 학번은 " << student_id_ << "입니다." << endl;
}
위의 예제에서는 Person 클래스의 ShowPersonInfo() 멤버 함수를 Student 클래스에서 상속받아 재정의하고 있습니다.
그리고서 Student 객체에서 ShowPersonInfo() 멤버 함수를 호출하면 기초 클래스인 Person 클래스의 멤버 함수가 아닌 재정의한 멤버 함수가 호출됨을 알 수 있습니다.
또한, 범위 지정 연산자(::)를 사용하면 파생 클래스에서 기초 클래스의 원래 멤버 함수를 호출할 수도 있습니다.
예제
Student hong(123456789, "길동", 29);
hong.ShowPersonInfo();
hong.Person::ShowPersonInfo();
위의 예제에서 Student 객체는 자신이 재정의한 ShowPersonInfo() 함수뿐만 아니라 기초 클래스의 원래 함수도 호출하고 있습니다.
파생 클래스에서 오버라이딩의 문제점
위와 같이 파생 클래스에서 멤버 함수를 직접 재정의하는 방법은 일반적인 상황에서는 잘 동작합니다.
하지만 이 방법은 다음 예제처럼 포인터 변수를 사용할 때, 예상치 못한 결과를 반환할 수도 있습니다.
예제
Person* ptr_person;
Person lee("순신", 35);
Student hong(123456789, "길동", 29);
ptr_person = &lee;
ptr_person->ShowPersonInfo();
ptr_person = &hong;
ptr_person->ShowPersonInfo();
위의 예제는 먼저 Person 객체를 가리킬 수 있는 포인터 변수 ptr_person을 생성하여, Person 객체의 주소값을 대입합니다.
그 후 포인터 변수에 멤버 접근 연산자(->)를 사용하여 Person 객체의 ShowPersonInfo() 함수를 호출합니다.
그 다음에 Student 객체의 주소값을 대입하고, 이번에는 Student 객체의 ShowPersonInfo() 함수를 호출합니다.
하지만 결과는 두 번 모두 Person 객체의 ShowPersonInfo() 함수가 호출됩니다.
왜냐하면, C++ 컴파일러는 포인터 변수가 실제로 가리키는 객체의 타입을 기준으로 함수를 호출하는 것이 아니라, 해당 포인터의 타입을 기준으로 함수를 호출하기 때문입니다.
따라서 Person 객체를 가리킬 수 있는 포인터 변수로는 Person 객체의 멤버 함수만을 호출할 수 있습니다.
이러한 문제점을 해결하기 위해서 C++에서는 virtual 키워드를 사용한 가상 함수를 제공하고 있습니다.
예제
class Student : public Person
{
private:
int student_id_;
public:
Student(int sid, const string& name, int age); // 파생 클래스 생성자의 선언
virtual void ShowPersonInfo(); // 파생 클래스에서 상속받은 멤버 함수의 재정의
};
위의 예제는 앞선 예제에서 사용한 ShowPersonInfo() 함수를 가상 함수로 선언한 예제입니다.
이렇게 멤버 함수를 가상 함수로 선언하면 포인터가 실제로 가리키는 객체에 따라 호출하는 대상을 바꿀 수 있게 됩니다.
다중 상속(multiple inheritance)
다중 상속(multiple inheritance)이란 두 개 이상의 클래스로부터 멤버를 상속받아 파생 클래스를 생성하는 것을 의미합니다.
C++에서는 쉼표(,)를 사용하여 상속받을 여러 개의 기초 클래스를 명시하는 것으로 간단히 다중 상속을 구현할 수 있습니다.
문법
class 파생클래스이름 : 접근제어지시자 기초클래스이름, 접근제어지시자 기초클래스이름[, 접근제어지시자 기초클래스이름, ...]
{
// 파생 클래스 멤버 리스트
}
다중 상속의 문제점
다중 상속은 여러 개의 기초 클래스가 가진 멤버를 모두 상속받을 수 있다는 점에서 매우 강력한 상속 방법입니다.
하지만 이러한 다중 상속은 단일 상속에는 없는 여러 가지 새로운 문제를 발생시킵니다.
1. 상속받은 여러 기초 클래스에 같은 이름의 멤버가 존재할 가능성이 있습니다.
2. 하나의 클래스를 간접적으로 두 번 이상 상속받을 가능성이 있습니다.
3. 가상 클래스가 아닌 기초 클래스를 다중 상속하면, 기초 클래스 타입의 포인터로 파생 클래스를 가리킬 수 없습니다.
위와 같이 다중 상속은 프로그래밍을 복잡하게 만들 수 있으며, 그에 비해 실용성은 그다지 높지 않습니다.
반드시 다중 상속을 사용해야만 풀 수 있는 문제란 거의 없으므로, 될 수 있으면 사용을 자제하는 것이 좋습니다.
'C++' 카테고리의 다른 글
C++ 템플릿 (0) | 2020.07.05 |
---|---|
C++ OOP다형성 (0) | 2020.07.05 |
C++ OOP캡슐화 (0) | 2020.07.05 |
C++ 연산자 오버로딩 (0) | 2020.07.05 |
C++ 생성자 & 소멸자 (0) | 2020.07.05 |
댓글