상속 (inheritance)
상위 클래스의 내용(속성, 메소드)을 하위 클래스가 그대로 물려받는 것
- 상위 클래스(super class) == 부모 클래스(parent class) == 기초 클래스(base class)
- 하위 클래스(sub class) == 자식 클래스(child class) == 파생 클래스(derived class)
상속 구현하기
- 상속 선언 문법은 다음과 같다.
// 단일 상속
class 파생클래스: 접근제어지시자 기초클래스{
// 파생클래스 멤버
}
// 다중 상속
class 파생클래스: 접근제어지시자 기초클래스, 접근제어지시자 기초클래스{
// 파생클래스 멤버
}
// EX
class Derived : public Base {
// members of Derived class
}
- 접근제어지시자가 public인 경우, 상위클래스의 멤버들의 접근제어지시자 그대로 파생클래스로 상속된다.
- 접근제어지시자가 protected인 경우, 상위클래스의 public 멤버들은 protected 상태로 바뀌어 파생클래스로 상속된다.
- 접근제어지시자가 private인 경우, 상위클래스의 모든 멤버들은 private한 상태로 바뀌어 파생클래스로 상속된다.
- 다중 상속은 어떤 경우에는 위력적인 성능을 보이지만, 여러 새로운 문제를 발생시킬 가능성이 높다. 따라서 될 수 있으면 사용을 자제하는 것이 좋다. (아래 링크의 하단부를 참고)
상속 구현
#include <iostream>
#include <string>
using namespace std;
class Base {
private:
string str;
public:
Base() : str("기초클래스") { cout << "기초 클래스의 생성자가 호출됨" << endl; }
void printStr() { cout << str << endl; }
};
class Derived : public Base{
private:
string str;
public:
Derived() : Base(), str("파생클래스") {
cout << "파생 클래스의 생성자가 호출됨" << endl;
printStr(); // 생성자 호출하면서 함수도 호출
}
};
int main() {
cout << "===== 기초 클래스 생성 =====" << endl;
Base b;
cout << "===== 파생 클래스 생성 =====" << endl;
Derived d;
return 0;
}
===== 기초 클래스 생성 =====
기초 클래스의 생성자가 호출됨
===== 파생 클래스 생성 =====
기초 클래스의 생성자가 호출됨
파생 클래스의 생성자가 호출됨
기초클래스
- Derived 클래스는 Base 클래스를 상속받는다.
- Derived 클래스의 생성자 부분을 보면, Derived 클래스는 printStr() 함수를 Base 클래스에서 물려받아 호출한다.
- Base 클래스에서 printStr() 함수를 그대로 상속받았기 때문에 Derived 클래스의 str이 아닌 Base 클래스의 str이 출력되었다.
protected
- 파생 클래스는 기초 클래스의 private 멤버에 접근할 수 없다.
- 다행히도 C++은 private와 public의 중간 정도의 성격을 갖는 protected 접근 지시자를 지원한다. 이를 사용하여 멤버를 선언하면 파생 클래스에서도 기초 클래스의 멤버에 접근할 수 있다.
파생클래스로 기초클래스의 private 멤버에 접근하기
#include <iostream>
#include <string>
using namespace std;
class Base {
private:
string baseStr;
public:
Base() : baseStr("기초클래스") { cout << "기초 클래스의 생성자가 호출됨" << endl; }
void printStr() { cout << baseStr << endl; }
};
class Derived : public Base{
private:
string derivedStr;
public:
Derived() : Base(), derivedStr("파생클래스") {
cout << "파생 클래스의 생성자가 호출됨" << endl;
// 기초 클래스의 private 멤버에 접근하기
cout << baseStr << endl;
}
};
int main() {
cout << "===== 기초 클래스 생성 =====" << endl;
Base b;
cout << "===== 파생 클래스 생성 =====" << endl;
Derived d;
return 0;
}
- 위 오류처럼 파생 클래스는 기초 클래스의 private 멤버에 접근할 수 없다.
파생클래스로 기초클래스의 protected 멤버에 접근하기
#include <iostream>
#include <string>
using namespace std;
class Base {
protected: // protected로 선언
string baseStr;
public:
Base() : baseStr("기초클래스") { cout << "기초 클래스의 생성자가 호출됨" << endl; }
void printStr() { cout << baseStr << endl; }
};
class Derived : public Base{
private:
string derivedStr;
public:
Derived() : Base(), derivedStr("파생클래스") {
cout << "파생 클래스의 생성자가 호출됨" << endl;
// 기초 클래스의 private 멤버에 접근하기
cout << baseStr << endl;
}
};
int main() {
cout << "===== 기초 클래스 생성 =====" << endl;
Base b;
cout << "===== 파생 클래스 생성 =====" << endl;
Derived d;
return 0;
}
===== 기초 클래스 생성 =====
기초 클래스의 생성자가 호출됨
===== 파생 클래스 생성 =====
기초 클래스의 생성자가 호출됨
파생 클래스의 생성자가 호출됨
기초클래스
- protected 키워드를 이용하면 파생클래스에서도 기초클래스 멤버에 접근할 수 있다.
오버라이딩
- 오버라이딩(Overriding)은 같은 이름의 함수를 새롭게 정의하는 것이다.
- 파생 클래스는 상속받은 함수를 그대로 사용해도 되고, 다시 이를 재정의하여 사용할 수 있다.
- 멤버 함수를 오버라이딩 하는 방법 2가지
- 1. 파생 클래스에서 직접 오버라이딩
- 2. 가상 함수를 이용하여 오버라이딩
파생 클래스에서 오버라이딩하기
class Derived : public Base{
private:
string str;
public:
Derived() : Base(), str("파생클래스") {
cout << "파생 클래스의 생성자가 호출됨" << endl;
printStr(); // 생성자 호출하면서 함수도 호출
}
void printStr() { cout << str << endl; } // 파생 클래스에서 재정의
};
- 위의 예제를 다시 이용하여 위처럼 파생 클래스에서 함수를 재정의한다.
- 이렇게 파생 클래스에서 함수를 다시 정의하면, 컴파일러는 기초 클래스의 멤버 함수 대신 파생 클래스의 멤버 함수를 호출한다. 결과는 다음과 같다.
===== 기초 클래스 생성 =====
기초 클래스의 생성자가 호출됨
===== 파생 클래스 생성 =====
기초 클래스의 생성자가 호출됨
파생 클래스의 생성자가 호출됨
파생클래스
파생 클래스 오버라이딩의 문제점
#include <iostream>
#include <string>
using namespace std;
class Base {
private:
string str;
public:
Base(const string& _str) { str = _str; }
void printStr() { cout << "기초클래스: " << str << endl; }
};
class Derived : public Base {
private:
int num;
public:
Derived(const string& _str, int _num) : Base(_str) { num = _num; }
void printStr() { cout << "파생클래스: " << num << endl; } // str 대신 num 출력하도록 오버라이딩
};
int main() {
Base* ptr;
Base a("base class");
Derived b("derived class", 10);
ptr = &a;
ptr->printStr();
ptr = &b; // Derived는 Base를 상속받기 때문에 is-a 관계. 따라서 Base 객체 포인터는 Derived 객체를 가리킬 수 있음
ptr->printStr(); // 이를 업 캐스팅(파생 클래스에서 기반 클래스로 캐스팅하는 것)이라한다.
return 0;
}
기초클래스: base class
기초클래스: derived class
- 파생 클래스에서 직접 오버라이딩할 때의 문제점은 포인터를 사용할 때 나타난다.
- 위 코드에서 ptr은 Base 클래스 타입의 포인터로 정의되어 있다.
- ptr이 가리키는 객체를 변경하여도 컴파일러는 포인터 ptr이 가리키는 변수의 타입을 기준으로 함수를 호출한다.
- 그렇기 때문에 파생 클래스에서 정의된 함수가 아닌, 기초 클래스의 멤버 함수가 호출되었다.
- 이러한 문제점을 해결하기 위해 C++에서는 virtual 키워드를 제공한다.
가상 함수를 이용하여 오버라이딩
#include <iostream>
#include <string>
using namespace std;
class Base {
private:
string str;
public:
Base(const string& _str) { str = _str; }
virtual void printStr() { cout << "기초클래스: " << str << endl; }
};
class Derived : public Base {
private:
int num;
public:
Derived(const string& _str, int _num) : Base(_str) { num = _num; }
virtual void printStr() { cout << "파생클래스: " << num << endl; }
// 기초 클래스에서만 virtual 키워드를 사용해도 되지만, 이왕이면 파생 클래스에서도
// virtual 키워드를 사용해서 해당 함수가 가상함수임을 명확하게 드러내는 편이 좋다.
};
int main() {
Base* ptr;
Base a("base class");
Derived b("derived class", 10);
ptr = &a;
ptr->printStr();
ptr = &b;
ptr->printStr();
return 0;
}
기초클래스: base class
파생클래스: 10
- virtual을 함수 원형 앞에 붙이기만 하면 된다.
- virtual 키워드를 통해 가상함수로 선언하면, 컴퓨터는 포인터가 실제로 가리키는 객체에 따라 멤버함수를 호출한다.
- 어떤 함수가 실행될지 컴파일러는 알지 못하고(컴파일 때는 모르고), 런타임 때 포인터가 가리키는 객체를 확인하여 알맞은 함수를 호출한다. 컴파일 때가 아닌 런타임 때 동작이 정해지는 것을 동적 바인딩(dynamic binding)이라고 한다.
- 가상함수를 생성하면 가상 함수 테이블(virtual function table, vtbl)가 같이 작성된다. C++ 프로그램은 가상함수가 호출되면 가상 함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아서 호출한다. 이때 약간의 오버헤드(메모리, 실행 속도 측면)가 발생하므로, C++에서는 virtual 함수를 Default로 두지는 않는다. (Java는 모든 함수가 virtual)
override 키워드
void printStr() override { cout << "파생클래스: " << num << endl; }
- C++11부터는 파생클래스에 override 키워드를 명시하여 이것이 오버라이딩되는 함수임을 나타낼 수 있다.
- override 키워드를 사용하면, 실수로 오버라이딩을 하지 않는 경우를 막을 수 있다.
레퍼런스로 접근하기
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { cout << "기초클래스" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "파생클래스" << endl; }
};
void test(Base& a) { a.show(); } // 레퍼런스 인자
int main() {
Base a;
Derived b;
test(a);
test(b);
return 0;
}
기초클래스
파생클래스
- 레퍼런스를 인자로 받는 함수로도 오버라이딩된 함수를 호출할 수 있다.
- 레퍼런스 인자의 타입이 Base이지만, virtual로 정의되어 있으므로 Derived 객체의 함수가 호출된다.