c++에서 상속을 도입한 이유를 생각해보자. 단순히 똑같은 코드를 작성하는 것을 방지하는게 아닌 객체지향프로그래밍에서 추구하는 실제 객체의 추상화를 좀 더 효과적으로 하기 위함이다.
c언어에서는 구조체 사이의 관계를 표현할 수 있는 방법이 없었다. 하지만 c++에서 상속이란 것을 도입함으로써, 클래스 사이 관계를 표현할 수 있게 되었다.
Manager가 Employee를 상속한다의 의미를 살펴보자.
Manager
클래스는 Employee
의 모든 기능을 포함함Manager
클래스는 Employee
의 기능을 모두 수행할 수 있기 때문에 Manager
를 Employee
라 칭해도 무방함Manager
는 Employee
라 할 수 있음위 내용을 Manager is a Employee!로 표현 가능하다.
결국 상속을 통해 클래스를 구체화 가능하다.
모든 클래스들의 관계를 is-a로 표현할 순 없다. 자동차 클래스를 생각했을 때 자동차를 구성하는 엔진 클래스, 브레이크 클래스, 오디오 클래스 등 수많은 클래스가 있다. 그렇다고 이들 사이의 관계를 is-a관계로 표현 불가능하다. 대신, 이들 사이는 has-a관계로 쉽게 표현 가능하다.
#include <iostream>
#include <string>
class Base
{
std::string s;
public:
Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }
void what() { std::cout << s << std::endl; }
};
class Derived : public Base
{
std::string s;
public:
Derived() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }
void what() { std::cout << s << std::endl; }
};
int main() {
Base p;
Derived c;
std::cout << "=== 포인터 버전 ===" << std::endl;
Base* p_c = &c;
p_c->what();
return 0;
}
위 코드에서 Base* p_c = &c;
를 보면 Derived
의 객체를 Base
객체를 가리키는 포인터에 넣었다. 이는 Derived is a Base이기 때문에 가능하다. (단지 Base 객체만 가리키는 포인터이므로 Derived 데이터가 소실되지 않는다.) 이러한 형태의 캐스팅을 업 캐스팅이라고 부른다.
#include <iostream>
#include <string>
class Base {
std::string s;
public:
Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }
void what() { std::cout << s << std::endl; }
};
class Derived : public Base {
std::string s;
public:
Derived() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }
void what() { std::cout << s << std::endl; }
};
int main() {
Base p;
Derived c;
std::cout << "=== 포인터 버전 ===" << std::endl;
Derived* p_p = &p;
p_p->what();
return 0;
}
위 코드 Derived* p_p = &p
를 보면 Base
의 객체인 p를 Derived
객체를 가리키는 포인터에 넣었다. 이 코드를 실행해보면 오류가 발생한다. 그 이유는 Derived
포인터가 Base
객체를 가리킬때 p_p->what();
는 Derived
의 what
함수가 실행된다. 하지만 객체는 Base
이기 때문에 해당 정보가 없어 문제가 발생한다.
따라서 함부로 다운 캐스팅 하는 것은 금지되어 있다.
#include <iostream>
#include <string>
class Base {
std::string s;
public:
Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }
void what() { std::cout << s << std::endl; }
};
class Derived : public Base {
std::string s;
public:
Derived() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }
void what() { std::cout << s << std::endl; }
};
int main() {
Base p;
Derived c;
std::cout << "=== 포인터 버전 ===" << std::endl;
Base* p_p = &c;
Derived* p_c = p_p;
p_c->what();
return 0;
}
위 코드를 실행하면 오류가 발생한다. 하지만 생각해보면 Derived
객체를 업캐스팅 한것이기 때문에 데이터가 소실되지 않아 다운캐스팅이 가능해야 한다. 하지만 다운캐스팅을 금지하고 있기 때문에 static_cast을 사용해 강제적으로 타입 변환을 하면 된다.(다운 캐스팅이 보장되지 않더라도 강제 캐스팅을 하면 컴파일러가 오류를 찾아내기 힘드니 꼭 확인하고 사용하자.)
위에서 말했던 문제를 해결하기 위해 dynamic_cast
를 사용할 수 있다. 이를 사용하면 컴파일 타임에 오류를 찾아낼 수 있다.
아래 소스 코드를 실행하게 되면 p_c
와p_p
모두 Base
객체를 가리키는 포인터이기 때문에 what()
을 호출하면 Base
객체의 what()
함수가 실행된다.
#include <iostream>
class Base {
public:
Base() { std::cout << "기반 클래스" << std::endl; }
void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};
class Derived : public Base {
public:
Derived() : Base() { std::cout << "파생 클래스" << std::endl; }
void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
int main() {
Base p;
Derived c;
Base* p_c = &c;
Base* p_p = &p;
std::cout << " == 실제 객체는 Base == " << std::endl;
p_p->what();
std::cout << " == 실제 객체는 Derived == " << std::endl;
p_c->what();
return 0;
}
이를 해결하기위해 virtual
키워드를 사용할 수 있다. virtual
키워드를 사용하게 되면 런타임시 컴퓨터가 함수를 호출하는 포인터가 어떤 클래스의 객체인지 확인 후 해당 객체의 함수를 실행하게 된다. 이렇게 런타임 시에 정해지는 일을 가리켜 동적 바인딩이라고 부른다.
virtual
키워드가 붙은 함수를 가상함수(virtual function)이라고 부르며 이렇게 파생 클래스의 함수가 기반 클래스의 함수를 오버라이드 하기 위해서는 두 함수의 꼴이 정확히 같아야 한다.
#include <iostream>
class Base {
public:
Base() { std::cout << "기반 클래스" << std::endl; }
virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};
class Derived : public Base {
public:
Derived() : Base() { std::cout << "파생 클래스" << std::endl; }
void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
int main() {
Base p;
Derived c;
Base* p_c = &c;
Base* p_p = &p;
std::cout << " == 실제 객체는 Base == " << std::endl;
p_p->what();
std::cout << " == 실제 객체는 Derived == " << std::endl;
p_c->what();
return 0;
}
C++11에서는 파생 클래스에서 기반 클래스의 가상 함수를 오버라이드 하는 경우, override
키워드를 통해 명시적으로 나타낼 수 있다. 해당 키워드를 사용하면 실수로 오버라이드 하지 않는 경우를 방지할 수 있다.
#include <iostream>
#include <string>
class Base {
std::string s;
public:
Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }
virtual void what() { std::cout << s << std::endl; }
};
class Derived : public Base {
std::string s;
public:
Derived() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }
void what() override { std::cout << s << std::endl; }
};