본문 바로가기
C++/기초

C++ 기초 : 다형성 (2)

글: 시플마 2024. 5. 4.

 

동물이라는 부모 클래스를 만들었습니다.

 

그리고 이를 상속받은 사자, 인간, 독수리 등의

자식 클래스를 만들었죠.

 

 

이때 동물이라는 부모 클래스로 만든 포인터는

사자, 인간, 독수리라는 자식 클래스로 만든

객체를 가리킬 수 있습니다.

 

 

하지만 각 객체의 앞에 존재하는 부모 클래스인 동물이라는 곳까지만

접근이 가능하기 때문에 동물 클래스로 만든 포인터는 자신이 가리키고 있는

객체가 어떤 자식 클래스로 만든 객체인지 알 수가 없습니다.

 

그래서 동물 클래스로 만든 포인터로 오버라이딩된 함수를

호출하면 무조건 동물 클래스에 있는 함수가 호출됩니다.

 

이러면 자식 클래스로 만든 객체라면 무엇이든

가리킬 수 있는 동물 포인터의 의미가 없어지는 것이죠.

 

 

이러한 문제점을 보완해 주는 것이 가상 함수이죠.

 

부모 클래스로 가서 오버라이딩된 함수 앞에

virtual이라는 키워드를 붙여 해당 함수를 가상 함수로

선언할 수 있습니다.

 

이 경우 동물 클래스로 만든 포인터로 

사자라는 자식 클래스로 만든 객체를 가리키고

오버라이딩된 함수를 호출한다고 할 때, 동물 클래스의

함수가 아닌 사자 클래스의 함수가 호출이 되죠.

 

 

아래 코드를 보시죠.

 

동물이라는 부모 클래스를 만들고

이를 상속받은 사자라는 자식 클래스를

만들었습니다. 각 클래스에는 

정보를 출력하는 Output이라는 함수가

구현되어어 있죠. 두 클래스의 Output 함수는

함수명과 인자가 서로 같은 형태이기

때문에 함수가 오버라이딩된 상태입니다.

 

 

이 상태에서 자식 클래스를 통해

사자 객체를 만들고 부모 클래스를 통해

동물 포인터를 만들었습니다.

 

이후 동물 포인터로 사자의 주솟값을 받아

사자를 가리키게 하였죠.

 

현재 상황을 그림으로 나타내면

 

위 그림과 같습니다. 

 

동물 포인터를 통해 가리키고 있는

객체, 사자에 접근하여 Output 함수를

호출하였습니다. 

 

이러면 동물 포인터가 실제로 가리키는

객체는 사자이지만, 동물 포인터는

가리키는 데이터를 동물로 인지하기 때문에

동물 클래스에 있는 Output 함수가 호출됩니다.

 

 

 

 


 

 

 

 

이때 동물 클래스에 

 

virtual 키워드를 붙입니다.

 

 

그러고 나서 실행해 보면

 

사자 클래스의 Output 함수가

호출된 것을 확인할 수 있습니다.

 

 

가상 함수의 원리는 무엇일까요?

 

위 그림처럼 동물 포인터가 가리키는

객체에 접근하여 Output 함수를 호출할 때,

호출하려는 함수가 virtual로 선언되어 있다면

가상 함수 테이블이라는 곳을 확인합니다.

그곳에 있는 Output 함수를 호출하게 되죠. 

 

동물 포인터로 접근한 객체가 사자 객체이므로

사자 객체의 가상 함수 테이블을 확인하겠죠?

 

사가 객체의 가상 함수 테이블이므로

사자 클래스에 있는 Output 함수를

호출하게 될 것입니다.

 

 

사자 객체의 부모 클래스인

동물 클래스 부분을 보면

 

__vfptr라는 것이 있는데

이것이 가상 함수 테이블입니다.

 

가상 함수 테이블을 확인해 보니

사자 클래스에 있는 Output 함수의

주솟값이 있는 것이 보이시나요?

동물 포인터로 사자 객체의 Output 함수를 호출하면

바로 여기에 적혀 있는 함수를 호출하게 되는 것이죠.

 

 

아래 코드처럼

 

동물 포인터로 동물 객체의 virtual로 선언된

Output 함수를 호출할 때에도 마찬가지로

가상 함수 테이블을 확인하여 그곳에 적혀 있는

Output 함수를 호출하겠죠.

 

동물 객체이므로 

가상 함수 테이블에는 동물 클래스에 있는

Output 함수의 주솟값이 적혀 있습니다.

 

그렇기 때문에 동물 클래스에 있는

Output 함수를 호출하게 되겠죠.

 

 

그림으로 표현하면

 

위 그림과 같겠죠.

 

 

 

 


 

 

 

 

 

사자 객체를 동물 포인터가 가리키는 상황에서도

동물 포인터를 통해 virtual로 선언되고 오버라이딩된(상속된)

사자 객체의 함수를 호출할 수가 있었습니다. 

 

 

그럼 상속된 함수가 아닌 자식 클래스에

독자적으로 추가된 함수는 동물 포인터가

호출하는 것이 가능할까요?

 

아래 코드를 보시면

 

자식 클래스인 사자 클래스에만 lionFunc라는

함수를 구현하였습니다.

 

해당 함수는 부모 클래스인 동물 클래스에는 

존재하지 않죠.

 

그렇기 때문에 동물 포인터로 사자 객체를

가리키고 있어도 사자 객체만이 갖고 있는

lionFunc 함수를 호출할 수 없습니다.

 

 

그래도 방법은 있습니다.

 

부모 클래스에서 선언되지 않은, 오직 자식 클래스에서만

구현된 함수를 호출하고 싶을 때 부모 클래스에서

자식 클래스형으로 다운 캐스팅하여 호출할 수 있습니다.

 

 

44번째 줄에 보이는 것처럼

동물 포인터를 사자 포인터로 강제 형 변환하면

사자 클래스에 존재하는 lionFunc 함수를

호출할 수 있죠.

 

하지만 이렇게 강제 형 변환하여 접근하는 것은

위험합니다. 현재 상황은 복잡하지 않기 때문에

동물 포인터가 사자 객체를 가리키는 것을 알고 있습니다.

그래서 사자 포인터로 형 변환하여 사자 클래스 멤버 함수를

호출하였지만, 만약 동물 포인터가 중간에 동물 객체를 가리키게

된 후 lionFunc 함수를 호출하면 사자 포인터로 형 변환하여

확인하려고 해도 동물 객체에는 lionFunc 함수 자체가

존재하지 않아 문제가 발생할 수 있죠.

 

 

이러한 문제를 방지하기 위해 사용할 수 있는 것은

 

'dynamic_cast'입니다. 

 

현재 동물 객체를 가리키고 있는 

동물 포인터를 dynamic cast를 통해

사자 포인터로 변환시킨 후 

사자 포인터에 대입하였습니다.

 

그러나 동물 포인터가 현재 동물 객체를

가리키고 있기 때문에 사자 포인터로 

변환하는 것은 문제가 있죠.

 

dynamic cast가 좋은 점은 

변환에 실패할 경우 nullptr을 반환한다는 것입니다.

 

그래서 51번째 줄처럼 

dynamic cast를 통해 변환된 포인터를

받았을 때 이것이 nullptr이 아닐 경우에만

함수를 호출하는 식의 동작을 할 수 있는 것이죠.

 

현재는 사자 포인터에 nullptr이 대입되어 

53번째 줄에 있는 함수 호출이 실행되지 않겠죠?

 

 

사실 동물 포인터는 어떤 객체를 가리키는지 모릅니다.

어떤 객체이든지 동물 객체 부분밖에 볼 수 없기 때문이죠.

 

근데 어떻게 동물 포인터만을 통해 dynamic cast는

형 변환이 가능한지, 불가능한지 판단하여

nullptr을 반환하는 걸까요?

 

아래 그림은

 

자식 클래스인 사자와 호랑이 클래스를 통해

객체를 만든 상황입니다.

 

동물 클래스에는 Output이라는

virtual 함수가 구현되어 있어

이를 오버라이딩한 사자와 호랑이 객체는

가상 함수 테이블이 생겼습니다.

 

사자의 가상 함수 테이블에 가면

사자 클래스의 정보가 들어있고,

호랑이 가상 함수 테이블에 가면

호랑이 클래스의 정보가 들어있으니

가상 함수 테이블을 통해 해당 객체가

무슨 타입인지 알 수 있겠죠?

 

그래서 동물 포인터만으로는 형 변환이 

가능한지 알 수 없지만 그 객체가 갖고 있는

가상 함수 테이블을 확인해 보면

객체가 어떤 타입인지 알 수 있기 때문에

dynamic cast를 통해 형 변환 여부를 판단할 수 있고

실패할 경우 nullptr을 반환받을 수 있는 겁니다.

 

 

근데 동물 포인터는 변수이죠?

 

변수라는 것은 런타임 중 언제든지 값이

바뀔 수 있습니다. 컴파일 단계에서는 동물 포인터가

사자 객체를 가리키고 있어 사자 포인터로 변환할 수 

있었는데 프로그램이 진행되면서 동물 포인터가

동물 객체를 가리키게 되어 사자 포인터로 변환할 수 

없는 상황이 되었습니다. 이런 경우는 어떻게 될까요?

 

dynamic cast이라는 이름 자체가 동적 캐스트라는 의미죠?

즉 런타임 중에도 계속 타입을 확인하며

변환의 결과를 변환해 주는 것이죠.

 

이러한 메커니즘을 RTTI(Run Time Type Identification or Information)

라고 합니다. 런타임 중 타입의 정보를 얻어올 수 있는 메커니즘이죠.

 

 

이러한 메커니즘 덕분에 런타임 중 동물 포인터가

동물 객체를 가리키게 되어 사자 포인터로 변환할 수 

없게 되었을 때 nullptr을 반환해줄 수 있는 겁니다.

 

 


강의 출처 : https://www.youtube.com/watch?v=ehjDBtql5f8&list=PL4SIC1d_ab-aOxWPucn31NHkQvNPHK1D1&index=85


'C++ > 기초' 카테고리의 다른 글

C++ 기초 : 다형성 (1)  (0) 2024.05.04
C++ 기초 : 상속 (2)  (0) 2024.05.04
C++ 기초 : 오버라이딩  (0) 2024.05.03
C++ 기초 : 상속 (1)  (0) 2024.05.03
C++ 기초 : tree (10)  (0) 2024.05.03