Language/C++

11.다형성

아무키 2023. 12. 25. 08:00
반응형

 다형성 (Polymorphism) 

 

정적 바인딩(Compile-time) (함수 오버로딩, 연산자 오버로딩)

동적 바인딩(Run-time)

 

런타임 다형성

런타임에서 같은 함수에 대해 다른 의미를 부여 ← 함수의 오버라이딩

추상화된 프로그래밍을 가능하게 함

C++에서 런타임 다형성의 구현을 위해 아래와 같은 조건이 필요

상속

기본 클래스 포인터 또는 참조자

가상 함수

 

다형성과 동적바인딩

정적 바인딩은 컴파일시 타입을 기준으로 호출 함수를 결정하지만, 동적 바인딩은 런타임시 실제 메모리에 저장된 타입을 기준으로 호출 함수를 결정

 

가상 함수

Move()와 같이, 유도 클래스에서 기본 클래스의 함수를 재정의 또는 오버라이드해 사용할 수 있음

오버라이드된 함수는 동적 바인딩을 통해 활용 가능

오버라이드될 수 있는 함수를 가상함수라고 함

→ 기본 클래스의 함수(Entity::Move)가 가상함수로 선언 후, 유도 클래스에서 해당 함수를 오버라이드해서 구현하면 동적 바인딩 됨

 

C++에서 동적 바인딩(런타임 다형성)의 구현을 위해 아래와 같이 조건이 필요

상속

기본 클래스 포인터 또는 참조자

가상 함수

 

가상함수(Virtual Function)

 

가상 함수의 선언, 기본 클래스에서 할일

오버라이드 할 함수를 기본 클래스에서 virtual로 선언 해주어야함

상속 계층구조에 있는 모든 해당 함수는 가상 함수가 됨

class Entity{
	public:
		virtual void Move(int dx, int dy);
		...
};
// Ex 01

#incude <iostream>

class Entity{
	protected:
		int x, y;
	public:
		Entity(int x, int y)
			:x{x}, y{y}{}
		virtual void Move(int dx, int dy){
			x += dx;
			y += dy;
		}
		void PrintPosition(){
			std::cout << x << ", " << y << endl;
		}
};


class Player : public Entity{
	private:
		int hp;
		int xp;
	public:
		Player(int x, int y, int hp, int xp)
			:Entity{x, y}, hp{hp}, xp{xp}{}
	virtual void Move(int dx, int dy){ 
// 반환형, 함수 이름, 인자 수 같아야 override가능
// 유도클래스 virtual 생략 가능
		x += dx * 2;
		y += dy * 2;
		
int main(){
		Player e{1,1,10,10};
		e.PrintPosition();
		
		e.Move(2, 1);
		e.PrintPosition();

 

오버라이드 할 함수를 유도 클래스에서 구현

함수 원형(prototype)과 반환형이 기본 클래스의 가상함수와 일치해야 함

유도 클래스의 함수에서는 virtual 키워드를 넣지 않아도 되지만, 혼동을 피하기 위해 명시해주는 것을 권고

유도 클래스에서 함수를 오버라이드하지 않으면, 기존과 같이 기본 클래스의 함수가 상속됨

class Player : public Entity{
	public:
		virtual void Move(int dx, int dy){
		...
}

 

가상 소멸자 (Virtual Destructor)

다형성 객체를 소멸할 때의 고려사항

→ 포인터를 사용한 뒤 해제할 때, 소멸자가 정적 바인딩되어 있다면 기본 클래스의 소멸자가 호출됨

class Entity{
	private:
		int x, y;
	public:
		Entity(int x, int y)
			: x{x}, y{y}{}
		~Entity(){
			std::cout << "Entity Destructor" << std::endl;
		}
};

class Player : public Entity{
	private:
		int hp;
	public:
		Player(int x, int y, int hp){
			:Entity{x, y}, hp {hp}, {}
		~Player(){
			std::cout << "Player Destructor" << std::endl;
		}
};

int main(){
		Entity* ptr = new Player{2, 3, 5};
		//use ptr

		delete ptr;
};

 

유도 객체를 올바른 순서로, 올바른 소멸자를 사용해 소멸시키는 방법이 필요

해결 방법 → 가상 소멸자

클래스가 가상 함수를 가지면, 항상 가상 소멸자를 함께 정의해야 함

→ 마찬가지로, 기본 클래스의 소멸자가 가상 소멸자면, 유도 클래스의 소멸자도 가상 소멸자

class Entity{
	public:
		virtual ~Entity();
		...
};

//앞 코드에서 Entity의 소멸자를 가상으로 선언해주면 Player의
//소멸자와 Entity의 소멸자가 모두 호출된다.

 

기본 클래스의 참조자를 활용한 다형성

Entity e(0, 0);
Entity &ref = e;
ref.Move(1,1) // Entity::Move()

Player p{0, 0, 2};
Entity &ref2 = p;
ref2.Move(1,1); //Player::Move()

//ex2

void MoveX(Entity& e, int dx){
	e.Move(dx,0);
}

Entity e{0, 0};
MoveX(e, 1); //Entity::Move()

Player  p{0, 0, 2};
MoveX(p, 1); //Player::Move()

 

Override 지정자 (Override Specifier)

 

기본 클래스의 가상 함수는 오버라이드 가능하다.

오버라이드를 위해서는 함수의 원형과 반환형이 동일해야 한다.

→ 만일 다르다면, 오버라이드가 아닌 “재정의”가 된다.

(서로 다른 별개 함수로 인식)

→ 재정의는 정적 바인딩

C++11부터 override 지정자 기능을 제공하여 오버라이딩시 실수를 방지하고, 코드의 가독성을 상승 가능

→ 어떤 함수가 오버라이딩된 함수인지 정의만 보고도 파악 가능하다.

 

override 지정자가 필요한 이유

class Base{
	public:
		virtual void say_hello() const{
			cout << "Hello, im base" << endl;
		}
		virtual ~Base(){}
};

class Derived : public Base{
	public:
		virtual void say_hello(){ //유도 클래스 const 빼먹음
			cout << "Hello, im derived" << endl;
		}
		virtual ~Derived(){}
};

//의도한 오버라이드인가 재정의인가 알수 없다.
//virtual void say_hello() const override로

 

final 지정자 (final Specifier)

 

C++11 부터 final 지정자 기능을 제공

클래스의 final

→ 클래스를 더 이상 상속하지 못하도록 함

class Base final{
};

class Derived : public Base{ //error
} 

// 더 이상 상속하지 못하도록 final 명시함

 

멤버 함수의 final

→ 유도 클래스에서 가상 함수를 오버라이드 하지 못하도록 함

class A{
	public:
		virtual void do_somthing():
};

class B : public A{
	public:
		virtual void do_somthing() override final;
};

class C : public B{ //error
	public:
		virtual void do_somthing();
};

 

추상 클래스 (Abstract Class)

객체를 생성할 수 없는 클래스

상속 계층구조에서 기본 클래스로 사용됨

아주 일반적이어서 객체를 생성하기엔 맞지 않다.

→ Entity(무슨 Entity?), Account(어떤 통장?)

구상 클래스 (Concrete Class)

객체를 생성할 수 있는 클래스

모든 멤버 함수가 구현되어 있어야 함

→ 지금까지 예시로 모든 클래스는 구상 클래스

 

추상 클래스는 하나 이상의 “순수 가상 함수”를 갖는다.

즉 “순수 가상 함수”가 있는 클래스는 추상 클래스이다.

멤버 함수의 선언 뒤에 “=0”을 붙이면 순수 가상 함수가 됨

virtual void function() = 0;

 

”순수 가상 함수”가 있는 클래스는 추상 클래스이다.

유도 클래스들은 반드시 기본 클래스의 순수 가상함수를 오버라이드 해야함

→ 오버라이드 하지 않는 경우, 유도 클래스도 추상 클래스로 간주됨

→ 즉, 유도 클래스에서 특정 함수 구현을 “강제”하는 의미를 가짐

 

사용 목적

기본 클래스에서 구현이 적절하지 않은 경우

유도 클래스에서는 반드시 구현해야 함을 명시하기 위해

 

ex) 모든 entity는 x, y 좌표를 가지고 있고 이동이 가능함. 그러나 실제로 게임 내에서 표현되는 객체가 되려면 어떤 로직으로 이동이 가능한지 구체적 기능이 필요함

→ 그래서 Player객체, Enemy 객체가 이동이 가능하려면, Move() 함수를 반드시 오버라이딩 해야함을 강제하기 위해 Move()를 순수 가상 함수로 구현함

 

순수 가상 함수와 추상 클래스 예시

class Shape{ //Abstract
	private:
		//member variables
	public:
		virtual void draw() = 0; //순수 가상 함수들이 있으니, 추상 클래스
		virtual void roate() = 0;
		virtual ~Shape(); //순수 가상 소멸자가 필요할 때도 있을까?
		...
};

class Circle : public Shape{
	private:
		// member variables for circle
	public:
		virtual void draw() override{ //가상함수들을 오버라이드하여 구현
			//implementation for circle
		}
		virtual void rotate() override{ // 구상 클래스가 됨
			//implementation for circle
		}
		virtual ~Circle();
};

 

추상 클래스는 객체를 생성할 수 없음

Shape shape; //error
Shape *ptr = new Shape; //error

 

하지만 여전히 추상(기본) 클래스의 포인터 / 참조자를 사용해 오버라이딩된 함수를 동적 바인딩 할 수 있음

Shape *ptr = new Circle();
ptr -> draw();
ptr -> ratate();

 

 

추상 클래스를 사용한 인터페이스 클래스 (Abstract Classes and Interface)

 

순수 가상 함수만을 가진 추상 클래스를 인터페이스 클래스라고 한다.

→ C#, Java언어는 따로 Interface라는 키워드로 인터페이스 구현이 가능

클래스의 사용에 있어서 일반적인 기능(서비스)을 묶어놓은 클래스

인터페이스 클래스를 기반으로 한 구상 클래스는 모든 기능(함수 / 서비스)를 구현해야 한다.

인터페이스는 껍데기?

유도 클래스가 꼭 가져야 하는 기능들을 명시해 놓기 위해서 사용

 

추상 클래스를 사용한 인터페이스 클래스 예시

class IShape{ //abstract, interface
	public:
		virtual void draw() = 0;
		virtual void rotate() = 0;
		virtual ~IShape(); //순수 가상함수만을 가진 인터페이스
};

class Circle : public IShape{
	public:
		virtual void draw() override{
			//implementation for circle
		} //인터페이스 클래스를 상속한 구상 클래스
		virtual void rotate() override{
			//implementation for circle
		}
		virtual ~Circle();
		...
};

 

통상적으로 인터페이스 클래스의 이름을 지을 때는 대문자 I를 앞에 붙임

반응형