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++] 오버라이딩(overriding) 본문

Programming/C++

[Programming/C++] 오버라이딩(overriding)

scii 2023. 1. 21. 01:28

순수 가상 함수

부모 클래스의 멤버 함수를 자식 클래스에서 재정의하는 것을 오버라이딩(overriding)이라고 부른다. 이 개념은 오버로딩과 혼동하지 않게 주의해야 한다. 오버라이딩과 관련한 몇 가지 주제를 살펴보는데, 먼저 순수 가상 함수를 살펴보자.

순수 가상 함수(Pure Virtual Functions)는 가상 함수의 한 종류다. 

순수 가상 함수는 왜 사용할까?

순수 가상 함수를 만드는 방법은 간단하다. 가산 함수의 선언 뒤에 다음과 같이 '=0'을 붙여주면 된다. 마치 함수에 0값을 대입한 것처럼 보인다.

virtual void Draw() const = 0;

순수 가상 함수는 함수의 정의 부분이 필요하지 않다. 위와 같이 함수의 원형망 가지고 있는 함수가 바로 순수 가상 함수다. 그렇다면 하는 일도 없는 함수를 왜 만들까?

순수 가상 함수의 의미는 간단하다. "이 함수는 정의가 없다. 그러므로 호출할 수가 없다. 하지만 자식 클래스에서 이 함수를 재정의(overriding)할 것이다. 그러니까 다형성을 사용해서 이 함수를 호출해라." 이것이 바로 순수 가상 함수로 만들어주는 의미다.

순수 가상 함수로 만들기

#include <iostream>

using namespace std;

class Shape{
public:
    void Move(double x, double y);
    // 순수 가상 함수로 만듦.
    virtual void Draw() const = 0;
    
    // 가상 함수가 클래스내의 하나라도 존재한다면, 소멸자 역시 가상 함수로 만들어야 한다!
    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;
}


// 사각형 클래스
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

이렇게 하나 이상의 순수 가상 함수를 가진 클래스를 추상 클래스라고 부르는데, 추상 클래스의 객체를 만드는 것은 불가능하다. 다음과 같이  Shape 클래스의 객체를 생성하려고 하면 오류가 발생한다.

Shape s;

추상 클래스는 이름 그대로 추상적인 개념을 상징하는 클래스다. Shape 클래스만 해도 '도형'이라는 것으로 머리 속에서만 존재하는 추상적인 개념이다. 그리고 이런 추상적인 개념을 눈 앞에 보이게 하는 것 자체가 모순이기 때문에 추상 클래스의 객체를 만들 수 없는 것도 이해할 수 있다.


콘크리트 클래스

콘트리트 클래스(Concrete Class)는 추상 클래스와 반대가 되는 개념이다. 추상 클래스가 아닌 클래스를 콘크리트 클래스라고 생각하면 된다. 혹은 객체를 생성해서 사용하기 위한 용도의 클래스라고 생각해도 된다. 추상 클래스는 객체를 생성해서 사용하기 위한 용도가 아니라 다형성을 위한 부모 클래스로서의 용도를 가지고 있다.

콘크리트 클래스를 굳이 번역하자면 '구체적인 클래스'가 될 것이다. 사전적인 의미로도 추상 클래스와 정반대의 의미다. 


다양한 종류의 멤버 함수

  • 일반적인 멤버 함수
  • 가상 함수
  • 순수 가상 함수

어떤 경우에 멤버 함수를 가상 함수로 만들 것인지, 아니면 순수 가상 함수로 만들것인지를 생각해보자.

  • 처음엔 그냥 멤버 함수로 만든다.
  • 다형성을 이용해야 하는 경우라면 가상 함수로 만든다.
  • 다형성을 위해서 함수의 원형만 필요한 경우라면 순수 가상 함수로 만든다.

멤버 함수를 새로 작성할 때 무조건 일반적인 멤버 함수로 만들자. 그렇게 쓰다가 문제가 없으면 그만이고 문제가 생기면 가상 함수로 고치는 방법을 고려해보면 된다.

이제 자식 클래스들이 생기고 다형성을 사용해서 이 클래스들을 다룰 필요가 생겼다고 해보자. 이때는 멤버 함수를 가상 함수로 바꾸지 않으면 안 되는 상황이 생긴다. 그런 상황이 닥치면 멤버 함수를 가상 함수로 만들면 된다.

하지만 순수 가상 함수의 경우에는 반드시 필요한 상황이란 없다. 그저 순수 가상 함수로 만드는 것이 '더 좋은' 상황이 될 뿐이다. 해당 클래스를 사용해서 객체를 생성할 필요가 없고 함수의 원형만 있어도 충분한 경우라면 순수 가상 함수로 만드는 것이 좋다. 다시 말해 "이 함수는 약속된 부분(인터페이스)으로서의 역할만 한다"라고 컴파일러나 다른 개발자에게 말해주는 셈이다.


오버로딩과 오버라이딩

오버로딩괸 함수를 오버라이딩할 때 주의해야 할 점을 살펴보자.

오버로드된 멤버 함수

다음 예제에는 애완 동물(Pet) 클래스와 강아지(Dog) 클래스가 등장한다. 강아지 클래스는 애완 동물 클래스의 자식 클래스이다.

#include <iostream>
#include <string>

using namespace std;

class Pet{
public:
    void Eat();
    void Eat(const string& it);
    
    string name;
};

void Pet::Eat(){
    cout<<name<<" where is the food?"<<endl;
}

void Pet::Eat(const string& it){
    cout<<name<<" i like "<<it<<endl;
}


class Dog: public Pet {
};

int main(int argc, const char * argv[]) {
    // 강아지 객체 생성
    Dog dog1;
    dog1.name = "rami";
    
    dog1.Eat();
    dog1.Eat("milk");
    
    return 0;
}


// 결과
rami where is the food?
rami i like milk

잘 작동한다. 그런데 강아지 클래스에서 Eat() 함수 중에 하나를 오버라이드하면 문제가 발생한다.

// 컴파일 에러
class Dog: public Pet {
public:
    void Eat();
};

void Dog::Eat(){
    cout<<name<<"Growl~"<<endl;
}

Dog 클래스에서 Eat()함수를 오버라이드한 것 밖에 없다. 그런데 컴파일 에러가 발생했다. 오버로드된 함수를 오버라이드할 때는 다음과 같은 규칙에 주의해야 한다.

  • 부모 클래스에서 오버로드된 함수 중에 어느 것 하나라도 오버라이드하면 나머지 다른 함수들도 모두 사용할 수 없다.

이 규칙 때문에 Eat() 함수 중에 하나만 오버라딩 하였지만, 나머지 Eat() 함수도 사용할 수 없게 된 것이다. 꼭 호출하고 싶다면 다음과 같이 할 수 있다.

dog1.Pet::Eat("milk");

오버로드된 생성자들

생성자는 오버라딩의 대상이 아니지만 컴파일러가 기본적으로 제공해주는 생성자에 대해서 같은 규칙을 적용해 볼 수 있다. 엄밀히 따져서 상속과는 관련 없는 이야기지만 상속의 경우와 비슷하기 때문에 알아볼 필요가 있다.

// 컴파일 에러 발생
class Dog: public Pet {
public:
    void Eat();
    
    // Dog 클래스에 인자기 있는 생성자 추가
    Dog(int n);
};

Dog::Dog(int n){
}

Dog 클래스에 디폴트 생성자가 없다는 오류가 발생한다. 아무런 생성자도 정의하지 않은 경우에는 컴파일러가 기본적으로 디폴트생성자와 복사 생성자를 제공해준다. 이 디폴트 생성자는 아무 일도 하지 않고 복사 생성자는 멤버를 1:1로 복사해준다.

그런데 생성자를 하나 만든 순간 컴파일러가 제공해주는 디폴트 생성자를 사용할 수 없게 되어버린다. 이런 경우에는 아무 일도 하지 않는 디폴트 생성자를 직접 추가해주는 수밖에 없다.

Comments