수색…
다형성 클래스 정의
전형적인 예는 사각형, 원 및 다른 구체적인 모양으로 파생 될 수있는 추상 모양 클래스입니다.
부모 클래스 :
다형성 클래스부터 시작해 보겠습니다.
class Shape {
public:
virtual ~Shape() = default;
virtual double get_surface() const = 0;
virtual void describe_object() const { std::cout << "this is a shape" << std::endl; }
double get_doubled_surface() const { return 2 * get_surface(); }
};
이 정의를 읽는 방법?
virtual
함수 키워드를 사용하여 소개 된 멤버 함수를 통해 다형성 동작을 정의 할 수 있습니다. 여기서get_surface()
및describe_object()
는 분명히 원 대신 사각형에 대해 다르게 구현됩니다. 함수가 객체에서 호출되면 객체의 실제 클래스에 해당하는 함수가 런타임에 결정됩니다.추상 모양에 대해
get_surface()
를 정의하는get_surface()
의미가 없습니다. 이것이 함수 뒤에= 0
이 오는 이유입니다. 이것은 함수가 순수 가상 함수 임을 의미합니다.다형성 클래스는 항상 가상 소멸자를 정의해야합니다.
비 가상 구성원 함수를 정의 할 수 있습니다. 이 함수가 객체에 대해 호출 될 때 컴파일 타임에 사용되는 클래스에 따라 함수가 선택됩니다. 여기서
get_double_surface()
는 이런 방식으로 정의됩니다.하나 이상의 순수 가상 함수를 포함하는 클래스는 추상 클래스입니다. 추상 클래스는 인스턴스화 될 수 없습니다. 포인터 또는 추상 클래스 유형의 참조 만 가질 수 있습니다.
파생 클래스
일단 다형 기본 클래스가 정의되면이를 파생시킬 수 있습니다. 예 :
class Square : public Shape {
Point top_left;
double side_length;
public:
Square (const Point& top_left, double side)
: top_left(top_left), side_length(side_length) {}
double get_surface() override { return side_length * side_length; }
void describe_object() override {
std::cout << "this is a square starting at " << top_left.x << ", " << top_left.y
<< " with a length of " << side_length << std::endl;
}
};
몇 가지 설명 :
- 부모 클래스의 가상 함수를 정의하거나 재정의 할 수 있습니다. 부모 클래스에서 함수가 가상이라는 사실은 파생 클래스에서 가상으로 만듭니다. 컴파일러에게 키워드
virtual
다시 말할 필요가 없습니다. 그러나 함수 선언의 주목할만한 변화로 인해 발생하는 미묘한 버그를 방지하기 위해 함수 선언 끝에 키워드override
를 추가하는 것이 좋습니다. - 부모 클래스의 모든 순수 가상 함수가 정의되면이 클래스의 객체를 인스턴스화 할 수 있습니다. 그렇지 않으면 추상 클래스가됩니다.
- 모든 가상 기능을 재정의 할 의무가 없습니다. 필요에 따라 부모의 버전을 유지할 수 있습니다.
인스턴스화의 예
int main() {
Square square(Point(10.0, 0.0), 6); // we know it's a square, the compiler also
square.describe_object();
std::cout << "Surface: " << square.get_surface() << std::endl;
Circle circle(Point(0.0, 0.0), 5);
Shape *ps = nullptr; // we don't know yet the real type of the object
ps = &circle; // it's a circle, but it could as well be a square
ps->describe_object();
std::cout << "Surface: " << ps->get_surface() << std::endl;
}
안전한 다운 캐스팅
다형성 클래스의 객체에 대한 포인터가 있다고 가정 해보십시오.
Shape *ps; // see example on defining a polymorphic class
ps = get_a_new_random_shape(); // if you don't have such a function yet, you
// could just write ps = new Square(0.0,0.0, 5);
다운 캐스트는 일반적인 다형성 Shape
에서 Square
또는 Circle
과 같이 파생 된보다 구체적인 모양으로 캐스팅됩니다.
왜 추락 하는가?
대부분의 경우, 가상 함수는 유형과 상관없이 객체를 조작 할 수 있으므로 객체의 실제 유형을 알 필요가 없습니다.
std::cout << "Surface: " << ps->get_surface() << std::endl;
다운 캐스트가 필요하지 않으면 디자인이 완벽 할 것입니다.
그러나 때때로 다운 캐스트가 필요할 수 있습니다. 일반적인 예는 하위 클래스에 대해서만 존재하는 비 가상 함수를 호출하려는 경우입니다.
예를 들어 서클을 고려해보십시오. 원은 지름이 있습니다. 따라서 클래스는 다음과 같이 정의됩니다.
class Circle: public Shape { // for Shape, see example on defining a polymorphic class
Point center;
double radius;
public:
Circle (const Point& center, double radius)
: center(center), radius(radius) {}
double get_surface() const override { return r * r * M_PI; }
// this is only for circles. Makes no sense for other shapes
double get_diameter() const { return 2 * r; }
};
get_diameter()
멤버 함수는 원에만 존재합니다. Shape
객체에는 정의되어 있지 않습니다.
Shape* ps = get_any_shape();
ps->get_diameter(); // OUCH !!! Compilation error
다운 캐스트하는 방법?
ps
가 서클을 가리키는 지 확실히 알면 static_cast
선택할 수 있습니다.
std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;
이것은 트릭을 할 것입니다. 그러나 매우 위험합니다. ps
가 Circle
이 아닌 다른 것으로 나타나면 코드의 동작이 정의되지 않습니다.
따라서 러시아어 룰렛을 사용하는 대신 dynamic_cast
안전하게 사용해야합니다. 특히 다형 클래스에 대한 것입니다.
int main() {
Circle circle(Point(0.0, 0.0), 10);
Shape &shape = circle;
std::cout << "The shape has a surface of " << shape.get_surface() << std::endl;
//shape.get_diameter(); // OUCH !!! Compilation error
Circle *pc = dynamic_cast<Circle*>(&shape); // will be nullptr if ps wasn't a circle
if (pc)
std::cout << "The shape is a circle of diameter " << pc->get_diameter() << std::endl;
else
std::cout << "The shape isn't a circle !" << std::endl;
}
다형성이 아닌 클래스에서는 dynamic_cast
를 사용할 수 없습니다. 클래스 나 부모 중 적어도 하나는 가상 함수를 사용할 수 있어야합니다.
다형성 및 소멸자
파생 된 인스턴스가 기본 포인터 / 참조로 저장되어있는 클래스를 다형 적으로 사용하려는 경우 기본 클래스의 소멸자는 virtual
이거나 protected
되어야합니다. 전자의 경우에는 객체 파괴가 vtable
을 검사하여 동적 유형에 따라 올바른 소멸자를 자동 호출합니다. 후자의 경우, 기본 클래스 포인터 / 참조를 통해 객체를 파기하는 것은 불가능하며 객체는 명시 적으로 실제 유형으로 처리 될 때만 삭제할 수 있습니다.
struct VirtualDestructor {
virtual ~VirtualDestructor() = default;
};
struct VirtualDerived : VirtualDestructor {};
struct ProtectedDestructor {
protected:
~ProtectedDestructor() = default;
};
struct ProtectedDerived : ProtectedDestructor {
~ProtectedDerived() = default;
};
// ...
VirtualDestructor* vd = new VirtualDerived;
delete vd; // Looks up VirtualDestructor::~VirtualDestructor() in vtable, sees it's
// VirtualDerived::~VirtualDerived(), calls that.
ProtectedDestructor* pd = new ProtectedDerived;
delete pd; // Error: ProtectedDestructor::~ProtectedDestructor() is protected.
delete static_cast<ProtectedDerived*>(pd); // Good.
이러한 두 가지 방법 모두 파생 클래스의 소멸자가 파생 클래스 인스턴스에서 항상 호출되어 메모리 누수를 방지합니다.