My Profile Photo

DongChanS's blog


수학과 학생의 개발일지


파이썬 유저를 위한 C++ (11) - 클래스 (5) - 다형성

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; // 순수 가상 함수의 선언
};

이런 클래스를 상속받은 클래스들은 해당 순수 가상 함수들을 전부 다 오버라이딩을 해야 합니다.

comments powered by Disqus