다형성을 활용한 도형 그리기 프로그램
실습 개요
이번 실습에서는 다형성의 개념을 실제로 적용하여 다양한 도형을 그리고 관리하는 프로그램을 만들어볼 것입니다.
이 과정을 통해 추상 클래스, 가상 함수, 상속 등의 개념을 실제 코드로 구현하고 그 효용성을 직접 체험해볼 수 있습니다.
학습 목표
- 추상 클래스와 순수 가상 함수의 사용법 익히기
- 상속을 통한 클래스 계층 구조 설계하기
- 다형성을 활용하여 객체를 유연하게 다루는 방법 이해하기
- 스마트 포인터를 사용하여 메모리를 안전하게 관리하는 방법 학습하기
- 실제 문제에 객체 지향 설계 원칙을 적용하는 능력 기르기
프로그램 요구사항
Shape
라는 추상 기본 클래스를 정의합니다.Circle
,Rectangle
,Triangle
등의 구체적인 도형 클래스를Shape
에서 파생시킵니다.- 각 도형은 면적과 둘레를 계산할 수 있어야 합니다.
- 각 도형은 자신을 그리는 메서드를 가져야 합니다(콘솔에 출력).
DrawingProgram
클래스를 만들어 여러 도형을 관리하고 그릴 수 있도록 합니다.
단계별 구현 가이드
Shape 클래스 정의
먼저 Shape
추상 클래스를 정의합니다.
이 클래스는 모든 도형의 공통 인터페이스를 제공합니다.
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual double getArea() const = 0;
virtual double getPerimeter() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
getArea()
: 도형의 면적을 반환합니다.getPerimeter()
: 도형의 둘레를 반환합니다.draw()
: 도형을 그립니다(여기서는 콘솔에 출력).- 가상 소멸자를 선언하여 다형적 삭제가 올바르게 동작하도록 합니다.
구체적인 도형 클래스 구현
Circle
, Rectangle
, Triangle
클래스를 Shape
에서 상속받아 구현합니다.
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() const override {
return M_PI * radius * radius;
}
double getPerimeter() const override {
return 2 * M_PI * radius;
}
void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() const override {
return width * height;
}
double getPerimeter() const override {
return 2 * (width + height);
}
void draw() const override {
std::cout << "Drawing a rectangle with width " << width
<< " and height " << height << std::endl;
}
};
class Triangle : public Shape {
private:
double a, b, c; // 세 변의 길이
public:
Triangle(double side1, double side2, double side3)
: a(side1), b(side2), c(side3) {}
double getArea() const override {
// 헤론의 공식 사용
double s = (a + b + c) / 2;
return std::sqrt(s * (s - a) * (s - b) * (s - c));
}
double getPerimeter() const override {
return a + b + c;
}
void draw() const override {
std::cout << "Drawing a triangle with sides "
<< a << ", " << b << ", " << c << std::endl;
}
};
각 도형 클래스는 Shape
의 순수 가상 함수를 오버라이드하여 구체적인 구현을 제공합니다.
DrawingProgram 클래스 구현
DrawingProgram
클래스는 여러 도형을 관리하고 그리는 역할을 합니다.
class DrawingProgram {
private:
std::vector<std::unique_ptr<Shape>> shapes;
public:
void addShape(std::unique_ptr<Shape> shape) {
shapes.push_back(std::move(shape));
}
void drawAll() const {
for (const auto& shape : shapes) {
shape->draw();
}
}
void printInfo() const {
for (const auto& shape : shapes) {
shape->draw();
std::cout << " Area: " << shape->getArea() << std::endl;
std::cout << " Perimeter: " << shape->getPerimeter() << std::endl;
}
}
};
std::unique_ptr
를 사용하여 도형 객체의 소유권을 관리합니다.addShape()
: 새로운 도형을 추가합니다.drawAll()
: 모든 도형을 그립니다.printInfo()
: 모든 도형의 정보(면적, 둘레)를 출력합니다.
메인 함수 구현
프로그램의 실행을 위한 메인 함수를 구현합니다.
int main() {
DrawingProgram program;
program.addShape(std::make_unique<Circle>(5));
program.addShape(std::make_unique<Rectangle>(4, 6));
program.addShape(std::make_unique<Triangle>(3, 4, 5));
std::cout << "Drawing all shapes:" << std::endl;
program.drawAll();
std::cout << "\nShape information:" << std::endl;
program.printInfo();
return 0;
}
프로그램 실행 및 결과 분석
프로그램을 컴파일하고 실행하면 다음과 같은 결과를 얻을 수 있습니다.
Drawing all shapes:
Drawing a circle with radius 5
Drawing a rectangle with width 4 and height 6
Drawing a triangle with sides 3, 4, 5
Shape information:
Drawing a circle with radius 5
Area: 78.5398
Perimeter: 31.4159
Drawing a rectangle with width 4 and height 6
Area: 24
Perimeter: 20
Drawing a triangle with sides 3, 4, 5
Area: 6
Perimeter: 12
이 결과를 통해 다음과 같은 점을 관찰할 수 있습니다.
- 다형성의 작동 :
DrawingProgram
은 구체적인 도형 타입을 알지 못하지만,Shape
인터페이스를 통해 모든 도형을 동일하게 처리할 수 있습니다. - 가상 함수의 동작 : 각 도형 객체의 구체적인
draw()
,getArea()
,getPerimeter()
메서드가 호출됩니다. - 코드 재사용 : 새로운 도형을 추가하더라도
DrawingProgram
클래스를 수정할 필요가 없습니다.
실습
새로운 도형 추가하기
정사각형(Square
) 클래스를 추가해봅시다.
class Square : public Shape {
private:
double side;
public:
Square(double s) : side(s) {}
double getArea() const override {
return side * side;
}
double getPerimeter() const override {
return 4 * side;
}
void draw() const override {
std::cout << "Drawing a square with side " << side << std::endl;
}
};
메인 함수에 다음 줄을 추가하여 정사각형 객체를 생성할 수 있습니다.
program.addShape(std::make_unique<Square>(5));
도형 정렬 기능 추가
도형들을 면적이나 둘레를 기준으로 정렬하는 기능을 DrawingProgram
클래스에 추가해봅시다.
class DrawingProgram {
// ... 기존 코드 ...
public:
void sortByArea() {
std::sort(shapes.begin(), shapes.end(),
[](const auto& a, const auto& b) {
return a->getArea() < b->getArea();
});
}
void sortByPerimeter() {
std::sort(shapes.begin(), shapes.end(),
[](const auto& a, const auto& b) {
return a->getPerimeter() < b->getPerimeter();
});
}
};
이제 program.sortByArea()
또는 program.sortByPerimeter()
를 호출하여 도형들을 정렬할 수 있습니다.
연습 문제
Shape
클래스에 색상 속성을 추가하고, 각 도형을 그릴 때 색상 정보도 출력하도록 프로그램을 수정하세요.- 복합 도형(
CompositeShape
)을 나타내는 클래스를 구현하세요. 이 클래스는 여러 개의Shape
객체를 포함하고, 전체 면적과 둘레를 계산할 수 있어야 합니다. DrawingProgram
클래스에 특정 유형의 도형만 선택하여 그리는 기능을 추가하세요. (힌트 :dynamic_cast
를 사용할 수 있습니다)
참고자료
- "Design Patterns : Elements of Reusable Object-Oriented Software" by Erich Gamma et al. - 객체 지향 설계 패턴에 대해 더 깊이 학습할 수 있습니다.
- "Effective C++" by Scott Meyers - C++의 효과적인 사용법과 최적의 실천 방법을 배울 수 있습니다.
- "C++ Primer" by Stanley B. Lippman et al. - C++의 기본 개념부터 고급 주제까지 폭넓게 다룹니다.