15. OOP 상속성(OOP Inheritance)
1. 키워드
- OOP 상속성(OOP Inheritance)
- 클래스 상속(Class Inheritance)
- 기초 클래스(Base Class), 부모 클래스(Parent Class), 상위 클래스(Super Class)
- 파생 클래스(Derived Class), 자식 클래스(Child Class), 하위 클래스(Sub Class)
- 오버라이딩(Overriding)
- 가상 함수(Virtual Function)와
virtual
키워드 - 단일 상속(Single Inheritance)과 다중 상속(Multiple Inheritance)
2. 파생 클래스
1) 상속
- 상속은 추상화, 캡슐화와 더불어 OOP를 구성하는 중요한 특징 중 하나이다.
- 상속은 사용자에게 높은 수준의 코드 재활용성을 제공하며, 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련한다.
2) 클래스 상속
- C++에서 클래스 상속이란 기존에 정의되어 있는 클래스의 모든 멤버 변수와 멤버 함수를 물려받아, 새로운 클래스를 작성하는 것을 의미한다.
- 이때 기존에 정의되어 있던 클래스를 기초 클래스 또는 부모 클래스, 상위 클래스라고도 한다.
- 그리고 상속을 통해 새롭게 작성되는 클래스를 파생 클래스 또는 자식 클래스, 하위 클래스라고도 한다.
- 이와 같은 클래스의 상속은 다음과 같은 장점을 가진다.
1] 기존에 작성된 클래스를 재활용할 수 있다.
2] 공통적인 부분은 기초 클래스에 미리 작성하여, 파생 클래스에서 중복되는 부분을 제거할 수 있다.
3) 파생 클래스
- 파생 클래스란 기초 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 가리킨다.
- C++에서 파생 클래스는 다음과 같은 문법을 통해 선언한다.
- 접근 제어 지시자는 기초 클래스의 멤버를 사용할 수 있는 파생 클래스의 접근 제어 권한을 설정한다.
- 이때 접근 제어 지시자를 생략하면, 파생 클래스의 접근 제어 권한은
private
으로 기본 설정된다. - 또한
,
(콤마)를 사용하여 상속 받을 기초 클래스를 여러 개 명시할 수 있다. - 이때 파생 클래스가 상속 받는 기초 클래스가 하나이면 단일 상속이라고 한다.
- 만약 여러 개의 기초 클래스를 상속 받으면 다중 상속이라고 한다.
4) 파생 클래스의 특징
- C++에서 파생 클래스는 다음과 같은 특징을 가지고 있다.
1] 파생 클래스는 반드시 자신만의 생성자를 작성해야 한다.
2] 파생 클래스에는 기초 클래스의 접근할 수 있는 모든 멤버 변수들이 저장된다.
3] 파생 클래스는 기초 클래스의 접근할 수 있는 모든 멤버 함수를 사용할 수 있다.
4] 파생 클래스에는 필요한 만큼 멤버를 추가할 수 있다.
- 우선 다음과 같은
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;
}
void Person::ShowPersonInfo() {
cout << name_ << "의 나이는 "
<< age_ << "살입니다." << endl;
}
- 다음은 기존에 작성된
Person
클래스를 상속받는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
멤버를 초기화할 수 있다. - 이때 기초 클래스의 생성자를 명시하지 않으면, 프로그램은 기초 클래스의 디폴트 생성자를 사용하게 된다.
#include <iostream>
#include <string>
using namespace std;
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); // 파생 클래스 생성자의 선언
};
int main(void) {
Student hong = Student(123456789, "길동", 29);
hong.ShowPersonInfo();
return 0;
}
Person::Person(const string &name, int age) { // 기초 클래스 생성자의 정의
name_ = name;
age_ = age;
}
void Person::ShowPersonInfo() {
cout << name_ << "의 나이는 "
<< age_ << "살입니다." << endl;
}
Student::Student(int sid, const string &name, int age) : Person(name, age) { // 파생 클래스 생성자의 선언
student_id_ = sid;
}
// 길동의 나이는 29살입니다.
- 만약 다음과 같이 기초 클래스에 대한 접근 제어 지시자가
private
인 경우, 파생 클래스에서는 기초 클래스의 어떠한 멤버에도 접근할 수 없다.
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string name_;
int age_;
public:
Person(const string &name, int age); // 기초 클래스 생성자의 선언
int test_person_ = 10;
void ShowPersonInfo();
};
class Student : private Person {
private:
int student_id_;
public:
Student(int sid, const string &name, int age); // 파생 클래스 생성자의 선언
int test_student_ = 20;
};
int main(void) {
Student hong = Student(123456789, "길동", 29);
// 기초 클래스의 private 영역이므로 접근 불가
// cout << hong.name_ << endl;
// 기초 클래스의 public 영역이어도 접근 불가
// cout << hong.test_person_ << endl;
// 파생 클래스의 private 영역이므로 접근 불가
// cout << hong.student_id_ << endl;
// 파생 클래스의 public 영역이므로 접근 가능
cout << hong.test_student_ << endl;
return 0;
}
Person::Person(const string &name, int age) { // 기초 클래스 생성자의 정의
name_ = name;
age_ = age;
}
void Person::ShowPersonInfo() {
cout << name_ << "의 나이는 "
<< age_ << "살입니다." << endl;
}
Student::Student(int sid, const string &name, int age) : Person(name, age) { // 파생 클래스 생성자의 선언
student_id_ = sid;
}
// 20
5) 파생 클래스의 객체 생성 순서
- 다음 그림은 파생 클래스의 생성자가 호출되면 수행되는 동작을 설명하고 있다.
- C++에서 파생 클래스의 객체가 생성되는 순서는 다음과 같다.
1] 파생 클래스의 객체를 생성하면, 프로그램은 제일 먼저 기초 클래스의 생성자를 호출한다.
2] 이때 기초 클래스 생성자는 상속 받은 멤버 변수의 초기화를 진행한다.
3] 그리고서 파생 클래스의 생성자가 호출된다.
4] 반대로 파생 클래스의 수명이 다하면, 먼저 파생 클래스의 소멸자가 호출되고, 그 후에 기초 클래스의 소멸자가 호출된다.
3. 멤버 함수 오버라이딩
1) 오버라이딩
- 함수 오버로딩(Overloading)이란 서로 다른 시그니처를 가지는 여러 함수를 같은 이름으로 정의하는 것이었다.
- 함수 오버라이딩이란 이미 정의된 함수를 무시하고, 같은 이름의 함수를 새롭게 정의하는 것이라고 할 수 있다.
2) 멤버 함수 오버라이딩
- C++에서 파생 클래스는 상속을 받을 때 명시한 접근 제어 권한에 맞는 기초 클래스의 모든 멤버를 상속 받는다.
- 이렇게 상속 받은 멤버 함수는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용할 수도 있다.
- 오버라이딩이란 멤버 함수의 동작만 재정의하는 것이므로, 함수의 원형은 기존 멤버 함수의 원형과 같아야 한다.
- C++에서는 다음과 같은 방법을 통해 멤버 함수를 오버라이딩할 수 있다.
1] 파생 클래스에서 직접 오버라이딩하는 방법
2] 가상 함수를 이용해 오버라이딩하는 방법
3) 파생 클래스에서의 오버라이딩
- C++에서는 파생 클래스에서 상속 받은 기초 클래스의 멤버 함수를 직접 재정의할 수 있다.
#include <iostream>
#include <string>
using namespace std;
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); // 파생 클래스 생성자의 선언
void ShowPersonInfo(); // 파생 클래스에서 상속 받은 멤버 함수의 재정의
};
int main(void) {
Person lee = Person("순신", 35);
lee.ShowPersonInfo();
Student hong = Student(123456789, "길동", 29);
hong.ShowPersonInfo();
return 0;
}
Person::Person(const string &name, int age) { // 기초 클래스 생성자의 정의
name_ = name;
age_ = age;
}
void Person::ShowPersonInfo() {
cout << name_ << "의 나이는 "
<< age_ << "살입니다." << endl;
}
Student::Student(int sid, const string &name, int age) : Person(name, age) { // 파생 클래스 생성자의 선언
student_id_ = sid;
}
void Student::ShowPersonInfo() {
cout << "이 학생의 학번은 "
<< student_id_ << "입니다." << endl;
}
// 순신의 나이는 35살입니다.
// 이 학생의 학번은 123456789입니다.
- 위의 코드에서는
Person
클래스의ShowPersonInfo()
멤버 함수를Student
클래스에서 상속 받아 재정의하고 있다. - 그리고서
Student
객체에서ShowPersonInfo()
멤버 함수를 호출하면 기초 클래스인Person
클래스의 멤버 함수가 아닌 재정의한 멤버 함수가 호출됨을 알 수 있다.
- 또한,
::
(범위 지정 연산자)를 사용하면 파생 클래스에서 기초 클래스의 원래 멤버 함수를 호출할 수도 있다.
Student hong = Student(123456789, "길동", 29);
hong.ShowPersonInfo();
hong.Person::ShowPersonInfo();
// 이 학생의 학번은 123456789입니다.
// 길동의 나이는 29살입니다.
- 위의 코드에서
Student
객체는 자신이 재정의한ShowPersonInfo()
함수뿐만 아니라 기초 클래스의 원래 함수도 호출하고 있다.
4) 파생 클래스에서 오버라이딩의 문제점
- 위와 같이 파생 클래스에서 멤버 함수를 직접 재정의하는 방법은 일반적인 상황에서는 잘 동작한다.
- 하지만 이 방법은 다음 코드처럼 포인터 변수를 사용할 때, 예상하지 못한 결과를 반환할 수도 있다.
#include <iostream>
#include <string>
using namespace std;
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); // 파생 클래스 생성자의 선언
void ShowPersonInfo(); // 파생 클래스에서 상속 받은 멤버 함수의 재정의
};
int main(void) {
Person *ptr_person; // Person 타입을 기준으로 함수를 호출함
Person lee = Person("순신", 35);
Student hong = Student(123456789, "길동", 29);
ptr_person = &lee;
ptr_person->ShowPersonInfo();
ptr_person = &hong;
ptr_person->ShowPersonInfo();
return 0;
}
Person::Person(const string &name, int age) { // 기초 클래스 생성자의 정의
name_ = name;
age_ = age;
}
void Person::ShowPersonInfo() {
cout << name_ << "의 나이는 "
<< age_ << "살입니다." << endl;
}
Student::Student(int sid, const string &name, int age) : Person(name, age) { // 파생 클래스 생성자의 선언
student_id_ = sid;
}
void Student::ShowPersonInfo() {
cout << "이 학생의 학번은 "
<< student_id_ << "입니다." << endl;
}
// 순신의 나이는 35살입니다.
// 길동의 나이는 29살입니다.
- 위의 코드는 먼저
Person
객체를 가리킬 수 있는 포인터 변수ptr_person
을 생성하여,Person
객체의 주소값을 대입한다. - 그 후 포인터 변수에
->
(멤버 접근 연산자)를 사용하여Person
객체의ShowPersonInfo()
함수를 호출한다. - 그 다음에
Student
객체의 주소값을 대입하고, 이번에는Student
객체의ShowPersonInfo()
함수를 호출한다.
- 하지만 결과는 두 번 모두
Person
객체의ShowPersonInfo()
함수가 호출된다. - 왜냐하면, C++ 컴파일러는 포인터 변수가 실제로 가리키는 객체의 타입을 기준으로 함수를 호출하는 것이 아니라, 해당 포인터의 타입을 기준으로 함수를 호출하기 때문이다.
- 따라서
Person
객체를 가리킬 수 있는 포인터 변수로는Person
객체의 멤버 함수만을 호출할 수 있다.
- 이러한 문제점을 해결하기 위해서 C++에서는
virtual
키워드를 사용한 가상 함수를 제공하고 있다.
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string name_;
int age_;
public:
Person(const string &name, int age); // 기초 클래스 생성자의 선언
virtual void ShowPersonInfo();
};
class Student : public Person {
private:
int student_id_;
public:
Student(int sid, const string &name, int age); // 파생 클래스 생성자의 선언
virtual void ShowPersonInfo(); // 파생 클래스에서 상속 받은 멤버 함수의 재정의
};
int main(void) {
Person *ptr_person; // Person 타입을 기준으로 함수를 호출함 -> 가상 함수 이용
Person lee = Person("순신", 35);
Student hong = Student(123456789, "길동", 29);
ptr_person = &lee;
ptr_person->ShowPersonInfo();
ptr_person = &hong;
ptr_person->ShowPersonInfo();
return 0;
}
Person::Person(const string &name, int age) { // 기초 클래스 생성자의 정의
name_ = name;
age_ = age;
}
void Person::ShowPersonInfo() {
cout << name_ << "의 나이는 "
<< age_ << "살입니다." << endl;
}
Student::Student(int sid, const string &name, int age) : Person(name, age) { // 파생 클래스 생성자의 선언
student_id_ = sid;
}
void Student::ShowPersonInfo() {
cout << "이 학생의 학번은 "
<< student_id_ << "입니다." << endl;
}
// 순신의 나이는 35살입니다.
// 이 학생의 학번은 123456789입니다.
- 위의 코드는 앞선 코드에서 사용한
ShowPersonInfo()
함수를 가상 함수로 선언한 것이다. - 이렇게 멤버 함수를 가상 함수로 선언하면 포인터가 실제로 가리키는 객체에 따라 호출하는 대상을 바꿀 수 있게 된다.
4. 다중 상속
- 다중 상속이란 두 개 이상의 클래스로부터 멤버를 상속 받아 파생 클래스를 생성하는 것을 의미한다.
- C++에서는
,
를 사용하여 상속 받을 여러 개의 기초 클래스를 명시하는 것으로 간단히 다중 상속을 구현할 수 있다.
1) 다중 상속의 문제점
- 다중 상속은 여러 개의 기초 클래스가 가진 멤버를 모두 상속 받을 수 있다는 점에서 매우 강력한 상속 방법이다.
- 하지만 이러한 다중 상속은 단일 상속에는 없는 여러 가지 새로운 문제를 발생시킨다.
1] 상속 받은 여러 기초 클래스에 같은 이름의 멤버가 존재할 가능성이 있다.
2] 하나의 클래스를 간접적으로 두 번 이상 상속 받을 가능성이 있다.
3] 가상 클래스가 아닌 기초 클래스를 다중 상속하면, 기초 클래스 타입의 포인터로 파생 클래스를 가리킬 수 없다.
- 위와 같이 다중 상속은 프로그래밍을 복잡하게 만들 수 있으며, 그에 비해 실용성은 그다지 높지 않다.
- 반드시 다중 상속을 사용해야만 풀 수 있는 문제란 거의 없으므로, 될 수 있으면 사용을 자제하는 것이 좋다.