C++/Polymorphism

C++ Polymorphism

Soul-Learner 2016. 12. 25. 18:16

C++ 프로그래밍, 다형성 ( Polymorphism )


다형성의 사전적 의미

프로그램 언어의 다형성(polymorphism; 폴리모피즘)은 그 프로그래밍 언어의 자료형 체계의 성질을 나타내는 것으로, 프로그램 언어의 각 요소들(상수, 변수, 식, 오브젝트, 함수, 메소드 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다.

다형성은 원래 생물학에서 유래한 단어인데, 동일한 종(Species)에 속하는 생물이라 할지라도 다양한 변이를 가지는 현상을 나타낸다


C++ 언어에서의 다형성

C++언어와 같은 객체지향 언어에서는 어떤 클래스의 포인터 변수에 그 클래스의 하위 클래스 객체의 주소도 할당할 수 있도록 허용된다. 그러므로 클래스의 포인터 변수에는 다양한 자료형의 주소를 할당할 수 있다는 말이다. 예를 들어, 도형(CShape) 클래스가 부모 클래스이고 삼각형(CTriangle) 클래스가 자식 클래스라면 CShape* 형 변수에 CTriangle 객체의 주소를 할당할 수 있다는 뜻이므로 CShape* 형 변수에는 CShape 형뿐만 아닌 그 하위 클래스 객체의 주소도 할당할 수 있게 된다. 이로 인해서 객체를 가리키는 포인터 변수에는 여러가지 형의 객체를 가리킬 수 있고 객체의 런타임 형(Types)에 따라서 함수의 기능에 차이를 보이는 성질을 다형성(Polymorphism)이라고 한다. 

결국, C++ 에서 객체들이 다형성을 갖게 하는 방법가상함수(Virtual Functions)를 오버라이드하는 것이다.


넓은 의미의 다형성

  • 부모 클래스에 선언된 가상 함수(Virtual Functions)가 자식 클래스에서 다른 기능으로 오버라이드(Override)되어 런타임에는 부모나 형제들과 다른 기능을 보이는 현상
  • 함수 템플릿(Function Template)을 이용하여 동일한 함수가 다양한 자료형을 처리할 수 있는 것처럼 보이는 현상
  • 함수 오버로드(Function Overload)를 통하여 동일한 함수가 다양한 자료형을 처리할 수 있는 것처럼 보이는 현상
  • 연산자 오버로드(Operator Overload)를 통하여 한가지 연산자가 다양한 기능을 보이는 현상


가상 함수 ( Virtual Functions )

도형CShape) 클래스가 있고 도형 클래스의 모든 자식 클래스에서 공통적으로 가져야 하는 함수가 있다면 부모 클래스인 도형 클래스에 해당 함수를 선언한 후에 자식 클래스에서 상속하는 것이 바람직할 것이다. 그러나 부모 클래스에서 자식 클래스의 모든 기능을 미리 구현하는 것은 적합하지 않을 때도 있다. 즉, 자식 클래스가 공통적으로 필요한 함수가 있는데 부모 클래스에서 그 기능을 모두 구현할 수 없는 경우가 있다는 것이다. 이런 경우에는 부모 클래스에 해당 함수를 선언할 때 virtual 키워드를 사용하고 자식 클래스에서 그 함수를 오버라이드하면 된다.


가상 함수를 자식 클래스에서 오버라이드하여 다형성을 활용하는 예(주석 부분을 주의 깊게 보세요)

#include <iostream>
#include <locale>
#include <string>
#define _USE_MATH_DEFINES
#include <cmath>

using namespace std;

class CShape
{
    protected:
    double width;
    double height;

    public:
    CShape(){}
    CShape(double w, double h) {
        width = w;
        height = h;
    }

    // 가상함수(Virtual Functions) 선언 : 자식 클래스에서 오버라이드 가능함
    virtual double get_area() { return 0; }
    void set_width(double w) { width = w; }
    void set_height(double h) { height = h; }
};

class CCircle : public CShape
{
    public:
    CCircle(double w, double h) : CShape(w,h)
    {
        wprintf(L"CCircle 생성자 실행\n");
    }

    // 부모한테 물려받은 get_area() 함수를 오버라이드하여 원의 면적을 구하는 기능으로 개조한다
    double get_area() {
        return M_PI * pow(width/2, 2);
    }
};

class CTriangle : public CShape
{
    public:
    CTriangle(double w, double h) : CShape(w,h) // 상위 클래스의 파라미터 있는 생성자 호출
    {
        wprintf(L"CTriangle 생성자 실행\n");
    }

    // 부모한테 물려받은 get_area() 함수를 오버라이드하여 삼각형에 면적을 구하는 기능으로 개조한다
    double get_area() {
        return width * height / 2;
    }
};

int main() {
    setlocale(LC_ALL, "");

    wcout << L"C++ 다형성(Polymorphism)" << endl;

    CShape shape;
    CCircle circle(5,5);
    CTriangle tri(5,5);

    // 동일한 CShape 포인터 변수에 각각 다른 자료형의 주소가 할당된다
    CShape* pShape = &shape;
    CShape* pShape1 = &circle;
    CShape* pShape2 = &tri;

    // 모두 CShape* 형의 변수를 이용한 함수의 호출이지만 서로 다른 기능을 보인다
    // 모두 CShape* 형이지만 실제 런타임 타입(Runtime Types)은 서로 다르다
    double shape_area = pShape->get_area();
    double circle_area = pShape1->get_area();
    double tri_area = pShape2->get_area();

    wcout << L"도형의 면적=" << shape_area << endl;
    wcout << L"원의 면적=" << circle_area << endl;
    wcout << L"삼각형 면적=" << tri_area << endl;

    return 0;
}


순수가상함수(Pure Virtual Functions)와 추상 클래스(Abstract Class)

위의 코드에서 CShape 클래스의 get_area() 함수는 기능을 정의하기가 곤란하다. 왜냐면, CShape 클래스를 상속하는 자식 도형 클래스는 다양한데 도형에 따라서 면적을 계산하는 방식은 다르므로 도형 클래스가 자식 도형의 면적 계산을 미리 구현할 수는 없기 때문이다. 이런 경우에는 부모 클래스의 가상 함수는 함수의 바디를 갖는 것 자체가 의미가 없다. 그래서 바디를 선언하지 않고 virtual double get_area() = 0; 과 같은 형식을 이용하여 일부러 함수의 바디를 작성하지 않은 순수가상함수를 선언하는 것이다

순수가상함수를 1개이상 가진 클래스는 추상클래스(Abstract Class)가 되며, 추상클래스는 객체를 생성할 수 없고 부모 클래스의 역할은 할 수 있다

추상클래스를 상속하는 자식 클래스에서는 반드시 가상함수를 오버라이드해야만 오류를 피할 수 있다.


추상 클래스(Abstract Class)의 예

#include <iostream>
#include <locale>
#include <string>
#define _USE_MATH_DEFINES
#include <cmath>

using namespace std;
// 순수가상함수를 1개이상 가진 클래스는 추상(Abstract)클래스가 됨
// 추상 클래스는 객체를 생성할 수 없으며, 부모 클래스의 역할은 할 수 있다
class CShape
{
    protected:
    double width;
    double height;

    public:
    CShape(){}
    CShape(double w, double h) {
        width = w;
        height = h;
    }

    // 순수가상함수(Pure Virtual Functions) 선언 : 현재 클래스는 객체를 생성할 수 없음
    virtual double get_area()=0;
    void set_width(double w) { width = w; }
    void set_height(double h) { height = h; }
};

class CCircle : public CShape
{
    public:
    CCircle(double w, double h) : CShape(w,h)
    {
        wprintf(L"CCircle 생성자 실행\n");
    }

    // 부모의 순수가상함수를 오버라이드하지 않으면 오류 발생
    double get_area() {
        return M_PI * pow(width/2, 2);
    }
};

class CTriangle : public CShape
{
    public:
    CTriangle(double w, double h) : CShape(w,h) // 상위 클래스의 파라미터 있는 생성자 호출
    {
        wprintf(L"CTriangle 생성자 실행\n");
    }

    // 부모의 순수가상함수를 오버라이드하지 않으면 오류 발생
    double get_area() {
        return width * height / 2;
    }
};

int main() {
    setlocale(LC_ALL, "");

    wcout << L"C++ 추상클래스와 순수가상함수" << endl;

    //CShape shape; // 오류, 추상 클래스는 객체를 생성할 수 없다.
    CCircle circle(5,5);
    CTriangle tri(5,5);

    CShape* pShape1 = &circle;
    CShape* pShape2 = &tri;

    double circle_area = pShape1->get_area();
    double tri_area = pShape2->get_area();

    wcout << L"원의 면적=" << circle_area << endl;
    wcout << L"삼각형 면적=" << tri_area << endl;

    return 0;
}


위의 모든 내용은 동적으로 생성되는 객체에도 동일하게 적용된다. 동적으로 객체를 생성하는 부분만을 보면 다음과 같다.

int main() {
    setlocale(LC_ALL, "");

    wcout << L"C++ 다형성(Polymorphism)" << endl;

    // 런타임에(동적으로) 객체를 생성한다
    CShape* pShape1 = new CCircle(5,5);
    CShape* pShape2 = new CTriangle(5,5);

    double circle_area = pShape1->get_area();
    double tri_area = pShape2->get_area();

    wcout << L"원의 면적=" << circle_area << endl;
    wcout << L"삼각형 면적=" << tri_area << endl;

    return 0;
}