Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-17 00:00
관리 메뉴

nomad-programmer

[Programming/C++] 가상 함수를 사용한 다형성의 구현 본문

Programming/C++

[Programming/C++] 가상 함수를 사용한 다형성의 구현

scii 2023. 1. 21. 00:02

언제 가상 함수가 필요할까?

가상 함수의 필요성을 알아보려면 소스 코드를 보는 것이 가장 좋다. 다음의 예제를 보자.

#include <iostream>

using namespace std;

class Shape{
public:
    void Move(double x, double y);
    void Draw() const;
    
    Shape();
    Shape(double x, double y);
    
protected:
    double _x, _y;
};

Shape::Shape(): _x(0), _y(0) {}

Shape::Shape(double x, double y): _x(x), _y(y) {}

void Shape::Move(double x, double y){
    _x = x;
    _y = y;
}

void Shape::Draw() const{
    cout<<"[Shape] Position = "<<_x<<", "<<_y<<endl;
}


// 사각형 클래스
class Rectangle: public Shape{
public:
    void Draw() const;
    void Resize(double width, double height);
    
    Rectangle();
    Rectangle(double x, double y, double width, double height);
    
protected:
    double _width;
    double _height;
};

Rectangle::Rectangle() : _width(100.0f), _height(100.0f) {}

Rectangle::Rectangle(double x, double y, double width, double height)
:Shape(x, y){
    Resize(width, height);
}

void Rectangle::Draw() const{
    cout<<"[Rectangle] Position = "<<_x<<", "<<_y<<" "<<"Size = "<<_width<<", "<<_height<<endl;
}

void Rectangle::Resize(double width, double height){
    _width = width;
    _height = height;
}


// 원 클래스
class Circle: public Shape{
public:
    void Draw() const;
    void SetRadius(double radius);
    
    Circle();
    Circle(double x, double y, double radius);
    
protected:
    double _radius;
};

Circle::Circle() : _radius(100.0f) {}

Circle::Circle(double x, double y, double radius)
:Shape(x, y){
    SetRadius(radius);
}

void Circle::Draw() const {
    cout<<"[Circle] Position = "<<_x<<", "<<_y<<" "<<"Radius = "<<_radius<<endl;
}

void Circle::SetRadius(double radius){
    _radius = radius;
}


int main(int argc, const char * argv[]) {
    Shape* shapes[5] = {nullptr};
    
    shapes[0] = new Circle(100, 100, 50);
    shapes[1] = new Rectangle(300, 300, 100, 100);
    shapes[2] = new Rectangle(200, 100, 50, 150);
    shapes[3] = new Circle(100, 300, 150);
    shapes[4] = new Rectangle(200, 200, 200, 200);
    
    for(int i=0; i<5; i++){
        shapes[i]->Draw();
    }
    
    for(int i=0; i<5; i++){
        delete shapes[i];
        shapes[i] = nullptr;
    }
    
    return 0;
}


// 결과
[Shape] Position = 100, 100
[Shape] Position = 300, 300
[Shape] Position = 200, 100
[Shape] Position = 100, 300
[Shape] Position = 200, 200

위의 코드에서 보이듯이 도형 클래스(shape)가 최상위 부모 클래스이고, 나머지 원, 사각형 클래스가 자식 클래스가 된다. 이런 계층 구조는 매우 타당하다. 예를 들어 원은 도형이다. 다시 말해 원과 도형은 is-a 관계를 가지고 있다는 뜻이다. 그렇기 때문에 원 클래스가 도형 클래스를 상속 받는 구조가 매우 합당하다고 할 수 있다. 

shape 클래스는 일반적인 도형을 상징하는 클래스이다. 여기서 도형이라 함은 원, 사각형, 삼각형 등을 모두 통틀어서 부르는 이름인 바로 그 '도형'을 뜻한다. 그래서 shape 클래스의 멤버들은 일반적으로 모든 도형에 필요한 것들이다. 예를 들어 _x, _y처럼 도형의 위치를 보관하는 멤버 변수는 도형 클래스의 모든 자식 클래스들에 필요한 것이다.

하지만 circle 클래스에 있는 _radius처럼 반지름을 보관하는 멤버 변수는 circle 클래스에만 필요한 멤버 변수다. 그렇기 때문에 _radius는 circle 클래스의 멤버로 만드는 것이 옳고, _x, _y는 shape 클래스의 멤버로 만드는 것이 옳다.

이제 main() 함수를 한번 보도록 하자. 모든 클래스들이 shape 클래스의 자식 클래스라는 점을 사용해서 하나의 배열에 모든 객체를 보관할 수 있다. 이것이 상속이 가진 장점이다.

만약 도형 클래스들이 수십 개 있었고, 또 그 클래스마다 배열을 따로 만들어서 관리했다면 Draw() 함수를 호출하는 코드 역시 그 클래스마다 하나씩 있어야 할 것이다.

// 클래스 별로 객체를 보관할 배열이 있어야 한다.
Circle* circles[10];
Rectangle* rects[10];
Triangle* triangles[10];
Ellipse* ellipses[10];
...

// 클래스 별로 화면에 출력하는 코드
for(int i=0; i<10; ++i)
    circles[i]->Draw();
    
for(int i=0; i<10; ++i)
    rects[i]->Draw();
    
for(int i=0; i<10; ++i)
    triangles[i]->Draw();
...

위의 소스 코드를 보면 상속을 사용하는 것이 얼마나 편하게 해주는지 확인할 수 있다. 하지만 문제가 있다. 실행 결과를 보면 모든 결과가 Shape::Draw() 함수만을 호출하고 있는 문제이다.

원하는 것은 이것이 아니다. 만약 객체가 circle 타입이었다면 Circle::Draw()가 호출되어야 하고, Rectangle 타입이었다면 Rectangle::Draw() 함수가 호출됐어야 한다.

바로 이러한 문제점을 해결하기 위해서 가상 함수를 사용해야 한다. 가상 함수가 어떻게 이 문제를 해결할 수 있는지 알아보자.


가상 함수

가상 함수를 사용해서 위의 문제를 해결하는 방법은 아래의 예제 코드에서 살펴볼 수 있다. 바로 virtual 키워드를 붙여준 것이다. Shape::Draw() 멤버 함수 맨 처음 부분에.

#include <iostream>

using namespace std;

class Shape{
public:
    void Move(double x, double y);
    // 가상 함수로 만듦.
    virtual void Draw() const;
    
    // 가상 함수가 클래스내의 하나라도 존재한다면, 소멸자 역시 가상 함수로 만들어야 한다!
    virtual ~Shape();
    
    Shape();
    Shape(double x, double y);
    
protected:
    double _x, _y;
};

Shape::Shape(): _x(0), _y(0) {}

Shape::Shape(double x, double y): _x(x), _y(y) {}

Shape::~Shape() {}

void Shape::Move(double x, double y){
    _x = x;
    _y = y;
}

void Shape::Draw() const{
    cout<<"[Shape] Position = "<<_x<<", "<<_y<<endl;
}


// 사각형 클래스
class Rectangle: public Shape{
public:
    void Draw() const override;
    void Resize(double width, double height);
    
    Rectangle();
    Rectangle(double x, double y, double width, double height);
    
protected:
    double _width;
    double _height;
};

Rectangle::Rectangle() : _width(100.0f), _height(100.0f) {}

Rectangle::Rectangle(double x, double y, double width, double height)
:Shape(x, y){
    Resize(width, height);
}

void Rectangle::Draw() const{
    cout<<"[Rectangle] Position = "<<_x<<", "<<_y<<" "<<"Size = "<<_width<<", "<<_height<<endl;
}

void Rectangle::Resize(double width, double height){
    _width = width;
    _height = height;
}


// 원 클래스
class Circle: public Shape{
public:
    void Draw() const override;
    void SetRadius(double radius);
    
    Circle();
    Circle(double x, double y, double radius);
    
protected:
    double _radius;
};

Circle::Circle() : _radius(100.0f) {}

Circle::Circle(double x, double y, double radius)
:Shape(x, y){
    SetRadius(radius);
}

void Circle::Draw() const {
    cout<<"[Circle] Position = "<<_x<<", "<<_y<<" "<<"Radius = "<<_radius<<endl;
}

void Circle::SetRadius(double radius){
    _radius = radius;
}


int main(int argc, const char * argv[]) {
    Shape* shapes[5] = {nullptr};
    
    shapes[0] = new Circle(100, 100, 50);
    shapes[1] = new Rectangle(300, 300, 100, 100);
    shapes[2] = new Rectangle(200, 100, 50, 150);
    shapes[3] = new Circle(100, 300, 150);
    shapes[4] = new Rectangle(200, 200, 200, 200);
    
    for(int i=0; i<5; i++){
        shapes[i]->Draw();
    }
    
    for(int i=0; i<5; i++){
        delete shapes[i];
        shapes[i] = nullptr;
    }
    
    return 0;
}


// 결과
[Circle] Position = 100, 100 Radius = 50
[Rectangle] Position = 300, 300 Size = 100, 100
[Rectangle] Position = 200, 100 Size = 50, 150
[Circle] Position = 100, 300 Radius = 150
[Rectangle] Position = 200, 200 Size = 200, 200
Program ended with exit code: 0

만약 virtual, override, final 키워드가 정확히 무엇인지 헛갈린다면 아래의 링크를 클릭하여 정확히 무엇인지 확인하고 오면 좋겠다.

2023.01.16 - [Programming/C++] - [Programming/C++] virtual, override, final 키워드

 

[Programming/C++] virtual, override, final 키워드

c++ 상속을 공부하며 많이 보았던 키워드는 virtual 키워드일 것이다. virtual에 비하여 많이 보지 못했던 키워드는 override와 final 키워드일 것이다. override와 final 키워드는 c++11 이후에 등장하는 키워

nomad-programmer.tistory.com

결과를 먼저 보자. 이제 객체의 타입에 맞는 Draw() 함수가 호출된다. Circle객체라면 Circle::Draw()가 호출되고 Rectangle 객체라면 Rectangle::Draw()가 호출된다. virtual 이라는 한 단어를 추가한 것뿐인데 문제가 바로 해결되었다.

Shape::Draw()함수를 가상 함수로 만들면 Circle::Draw()와 Rectangle::Draw() 함수 역시 자동적으로 가상 함수가 된다. Circle::Draw()와 Rectangle::Draw() 함수를 선언할 때 virtual 키워드를 붙여주는 것과 상관 없이 자동적으로 그렇게 된다는 뜻이다.

또 virtual 키워드는 클래스의 정의 안쪽에서만 한 번 붙여주면 된다. 클래스 밖에서 함수를 정의할 때는 virtual 키워드가 필요 없다.


소멸자를 가상 함수로 만들기

소멸자가 있다면 소멸자를 반드시 가상 함수로 만들어야 한다. 소멸자를 가상 함수로 만드는 방법은 일반 멤버 함수와 동일하다.

virtual ~Shape();

그렇지 않으면 객체를 해제시킬 때 올바른 소멸자가 호출되지 않는다. 일반 멤버 함수와 동일하게 소멸자도 가상 함수로 만들어줘야만 올바른 소멸자가 호출될 수 있다. 다음과 같은 규칙을 기억하고 있으면 된다.

  • 클래스에 하나 이상의 가상 함수가 있는 경우에는 소멸자도 반드시 가상 함수로 만들어야 한다.

다형성과 가상 함수

다형성이란 다양한(poly) 형태(morph)를 뜻한다고 정리할 수 있겠다. 객체지향 프로그래밍에서의 다형성은 다음과 같이 정의할 수 있다.

  • 객체지향 프로그래밍에서의 다형성이란 타입에 관계 없이 동일한 방법으로 다룰 수 있는 능력을 말한다.

다형성은 부품간의 조립, 다시 말해 객체간의 연결을 유연하게 해주는 원동력이 된다.

C++와 다형성

부모 클래스의 포인터로 자식 객체를 가리킬 수 있따는 점과 이런 경우에도 가상 함수를 통해 알맞은 자식 클래스의 함수가 호출된다는 점이 다형성을 지원하는 C++의 기능이다.

동적 바인딩

바인딩이란 함수를 호출하는 명령과 실제로 호출되는 함수를 짝지어 주는 일을 말한다. 예를 들어 다음의 코드를 보자. 다음 코드에 의해서 어떤 함수가 실행될 지 알 수 있는가?

void Func(Shape* s){
    s->Draw();
}

알 수 없다. s가 가리키고 있는 개체에 따라서 Circle::Draw()가 호출될 수도 있고, Rectangle::Draw()가 호출될 수도 있다. 결국 프로그램을 실행해봐야 알 수 있는 것이다. 이렇게 프로그램을 실행한 후에야 어떤 함수를 호출할 지 알 수 있는 경우를 동적 바인딩(Dynamic Binding)이라고 한다. 혹은 후기 바인딩이라고도 한다.

반대로 다음과 같이 프로그램이 실행되지 않은 상태에서도 어떤 함수가 호출될지 명백하게 알 수 있는 경우를 정적 바인딩이라고 한다.

Circle c;
c.Draw();

오버로딩과 다형성

오버로딩도 다형성의 한 종류다. 하나의 이름을 사용하지만 인자에 따라서 여러 가지 다른 함수들이 호출될 수 있기 때문이다. 하지만 다형성을 논할 때는 오버로딩은 크게 중요하게 다뤄지지 않는다. 오버로딩의 경우에는 굳이 프로그램을 실행시켜보지 않아도 인자만 보면 어느 함수가 호출될지 알기 때문이다.

오버로딩처럼 인자에 의해서 호출될 함수를 결정하는 경우를 컴파일 시간 다형성(Compile-time Polymorphism) 혹은 인자와 관련된 다형성(Parametic Polymorphism)이라고 부른다. 반면에 가상 함수와 같이 객체의 타입에 따라서 호출될 함수를 결정하는 다형성을 실행 시간 다형성(Run-time Polymorphism)이라고 부른다.

Comments