함수 오버라이딩 (Function Overriding)
파생 클래스에서 기반 클래스의 함수와 동일한 이름의 함수를 정의하고 사용해야 할 수 있다. 이때 기반 클래스의 함수가 호출되면 의도한 함수가 아니라 다른 함수가 호출되는 것이므로 문제가 발생할 수 있고, 이를 방지해야 한다.
예를 들어 아래와 같은 코드가 있다고 가정하자.
#include <iostream>
using namespace std;
class Base {
public:
void f() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
void f() {
cout << "Derived" << endl;
}
};
int main() {
Derived d;
Base* b;
b = &d;
d.f();
b->f();
return 0;
}
출력은 다음과 같다.
Derived
Base
업캐스팅을 진행하면서 기반 클래스를 가리키게 되므로 업캐스팅된 포인터가 멤버 함수 호출시 기반 클래스의 멤버 함수가 호출되었다.
그런데 앞서 언급한 것과 같이 업캐스팅을 하더라도 파생 클래스에서 재정의된 함수를 사용하고 싶을 수 있다. 이때 virtual
키워드가 사용된다. 앞선 코드를 아래와 같이 수정하자.
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
virtual void f() {
cout << "Derived" << endl;
}
};
int main() {
Derived d;
Base* b;
b = &d;
d.f();
b->f();
return 0;
}
그렇다면 출력은 아래와 같이 의도한대로 나온다.
Derived
Derived
이때 virtual
로 재정의된 함수를 가상 함수(virtual function)이라 하고, 이렇게 가상 함수로 함수를 재정의하는 것을 함수 오버라이딩이라 한다.
오버라이딩을 하려면 함수의 매개변수와 이름, 리턴 타입이 모두 일치해야 한다. 또한 파생 클래스에서는 virtual
키워드가 생략될 수 있다. 상속이 반복되더라도 기반 클래스의 함수가 virtual
로 정의되어 있다면 파생 클래스 모두에서 생략할 수 있다.
virtual
은 동적 바인딩(dynamic binding) 지시어로 컴파일러에게 함수에 대한 호출 바인딩을 실행 시간까지 미루도록 지시한다. 이를 통해 실행 시점에 알맞는 함수를 실행시킬 수 있는 것이다.
오버라이딩을 통해서 기반 클래스에 있는 멤버 함수를 파생 클래스마다 다르게 정의하여 사용할 수 있고, 이 파생 클래스의 객체들을 업캐스팅하여 사용할 수 있다.
범위 지정 연산자
만약 파생 클래스에서 기반 클래스의 함수를 그대로 사용해야 한다면 범위 지정 연산자 ::
을 통해 사용할 수 있다. 위 코드에서는 아래와 같이 사용하는 것이다.
class Derived : public Base {
public:
void f() {
Base::f();
cout << "Derived" << endl;
}
};
가상 소멸자
소멸자를 virtual
키워드로 선언하는 것을 말한다. 소멸자 호출시 동적 바인딩이 발생한다.
예를 들어 아래와 같은 코드가 있다고 가정하자.
#include <iostream>
using namespace std;
class Base {
public:
~Base() {
cout << "~Base()" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "~Derived()" << endl;
}
};
int main() {
Derived *dp = new Derived();
Base *bp = new Derived();
delete dp;
delete bp;
}
그렇다면 출력은 아래와 같다.
~Derived()
~Base()
~Base()
그런데 업캐스팅하여 사용한 객체에 대해서는 파생 클래스의 소멸자가 실행되지 않았다. 이럴 때 소멸자를 가상 함수로 선언하여, 즉 virtual
키워드를 사용하여 업캐스팅한 객체에 대해서도 파생 클래스의 소멸자가 실행되도록 만들어줄 수 있다.
코드를 아래와 같이 수정하자.
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() {
cout << "~Base()" << endl;
}
};
class Derived : public Base {
public:
virtual ~Derived() {
cout << "~Derived()" << endl;
}
};
int main() {
Derived *dp = new Derived();
Base *bp = new Derived();
delete dp;
delete bp;
}
그렇다면 출력은 아래와 같다.
~Derived()
~Base()
~Derived()
~Base()
즉 의도한 대로 파생 클래스의 소멸자 역시 실행되는 것을 확인할 수 있다.
추상 클래스
그런데 생각해보면 어떤 함수가 파생 클래스를 통해 재정의되어 사용된다면 기반 클래스에서 해당 함수를 구현할 필요가 없을 수 있다. 즉 기반 클래스의 해당 함수를 직접적으로 사용할 일이 없고, 파생 클래스의 객체를 업캐스팅하여 사용할 때 함수 오버라이딩을 위해서만 기반 클래스의 함수가 선언되어야 한다면 굳이 구현할 필요가 없다.
이때 순수 가상 함수(pure virtual function)을 선언하여 사용하는데, 이는 함수의 코드가 없고 선언만 있는 가상 멤버 함수이다.
선언은 다음과 같다.
virtual return_type function_name(parameter) = 0;
즉 함수를 선언할 때 뒤에 =0;
을 붙이면 된다.
만약 어떤 클래스가 최소 하나의 순수 가상 함수를 가진다면 이를 추상 클래스(abstract class)라 한다. 물론 모든 멤버 함수를 순수 가상 함수로 선언할 필요는 없다.
추상 클래스는 온전한 클래스가 아니므로 객체 생성이 불가능하다. 포인터만 선언 가능한데, 때문에 업캐스팅하여서만 사용할 수 있다. 즉 추상 클래스는 파생 클래스를 업캐스팅하여 사용하는 목적으로만 사용할 때 활용할 수 있다.
추상 클래스를 상속받은 파생 클래스 역시 추상 클래스의 순수 가상 함수를 구현하지 않는다면 추상 클래스 취급이 되어 객체 생성이 불가능하니 주의해야 한다. 즉 추상 클래스를 상속받은 클래스는 객체를 선언하기 위해서는 순수 가상 함수를 구현해야 한다.
'Language > C & C++' 카테고리의 다른 글
[C++] 표준 템플릿 라이브러리(STL) (0) | 2024.11.14 |
---|---|
[C++] 템플릿(template)을 통한 일반화(generic) (0) | 2024.11.13 |
[C++] 업캐스팅(upcasting) 및 다운캐스팅(downcasting) (0) | 2024.11.13 |
[C++] 클래스 상속(inheritance) 및 다중 상속 (0) | 2024.11.01 |
[C++] 연산자 오버로딩(operator overloading) (0) | 2024.10.27 |