부모 클래스의 포인터나 참조를 통해 여러 자식 클래스들을 동일하게 다루면서도 각 자식의 고유한 동작을 실행시키는 것
→ 이게 다형성(polymorphism)이고, 그 핵심이 override
#include <iostream>
class Base {
int x;
public:
Base(int x = 0) : x(x) {};
virtual void Print() const { std::cout << x << ' '; }
};
class Derived : public Base {
int x;
public:
Derived(int x1 = 0, int x2 = 0) : Base(x1), x(x2) {};
void Print() const override { //⚠️ override
Base::Print();
std::cout << x << ' ';
}
};
→ override 키워드를 사용하여 이 함수가 부모 클래스의 가상 함수인 Print를 재정의하도록 하였다.
override란, 부모 클래스의 가상 함수(virtual function)를 자식 클래스에서 고쳐서 새로 정의하는 것이다. override 키워드는 "내가 이 함수는 재정의하는 거다!"를 명시한다. 주요 목적은 컴파일러가 재정의를 정확하게 수행했는지를 검사하게 하여, 실수를 방지하는 데 있다.
이를 통해 다형성(polymorphism)을 구현할 수 있다
컴파일 에러를 발생시켜, 실수를 미리 방지할 수 있다.
가상 함수를 재정의할 땐 항상 override를 붙이는 것이 좋은 습관이다.
| 항목 | 상속 | override |
|---|---|---|
| 의미 | 부모의 기능을 그대로 물려받음 | 부모의 가상 함수를 재정의함 |
| 키워드 필요 여부 | 없음 | virtual(부모), override(자식) 필요 |
| 실행 시 결정 | 정적 바인딩 (기본적으로) | 동적 바인딩 (virtual 함수인 경우) |
| 목적 | 코드 재사용 | 동작 변경, 다형성 구현 |
class Base {
public:
void Show() { std::cout << "Base Show\n"; }
};
class Derived : public Base {};
Derived는 Show()를 아무것도 안 해도 그대로 사용할 수 있음.
→ 상속
class Base {
public:
virtual void Show() { std::cout << "Base Show\n"; }
};
class Derived : public Base {
public:
void Show() override { std::cout << "Derived Show\n"; }
};
→ 이것은 단순히 Show()를 물려받는 게 아니라 고쳐서 다른 동작을 하도록 만든 것, 즉 재정의(override)
상속은 "부모 거 그냥 쓸게요"
override는 "부모 거 내가 다시 만들게요"
부모 클래스의 가상 함수 동작을 자식 클래스에서 다르게 하고 싶을 때
동일한 부모 클래스를 상속받은 여러 클래스가 각각 다른 동작을 해야 할 때
부모 포인터로 자식 객체를 다룰 때 정확한 함수가 호출되도록 할 때
#include <iostream>
class Base {
int x;
public:
Base(int x = 0) : x(x) {};
virtual void Print() const { std::cout << x << ' '; }
// ⚠️ virtual
};
class Derived : public Base {
int x;
public:
Derived(int x1 = 0, int x2 = 0) : Base(x1), x(x2) {};
void Print() const override {
// ⚠️ override
Base::Print();
std::cout << x << ' ';
}
};
int main() {
Derived d1(1, 2);
Base& b1 = d1; // ok, reference to a derived class
d1.Print(); // 1 2
b1.Print(); // 1 2
}
→ Base 타입의 참조 변수 b1이 Derived 객체를 참조하고 있으므로, virtual 함수인 Print()가 오버라이딩된 Derived의 Print()를 호출하며, 이 함수는 먼저 Base::Print()로 정수형 멤버 변수 x를 출력하고, 이어서 Derived의 멤버 변수 x를 출력하여 최종적으로 1 2를 출력한다.
b1은 겉모습은 Base지만 실제로 가리키는 객체는 Derived 타입이다. 즉, b1이 바라보는 실제 데이터는 Derived 객체다. 따라서 함수의 override가 필요하다.
→ Base 클래스의 Print()만 호출되고, Derived의 x는 출력되지 않는다. 즉 1만 출력한다.
C++에서 virtual 함수는 런타임(dynamic) 바인딩을 사용한다.
override만 있다고 자식 함수가 호출되는 게 아니다.
부모 함수가 virtual일 때만, Base 타입 참조나 포인터를 통해서도 자식 클래스의 재정의된 함수가 호출된다.
b1.Print()에서 Derived::Print()가 호출된 건, Base::Print()가 virtual로 선언되어 있었기 때문이다. override는 그 위에 안전하게 덧붙인 보증 장치일 뿐이다.
+) override의 역할
자식 클래스가 Print()를 다시 정의하면서 override를 붙이면, 컴파일러가 이 함수가 부모의 virtual 함수와 정확히 매칭되는지 검사한다. override가 있어도 실제로 중요한 건 부모가 virtual을 붙어있지 않으면 override 되지 않는다.