Polymorphism
◆ 다형성과 동적 바인딩
◆ 가상 함수
◆ 기본 클래스의 포인터 / 참조자
◆ override / final 지정자
◆ 순서 가상 함수와 추상 클래스
◆ 추상 클래스와 인터페이스
이번 강의는 객체 지향 프로그래밍(OOP)의 핵심 개념 중 하나인 다형성에 대해 다룬다.
이는 OOP Part 1,2, 그리고 상속에 이어지는 내용으로 이번 학기의 가장 중요한 주제이다.
다형성에 대해 공부해보자
문법 자체는 어렵지 않으나, 헷갈릴 수 있는 요소들이 있을 수 있어 헷갈린 부분을 중점적으로 설명할 것이며, 학생들도 이해하기 어려운 부분을 주의 깊게 확인하길 권장한다.
먼저 다형성과 동적 바인딩에 대해 알아보자.
동적 바인딩과 반대되는 개념이 정적 바인딩이다.
정적 바인딩은 지난 시간 마지막 부분에서 공부하였다.
첫번째 목표는 정적 바인딩과 동적 바인딩의 차이점을 알아두는 것이다.
중요한 Part이다.★★★★★
◆ 다형성
● 정적 바인딩 (Compile-time) (함수 오버로딩, 연산자 오버로딩)
● 동적 바인딩 (Run-time)
◆ 런타임 다형성
● 런타임에서 같은 함수에 대해 다른 의미를 부여함 => 함수의 오버라이딩
◆ 추상화된 프로그래밍을 가능하게 한다
◆ C++에서 런타임 다형성의 구현을 이해 아래와 같은 조건이 필요하다
● 상속
● 기본 클래스 포인터 또는 참조자
● 가상 함수
다형성 (Polymorphism) 이라고 하며, 어떤 특성을 이르는 단어이다.
개발자가 실제로 코드를 작성할 떄 해야 하는 것은 동적 바인딩이다.
동적 바인딩이란?
다형성은 객체 지향 프로그래밍의 핵심 특성 중 하나로, "여러 형태를 가질 수 있는 능력"을 의미한다.
- 다형성의 기본 개념
다형성은 같은 인터페이스를 사용하여 다양한 타입의 객체들이 각자 다른 방식으로 응답할 수 있게 해주는 기능이다. 동일한 함수 호출이 객체의 실제 타입에 따라 다른 동작을 수행할 수 있게 해 준다.
- C++에서 다형성의 종류
1. 컴파일 타임 다형성(정적 다형성)
● 함수 오버로딩 : 같은 이름의 함수를 매개변수 타입이나 개수를 다르게 하여 여러 버전으로 정의
● 연산자 오버로딩 : 연산자 (+,-,* 등) 의 동작을 사용자 정의 타입에 맞게 재정의
2. 런타임 다형성(동적 다형성)
● 가상 함수(virtual function): 부모 클래스에서 선언된 함수를 자식 클래스에서 재정의
● 순수 가상 함수(pure virtual function): 무혁이 없는 가상함수로, 파생 클래스에서 반드시 구현해야 한다.
정적 바인딩과 동적 바인딩에 대한 정리
먼저 지난 시간에 배운 정적 바인딩에 대해서 다시 한번 이야기를 해보자.
정적 바인딩(Static Binding)은 컴파일 타임에 어떤 함수를 호출할지 타입을 기준으로 결정되는 방식이다.
Entity e;
e.Move(); // Entity::Move()
상속 관계에서는
Entity* e1 = new Player(); // Entity 포인터에 Player 갤체 주소를 담을 수 있다.
e1.Move(); //Entity* 타입을 보고, Entity 클래스에 있는 Move() 멤버 함수를 호출해야 겠다고 결정
실제로 e1 이 가리키고 있는 주소값은 Player 객체 이지만
Entity* 타입을 보고 Entity 클래스에 있는 Move() 멤버 함수를 호출해한다.
이것이 정적 바인딩의 핵심이다.
"컴파일 타임"이라고 하는 이유는, 코드를 컴파일할 때 컴파일러가 코드를 해석하면서
e1.Move(); 를 보고 e1 의 타입을 본 후 Entity* 타입임을 확인한 후,
Entity 클래스 안에 있는 Move() 멤버 함수를 호출하도록 결정하기 때문이다.
동적 바인딩(Dynamic Binding)
동적 바인딩은 런타임(프로그램 실행 시)에 어떤 함수를 호출할지 결정하는 방식이다.
코드 실행 시 e1 변수에 Entity 객체의 주소가 들어있다면 Entity 의 Move() 함수가 실행되고,
Player 객체의 주소가 들어있다면 Player 의 Move() 함수가 실행된다
다형성과 바인딩의 연관성
런타임 다형성은 프로그램을 실행할 때 함수에 대해 다른 의미를 부여하는 성질을 말한다.
이는 주로 함수 오버라이딩을 통해 구현된다.
프로그램 실행 시에 따라 어떤 함수가 호출되는지 달라지는 것이 런타임 다형성의 특징이다.
런타임 다형성은 동적 바인딩을 통해 구현된다. 따라서 런타임 다형성과 동적 바인딩은 같은 개념으로 볼 수 있다.
정적 바인딩과 동적 바인딩에 차이를 리마인드하자.
C++에서 동적 바인딩 구현 조건
세 가지 조건을 만족해야 한다.
1. 상속 관계가 있어야 한다.
2. 기본 클래스의 포인터 또는 참조자를 사용해야 한다.
3. 함수가 가상함수로 선언되어야 한다.
이 세가지 조건이 모두 충족되어야 동적 바인딩이 가능해진다.
중요한 Part이다.★★★★★
◆ 정적 바인딩의 예시 1
int main()
{
Entity entity{ 0,0 };
entity.Move(1, 1); // Entity::Move()
Player player{ 0,0,2 };
player.Move(1, 1); //Player::Move()
Boss boss{ 0,0,2 };
boss.Move(1, 1); //Boss::Move()
Entity* ePtr = new Boss{ 0,0,2 };
ePtr->Move(1, 1); // Entity::Move()
}
* is-A 관계(타입을 두개 갖게 됨)를 생각해 보면, 가능한 명령문.
기본 클래스를 사용하는 곳에는 항상 유도 클래스도 사용 가능하다.
* 컴파일러는 ePtr의 선언 타입 (Entity*) 를 기준으로 호출할 함수를 미리 결정한다.
== 정적 바인딩
정적 바인딩의 예시를 보자.
그림을 보면 어떤 상속인지 확인할 수 있다.
Entity 클래스에 Move() 함수가 있고
화살표 (↑) 가 뜻하는 것은 Entity 클래스를 베이스 해서
Player 와 Enemy 클래스가 만들어졌다.
Player 와 Enemy 클래스도 Move() 함수를 가지고 있다.
Player 클래스 밑에 Move() 함수는 Entity 클래스의 Move() 를 상속한다는 의미가 아니고
Player 클래스에도 Move() 함수가 있다는 말이다.
한단계 더 있다. Enemy 클래스를 베이스로
Boss 클래스가 만들어졌다. Boss 클래스도 Move() 함수를 가지고 있다.
Boss 클래스는 Enemy 를 상속을 하였고, Enemy 는 Entity 를 상속하였다.
이렇게 클래스 하이라키(?) 를 구성을 하였고,
Entity entity{ 0,0 }; 을 선언하면
entity.Move(1, 1);
Entity 클래스에 있는 Move() 가 호출되었다.
이유는 C++ 의 디폴트는 정적 바인딩이기 때문이다.
정적 바인딩을 위한 3가지 조건을 충족하지 못하게 된다면 컴파일러는 컴파일 타임에 Move)() 함수를 타입을 기준으로 Move() 함수가 실행된다.
Entity* ePtr = new Boss{ 0,0,2 };
ePtr->Move(1, 1); // Entity::Move()
Entity 객체를 가리킬 수 있는 포인터를 만들었다. (Entity*)
실제로 가리킨 주소값은 Boss 클래스의 개체이다.
힙 메모리에 만들어져있다.
ePtr 안에 있는 메모리값은 Boss 클래스이지만
ePtr 의 타입은 Entity* (Entity의 포인터 타입) 으로 되어있다.
이 코드를 컴파일을 하면은 ePtr 의 Move() 는 Entity 의 Move() 함수로 실행이 된다.
Entity* ePtr = new Boss{ 0,0,2 };
왼쪽과 오른쪽의 타입이 다른데 가능한 문법이냐?
상속 관계에서는 Base 클래스로 Drived (유도 클래스를 영어로 Drived 라고 하나?) 클래스를 가리키는 건 가능하다.
상속을 받으면 두가지 속성을 가지는 것과 마찬가지이기 때문에
Base 클래스가 사용되는 곳에는 Drived 클래스도 항상 사용이 가능하다.
그것이 두가지 타입이 가능하다고 언급했던 부분이다.
한번 더 강조를 한다면
기본 클래스를 사용하는 곳에는 항상 유도 클래스도 사용이 가능하다.
컴파일러는 ePtr 의 타입이 Entity* 이기 때문에 Entity* 타입을 기준으로 호출할 함수를 미리 결정한다.
왜? C++ 은 기본적으로 정적 바인딩이 기본이기 때문에
별도의 작업을 해주지 않는 이상 정적 바인딩을 해서 타입을 보고 실행할 함수를 결정한다.
실제 메모리에 올라간 것은 Boss 이지만, ePtr 의 타입은 Entity 의 포인터 타입이다.
정적 바인딩인 경우에는 ePtr 의 타입의 Move() 함수를 결정한다.
중요한 Part이다.★★★★★
◆ 정적 바인딩의 예시 2
void DisplayPosition(const Entity& e)
{
e.ShowPosition(); // Entity::ShowPosition() called
}
Entity entity{ 0,0 };
DisplayPosition(entity);
Player player{ 0,0,2 };
DisplayPosition(player);
Enemy enemy{ 0,0,2 };
DisplayPosition(enemy);
* 컴파일러는 e의 선언 타입 (Entity&) 을 기준으로 호출할 함수를 미리 결정한다.
이번에는 다른 코드를 보자.
Entity 클래스는 Player , Enemy 클래스를 기반으로 하여 만들어졌고,
Enemy 클래스는 Boss 클래스를 기반으로 만들어졌다.
모든 클래스에는 ShowPosition() 이 있다.
void DisplayPosition(const Entity& e) 함수가 있다.
함수 인자에 const 에 Entity 참조자인 e 값을 받는다.
그럼 e 에 있는 ShowPosition() 멤버 함수를 가지고 있다.
이 함수를 호출할 경우 Entity 에 있는 ShowPosition() 함수가 호출이 된다.
e 가 Entity 타입이니깐 Entity 클래스에 있는 ShowPosition() 함수가 호출 된 것이다.
Enemy enemy{ 0,0,2 };
DisplayPosition(enemy);
이 코드에서 Enemy 객체를 만들고, DisplayPosition() 함수에
Enemy 객체를 인자로 넣었다.
void DisplayPosition(const Entity& e)
e 는 Enemy 인자로 전달이 된것이다.
이 코드도 가능하다.
앞서
Entity* ePtr = new Boss{ 0,0,2 };
ePtr->Move(1, 1); // Entity::Move()
이 코드와 동일한 이야기이다.
Entity 를 인자로 받는데도 , Enemy 객체를 넘겨줄 수 있다.
기본 클래스를 사용할 수 있는 객체는 유도 클래스도 사용할 수 있다.
e 객체에는 Player , Enemy , Boss 객체 모두 넘어올 수 있다.
e 객체가 다른 클래스의 객체여도 오직 기본 클래스 Entity 객체의 Move() 만 호출이 된다.
이유는 정적 바인딩이기 때문이다.
타입을 기준으로 호출할 함수를 결정하기 때문이다.
컴파일러는 e 의 선언 타입 (Entity&) 을 기준으로 호출 할 함수를 미리 결정한다.
https://youtu.be/9oieYn0IaQ4?si=RzSx-4SmjCUEsYOU
'C++ > C++ : Study' 카테고리의 다른 글
다형성 (3) - 기본 클래스 포인터 & 참조자 (0) | 2025.05.01 |
---|---|
다형성(2) - 동적 바인딩 & 가상 함수 (0) | 2025.05.01 |
상속(8) - 멤버의 사용 (0) | 2025.04.17 |
상속(7) - 복사 생성자 (1) | 2025.04.02 |
상속(6) - 기본 클래스 생성자에 인수 전달 (0) | 2025.04.01 |