다형성 (4) - overrider & Final 지정자
override Specifier
◆ override 지정자
● (복습) 기본 클래스의 가상 함수는 오버라이드 가능하다.
● (복습) 오버라이드를 위해서는 함수의 원형과 반환형이 동일해야 한다.
☞ 만일 다르다면, 오버라이드가 아닌 "재정의"가 되어버린다. (서로 다른 별개의 함수로 인식)
☞ 재정의는 정작 바인딩
● C++ 11부터 override 지정자 기능을 제공하여 오버라이딩 시 실수를 방지하고, 코드의 가독성을 상승할 수 있다.
☞ 어떤 함수가 오버라이딩 된 함수인지 정의만 보고도 파악 가능
복습 겸 기존 개념을 다시 설명한다.
기본 클래스의 가상 함수는 오버라이드가 가능하며
오버라이드를 하기 위해서는 함수의 원형 반환형이 동일해야 한다.
기본 클래스에 있는 Move() 함수를 가상함수로 만들었다.
Entity::virtual void Move(int dx, int dy);
그 후 Player 클래스에서 오버라이드를 하려고 한다.
Player 클래스에 Move() 함수를 오버라이드 하려는 이유가 뭐냐면
Player 가 움직일 때는 Entity와 다르게 움직이기 위해서이다.
Entity::Move()는 dx , dy 주어진 만큼 움직이는데 반면
Player::Move()는 움직이는 로직이 다르다. dx와 dy의 2배만큼 움직인다.
예를 들어 Player::MovePlayer()라는 함수를 추가했다고 가정해 보자
class Entity {
protected:
int x;
int y;
public:
Entity(int x, int y)
:x{ x }, y{ y } { }
~Entity()
{
cout << "Entity Destructor" << endl;
}
virtual void Move(int dx, int dy) {
x += dx;
y += dy;
}
void PrintPosition()
{
cout << x << ", " << y << endl;
}
};
class Player : public Entity {
private:
int hp;
public:
Player(int x, int y, int hp)
:Entity{x,y},hp{hp} { }
virtual void Move(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
void MovePlayer(int dx, int dy) // 멤버 함수 추가
{
x += dx * 2;
y += dy * 2;
}
virtual ~Player()
{
cout << "Player Destructor" << endl;
}
};
int main()
{
Entity* e = new Player{ 1,1,10 };
e->MovePlayer(); // 성립되지 않는다.
}
e->MovePlayer(); 를 사용할 수 없다.
이유는 Entity* e = new Player { 1,1,10 }; 은 정적바인딩 상태이기 때문이다.
오버라이딩을 안 했으니 MovePlayer() 함수를 사용할 수 없다.
int main()
{
Player p{ 1,1,10 };
p.MovePlayer(2, 2);
}
이렇게 구현은 가능하다.
Player::MovePlayer() 함수는 Entity::Move()와 별개의 함수가 된다.
객체들을 하나의 기본 클래스 포인터로 만들어서 배열로 저장하여 for 문을 돌리는 것 할 수 없게 된다.
소프트웨어를 설계할 때 하고자 하는 기능이 있으면 오버라이딩하여 동적바인딩을 하면 된다.
오버라이딩 하려면 먼저 가상함수부터 만들어줘야 하고
유도클래스에서는 동일한 함수 이름과 인자 반환형이 있어야 한다.
그리고 유도 클래스의 가상함수에도 virtual로 적어주면 좋다.
만일 함수의 인자나 반환형이 달라 오버라이딩을 하지 못한 경우 "재정의"라고 부른다.
Entity::void Move(int dx, int dy); , Player::void Move(int dx); 인 경우 서로 다른 별개의 함수로 인식한다.
재정의는 정적 바인딩이다.
C++ 버전이 11보다 낮을 경우 에러가 나올 수 있지만
대부분 사용하는 C++ 11 이상 버전인 경우
override 지정자 기능을 사용하여 오버라이딩 시 실수를 방지한다.
또한 코드의 가독성을 상승시켜주어 어떤 함수가 오버라이딩 된 함수인지 정의만 보고 파악이 가능하다.
◆ override 지정자가 필요한 이유
class Base {
public:
virtual void sayHello() const {
cout << "Hello, I'm Base" << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
virtual void sayHello() {
cout << "Hello I`m Derived" << endl;
}
virtual ~Derived() {}
};
* 유도 클래스에 실수로 const를 빼먹은 경우에
오버라이드가 아닌 재정의된 함수가 되었다.
* 사용자의 의도( 오버라이드? 재정의? )를 컴파일러는 알 수 없기 때문에
컴파일 에러가 발생하지 않음.
● 재정의 되었기에, 아래와 같이 의도하지 않게 동작함
int main()
{
Base* p1 = new Base();
p1->sayHello(); // "Hello, I'm Base"
Base* p2 = new Derived();
p2->sayHello(); // "Hello, I'm Base"
}
강사님이 설명해준 코드는 하기에 작성한다.
#include <iostream>
using namespace std;
class Entity {
protected:
int x;
int y;
public:
Entity(int x, int y)
:x{ x }, y{ y } { }
~Entity()
{
cout << "Entity Destructor" << endl;
}
virtual void Move(int dx, int dy) {
x += dx;
y += dy;
}
virtual void PrintPosition()
{
cout << "Entity" << x << ", " << y << endl;
}
};
class Player : public Entity {
private:
int hp;
public:
Player(int x, int y, int hp)
:Entity{x,y},hp{hp} { }
virtual void Move(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
void MovePlayer(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
virtual ~Player()
{
cout << "Player Destructor" << endl;
}
virtual void PrintPosition()
{
cout << "Player: " << x << ", " << y << endl;
}
};
int main()
{
Entity* e = new Player{ 1,1,10 };
e->PrintPosition(); // Player : 1 , 1
}
int main()
{
Player p{ 1,1,10 };
const Entity& e = p;
e.PrintPosition(); // e 에 Error Message 뜬다.
}
e는 const로 선언이 되어있기 때문에 e에서 멤버 함수를 호출했을 때 값이 바뀌면 안 된다.
PrintPosition() 함수는 값을 변하지 않는 코드이지만 컴파일러에게 알려주기 위해서는
PrintPosition() 함수 앞에 const 선언을 해야 한다. (지난 oop 파트에서 배운 내용 복습)
그렇기에 Entity::virtual void PrintPosition() const로 변경되어야 하며
Player::virtual void PrintPosition() const로 변경해서 제대로 된 오버라이딩을 할 수 있게
함수 이름, 인자, 반환형, 지정자 까지 동일하게 맞춰주어야 한다.
만일 실수로 Player::virtual void PrintPosition() 선언하여 const 지정자를 뺀 경우
Entity::virtual void PrintPosition() const와 다른 함수가 되어서
오버라이딩이 되지 않았음을 기억하자.
#include <iostream>
using namespace std;
class Entity {
protected:
int x;
int y;
public:
Entity(int x, int y)
:x{ x }, y{ y } { }
~Entity()
{
cout << "Entity Destructor" << endl;
}
virtual void Move(int dx, int dy) {
x += dx;
y += dy;
}
virtual void PrintPosition() const
{
cout << "Entity :" << x << ", " << y << endl;
}
};
class Player : public Entity {
private:
int hp;
public:
Player(int x, int y, int hp)
:Entity{x,y},hp{hp} { }
virtual void Move(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
void MovePlayer(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
virtual ~Player()
{
cout << "Player Destructor" << endl;
}
virtual void PrintPosition()
{
cout << "Player: " << x << ", " << y << endl;
}
};
int main()
{
Player p{ 1,1,10 };
const Entity& e = p;
e.PrintPosition();
// Entity : 1, 1
}
오버라이딩 된 코드가 아니라서 Entity : 1,1 이 나왔다. 오버라이딩을 해야 하는 경우 Player::PrintPosistion() 함수에도 const 지정자를 넣어야 한다.
#include <iostream>
using namespace std;
class Entity {
protected:
int x;
int y;
public:
Entity(int x, int y)
:x{ x }, y{ y } { }
~Entity()
{
cout << "Entity Destructor" << endl;
}
virtual void Move(int dx, int dy) {
x += dx;
y += dy;
}
virtual void PrintPosition() const
{
cout << "Entity :" << x << ", " << y << endl;
}
};
class Player : public Entity {
private:
int hp;
public:
Player(int x, int y, int hp)
:Entity{x,y},hp{hp} { }
virtual void Move(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
void MovePlayer(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
virtual ~Player()
{
cout << "Player Destructor" << endl;
}
virtual void PrintPosition() const
{
cout << "Player: " << x << ", " << y << endl;
}
};
int main()
{
Player p{ 1,1,10 };
const Entity& e = p;
e.PrintPosition();
// Player : 1, 1
}
실수하지 않기 위해서는 Player::virtual void PrintPosition() const override로 선언하여 override를 붙인다. 기본 클래스가 const로 선언된 멤버 함수라면 Player::virtual void PrintPosition() override 에는 Error 코드가 뜬다.
class Player : public Entity {
private:
int hp;
public:
Player(int x, int y, int hp)
:Entity{x,y},hp{hp} { }
virtual void Move(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
void MovePlayer(int dx, int dy)
{
x += dx * 2;
y += dy * 2;
}
virtual ~Player()
{
cout << "Player Destructor" << endl;
}
virtual void PrintPosition() override // ERROR 발생
{
cout << "Player: " << x << ", " << y << endl;
}
};
override 지정자가 기본 클래스에서 함수를 받아 오버라이딩 할 거라고 선언하기 때문에 기본 클래스의 함수 이름, 인자, 반환형, 지정자가 다를 경우 컴파일에러를 띄운다.
지금이야 코드가 간단해서 실행파일로 쉽게 오버라이딩 오류를 찾아서 수정할 수 있지만,
프로그램이 복잡해질수록 오류를 파악하기 어렵고 컴파일하고 실행하는데 까지만 해도 시간이 굉장히 오래 걸릴 수 있다.
그래서 미연에 방지를 해주는게 좋다. 그렇기에 override 지정자를 사용한다.
또 다른 이유는 PrintPosition() 멤버 함수를 파악하려면 시간이 걸린다.
override 지정자를 넣어준 경우에는 오버라이드 하는 함수를 정확하게 알 수 있어
코드의 가독성이 좋아진다.
◆ override 지정자
class Base {
public:
virtual void sayHello() const {
cout << "Hello, I'm Base" << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
virtual void sayHello() override { // 컴파일 오류
cout << "Hello I`m Derived" << endl;
}
virtual ~Derived() {}
};
* 사용자의 의도 (오버라이드)를 파악했기에, 함수의 원형이 일치하지 않는다는 컴파일 에러 발생
코드의 규모가 커질수록 정확한 오류 원인 파악이 어려움. 가능한 한 빨리 컴파일 단계에서부터 의도하지 않은 동작을 막아야 함
final Specifier
◆ final 지정자
● C++ 11부터 final 지정자 기능을 제공
● 클래스의 final
☞ 클래스를 더 이상 상속하지 못하게 함
● 멤버 함수의 final
☞ 유도 클래스에서 가상 함수를 오버라이드 하지 못하도록 함
클래스에서 배운것 처럼 클래스의 final 은 더 이상 상속을 하지 못하게 하고,
멤버 함수에서는 가상 함수를 오버라이드 하지 못하도록 한다.
● 클래스의 final
class Base final {
};
class Deriverd : public Base {
};
Error! Base를 더 이상 상속하지 못하도록 final로 명시함
: Public Base 부분에서 컴파일 에러가 난다.
● 멤버 함수의 final
class A {
public:
virtual void doSomething();
};
class B : public A {
public:
virtual void doSomething() final;
};
class C : public B {
public:
virtual void doSomething(); // 컴파일 Error
};
Class B의 doSomething() 오버라이드는 OK
Class C의 doSomething()는 컴파일 에러
B에서 doSomething()을 final로 명시하였으므로, C에서는 오버라이드 불가.
Polymorphism
◆ 다형성과 동적 바인딩 : 기본 동작인 정적 바인딩은 컴파일 시 타입을 기준으로 호출 함수를 결정하지만, 동적 바인딩을 하게 되면 런타임 시 실제 메모리에 저장된 타입을 기준으로 호출 함수를 결정
◆ 가상 함수 : 컴파일러에게 동적 바인딩을 하려고 알려주기 위해서는 기본 클래스의 함수를 virtual 키워드를 통해 가상 함수로 만들어주어야 한다. 이런 경우, 소멸자도 항상 가상 함수로 선언 필요하다.
◆ 기본 클래스의 포인터 / 참조자 : 동적 바인딩을 위해서는 기본 클래스의 포인터 / 참조자로도 유도 클래스를 가리키고 함수를 호출
◆ override / final 지정자 : override는 가상함수의 오버라이딩을 명시하여 실수 방지, final 은 더 이상 상속 / 오버라이드를 하지 못하도록 하는 키워드
◆ 순수 가상 함수와 추상 클래스
◆ 추상 클래스와 인터페이스
중요한 파트였다. override 지정자는 많이 사용하기에 알아두어야한다.
override 키워드는 가상함수를 오버라이딩 하고 있다는 것을 컴파일러에 명시하여 실수를 막아준다.
final 은 상속이나 오버라이드를 더이상 하지 않겠다는 의미이다.
추가적으로 멤버 함수에 const 지정자 여부에 따라서 오버라이드가 아니라 서로 다른 함수로 인식 될 수 있다.
https://youtu.be/ibzD79klgvg?si=WyBSfLfaE2HHkzuZ
공부하느냐 오랜 시간이 걸렸지만 기존에 배웠던 지정자를 이용하기 때문에 쉽게 이해하였다.