14) 클래스 (5)
이번 시간에는 OOP의 중요한 성질인 다형성에 대해서 알아보도록 하겠습니다.
14-1. 다형성
C++에서의 다형성은, 객체의 타입에 따라서, 그 멤버함수가 다른 기능을 하는 것을 말합니다.
지난 시간에 파생 클래스에 대해서 잠깐 소개하면서, 부모 클래스의 메서드를 파생 클래스에서 변경할 수 있는 메서드 오버라이딩(Overriding)에 대해서 알아보았습니다.
ex) 생성자와 메서드를 오버라이딩
class Shape {
private:
std::string name_;
public:
Shape(std::string name){
name_ = name;
}
void Introduce(){
std::cout << "이 객체의 이름은 " << name_ << std::endl;
}
};
class Rectangular: public Shape{
private:
int height_, width_;
public:
Rectangular(std::string name, int height, int width);
void Introduce(){
std::cout << "가로 " << height_ << ", 세로 " << width_ << "인 직사각형" << std::endl;
}
int CalcArea(){
return height_ * width_;
}
};
Rectangular::Rectangular(std::string name, int height, int width): Shape::Shape(name) {
height_ = height;
width_ = width;
};
14-2. 위 방법의 문제점
하지만, 이런 방식으로 메서드 오버라이딩을 하면 문제가 생깁니다.
int main(){
Shape rec = Rectangular("직사각형", 15, 10);
rec.Introduce(); // 이 객체의 이름은 직사각형
Shape *rec_ptr = new Rectangular("직사각형", 15, 10);
rec_ptr -> Introduce(); // 이 객체의 이름은 직사각형
}
기본적으로 파생 클래스는 부모 클래스에 포함되는 범주이기 때문에,
다음과 같이 객체를 Shape 타입으로 선언할 수는 있지만, 메서드는 Shape의 것을 따르게 됩니다.
왜냐하면 객체와 메서드간의 static linkage를 가지기 때문입니다.
(static linkage: 프로그램이 실행되기 전에는 function call이 고정됨, 객체의 타입이 동적으로 변해도 처음의 링크를 따름)
그렇기 때문에 실제로 메서드 오버라이딩이 안 되는 것 뿐만 아니라, 파생 클래스에서 추가된 메서드도 사용할 수 없습니다.
ex) 실험: CalcArea 메서드를 추가해서 사용해보기
class Rectangular: public Shape{
private:
int height_, width_;
public:
Rectangular(std::string name, int height, int width);
void Introduce(){
std::cout << "가로 " << height_ << ", 세로 " << width_ << "인 직사각형" << std::endl;
}
int CalcArea(){
return height_ * width_;
}
};
int main(){
Shape *class_ptr = new Rectangular("직사각형", 15, 10);
class_ptr -> Introduce();
std::cout << class_ptr -> CalcArea();
}
그러면 Shape 클래스는 CalcArea라는 멤버가 없다는 에러가 발생합니다.
prog.cc: In function 'int main()':
prog.cc:39:28: error: 'class Shape' has no member named 'CalcArea'
39 | std::cout << class_ptr -> CalcArea();
| ^~~~~~~~
status: 1
이는 객체가 중간에 변경되어도 처음에 선언한 타입(부모 클래스) 그대로 인식되기 때문인데요,
메서드 오버라이딩이 잘 작동하려면 객체의 타입을 동적으로 인식해서 메서드가 바뀌어야 합니다.
14-3. Virtual 키워드
객체의 타입을 동적으로 인식하기 위해서 virtual이라는 키워드를 사용할 수 있습니다.
virtual void Introduce(){
std::cout << "이 객체의 이름은 " << name_ << std::endl;
}
사용법은 간단합니다. 부모 클래스의 메서드에 virtual 키워드를 사용하면 됩니다.
int main(){
Shape rec = Rectangular("직사각형", 15, 10);
rec.Introduce(); // 이 객체의 이름은 직사각형
Shape *class_ptr = new Rectangular("직사각형", 15, 10);
class_ptr -> Introduce(); // 가로 15, 세로 10인 직사각형
}
특이한 점은, 포인터를 통해서 동적으로 할당된 객체에 대해서만 적용이 되었는데요.
왜냐하면 virtual 키워드는 컴파일러가 pointer의 타입 대신에, pointer가 가리키는 컨텐츠를 파악하도록 만들기 때문입니다.
그렇기 때문에 애초에 pointer에 대해서만 적용할 수 있는 해법입니다.
14-4. Pure virtual functions
그런데, 메서드 오버라이딩은 부모 클래스의 멤버함수에 virtual 키워드를 사용할 수 있겠지만,
애초에 CalcArea()
같은 함수는 부모 클래스에 존재하지 않는데, 이것을 사용하고 싶을 때는 어떻게 해야 할까요?
정답은, 부모 클래스에 가짜 virtual 멤버함수를 만드는 것입니다.
class Shape {
// ...생략
virtual int CalcArea() = 0;
};
class Rectangular: public Shape{
private:
int height_, width_;
public:
// ... 생략
int CalcArea(){
return height_ * width_;
}
};
int main(){
Shape *class_ptr = new Rectangular("직사각형", 15, 10);
std::cout << class_ptr -> CalcArea(); // 150
}
이런 가짜 virtual 함수를 Pure virtual function이라고 부릅니다.
함수원형 = 0
과 같은 표현을 사용하면 컴파일러가 기본적인 virtual function에 대한 셋팅을 해둡니다.
14-5. 추상 클래스 (Abstract class)
추상 클래스라 함은, 하나 이상의 Pure virtual function을 포함하는 클래스입니다.
일종의 인터페이스와 비슷한 개념인데요,
이 클래스 자체는 별 기능이 없지만, 상속받는 클래스에 뼈대를 제공해주는 역할을 할 수 있습니다.
class Animal
{
public:
virtual ~Animal() {} // 가상 소멸자의 선언
virtual void Cry()=0; // 순수 가상 함수의 선언
};
이런 클래스를 상속받은 클래스들은 해당 순수 가상 함수들을 전부 다 오버라이딩을 해야 합니다.