Back-end/C++

22. 03. 30 - 가상 함수, 함수 포인터, 함수 배열

giggs 2022. 4. 7. 10:03

 

가상 함수 

  • 앞 시간에 배운 다형성을 구현하는 방법 중 Override는 다형성 구현을 위해
  • 상속의 관계에서 부모의 가상 함수를 자식 클래스에서 재정의 하는 방식을 이용하였다.
  • 어떤 타입의 객체가 들어올지 모르는 상태에서 
  • 함수를 정적 바인딩(컴파일 시에 결정)이 아닌
  • 동적 바인딩(함수 실행 시)으로 만들어 주기 위해서 가상 함수로 사용

 

 

 

가상 함수는 어떤 방식으로 동적 바인딩을 가능하게 만들어 주는 것일까? 

가상 함수의 특징과 작동원리에 대하여 실습을 통해 알아보자.

 

 

 


 

 

 

기본 세팅 

  • 부모 클래스에 함수 3개 만들기 ( 일반 함수 1개 가상 함수 2개)
  • 자식 클래스 2개 만들기 - ( 일반 함수 재정의 , 가상 함수 1개 재정의 )

 

 

 

 

 

부모 A클래스

  • 일반 함수 info ( )
  • 가상 함수 print( )
  • 가상 함수 print2( )

 

자식 B 클래스 : A 상속

  • 일반 함수 info( )
  • 가상 함수 print( )

 

자식 B 클래스 : A 상속

  • 일반 함수 info( )
  • 가상 함수 print2( )

 

 

 

 


 

 

 

 

3개의 함수 ( info , print , print2 )를  호출하는 함수를 만든다.

  • A& - 부모의 타입으로는 자식 a, b, c 다 올 수 있다
  • ref로 A타입 B타입 C타입 객체를 각각 넣어서 함수 호출 - 출력 테스트

 

 

 

 

 

 


 

 

 

각 타입별 함수 호출 - 출력 테스트

 

 

 

 

 

## Check Point

 

 

일반 함수 info( ) 는

  • 재정의 여부 상관없이 다 A :: info( ) 호출 
  • 일반 함수는 들어오는 데이터 타입에 상관없이 담고 있는 그릇의 데이터 타입에 맞춰 함수 호출
  • A타입의 객체를 받은 거라서( A상속받아 만든 B, C포함) A타입의 info ( ) 호출됨 

 

 

가상 함수 2개 중 - 재정의 된 것은 그 객체의 함수 호출

  • B 클래스에서는 Print( ) 재정의 ----> B :: Print 출력 확인
  • C 클래스에서는 print2( ) 재정의 ----> C :: Print2 출력 확인

 

 

가상 함수 2개 중 - 재정의 되지 않은 것은 부모의 것 그대로 호출

  • B 클래스에서는 Print2( ) 는 재정의하지 않았다. - A :: Print2 출력 확인
  • C 클래스에서는 Print( ) 는 재정의하지 않았다. - A :: Print 출력 확인

 

 

일반 함수는 자식 클래스에서 재정의되어있어도 부모의 것으로 출력되는 부분도 check  

 

 


 

 

직관적으로 비교하기 좋게 코드 순서를 조정해서 출력해 보았다.

 

 

일반함수를 자식이 재정의 한 의미는 - 재정의가 아닌 자식 클래스에서 새로운 함수를 정의 한 것이다.

 

 

 

A, B, C 타입 모두 일반 함수인 info ( )는

  • A 타입 A :: info( )으로 모두 출력됨을 확인

 

가상 함수가 재정의된 부분은

  • 그 데이터 타입에 맞춰 출력됨을 확인 - B :: print( )와 C :: print2 확인

 

가상 함수가 재정의되지 않은 부분은

  • 부모인 A타입의 것이 출력됨을 확인 - B객체 A :: print2( )와 C 객체 A :: print 확인

 

 

 

 


 

 

 

일반 함수 info( ) 호출 시

 

 

 

컴파일러는 일반함수인 info( ) 에 대하여 43번line의 의미를 - 44번 line 으로 받아들인다.

 

 

 

 

 

일반 함수는 들어오는 데이터 타입에 관계없이 그것을 담는 그릇 - A타입 -에 맞춰서 호출!

컴파일 시에 정해진다 - 정적 바인딩

 

 

 

 


 

 

 

가상 함수 print ( ) 호출 시

 

그릇에 담겨있는 데이터 타입에 준하여 작동

어디의 print 함수 호출될지 몰라서 컴파일 시에 바인딩 결정할 수 

유보한다 - 동적 바인딩

 

 

 

컴파일러는 가상 함수인 print( ) 에 대하여 49번line의 의미를 - 51번 line 으로 받아들인다.

 

 

 

 

클래스에 가상 함수가 하나라도 있을 경우

  • _vfptr 이라는 가상 변수가 ( 멤버 변수로 ) 자동으로 만들어짐 – 함수의 포인터 변수
  • _vfptrArray 라는 가상 함수 테이블이 자동으로 만들어짐 – 함수의 포인터 배열
  • 부모의 가상 함수 테이블에 저장되어있는 가상 함수가 2개였으므로
  • 상속받은 b, c클래스에도 가상 함수 테이블 안에 가상 함수 2개 만들어짐

 

 

 


 

 

정리

 

 

일반 함수는

  • 그릇의 데이터 타입의 함수 호출 – 고정
  • 인자로 A, B, C 타입 어떤 타입이 들어오든 간에 무조건 담는 그릇인 A 클래스 info( ) 호출!
  • 정적 바인딩 – 컴파일 타임(실행 파일을 만들 때)에 바인딩이 결정된다. 
  • ref . A :: info( ); - A에 있는 info 함수를 호출할거야라고 받아들인다.

 

 

가상 함수는

  • 그릇의 데이터 타입이 아닌 담겨있는 타입으로 호출
  • 재정의 되지 않았으면 부모의 함수 그대로 사용
  • 어떤 데이터 타입이 들어올 줄 모른다 - 어떤 데이터 타입의 info( ) 함수가 호출될지 모른다.
  • 동적 바인딩 - 컴파일 타임(실행 파일을 만들 때 ) 바인딩 결정 못한다. - 바인딩 유보한다. 

 

 

 

 

 


 

 

 

 

 

가상 함수의 작동원리

 

 

 

 

알기 위해서 먼저 함수 포인터와 함수 배열 Check

 

함수 포인터

  • 함수도 메모리에 위치한다 - 주소 값을 가지고 있다.
  • 함수명은 함수를 저장하고 있는 공간의 주소 값 그 자체
  • 주소 값 저장 가능하다.

 

함수명은 함수를 저장하고 있는 공간의 주소 값이다. - pfunc도 같은 개념

 

 

15번 Line  함수 포인터 변수 - 함수의 주소를 저장하는 변수 pfunc

15번 Line - 여기에 포인터 연산자( * )를 해주면 공간을 의미 - 그 공간에는 함수가 저장되어있다.

 

 

 

 

 


 

 

함수 포인터 배열

  • 함수 포인터들의 배열
  • 함수의 주소를 저장하고 있는 변수 pfunc들을 저장하고 있는 배열

 

 

 

 

 

21번 Line -- pfuncArray는 함수의 주소를 저장하고 있는 pfunc들이 위치하고 있는 배열

21번 Line -- 여기에 포인터 연산자( * )를 해주면 공간을 의미 - 그 공간에는 *pfunc 저장되어있다.

22번 Line -- 그 배열 [ 0 ] 번째에 add - 함수명(주소 값)을 [ 1 ] 번째에 sub - 함수명(주소 값)을 대입

( 함수명은 자체가 주소 값이므로 &생략 가능 – 써주면 좋긴 하다 )

25번 Line -- 배열 0번째에 있는 함수 -  주소 값으로 add 함수 호출!

26번 Line -- 배열 1번째에 있는 함수 - 주소 값으로 sub 함수 호출!

 

 

 

 

 


 

 

 

이처럼

  • 함수가 있고 - add( ) , sub( )
  • 함수의 주소를 저장하는 변수가 있고 - pfunc - 함수 포인터 변수
  • *pfunc ( 함수의 주소를 저장하는 변수 )를 배열의 형식으로 가지고 있는 테이블이 있고- *pfuncArray[ ]
  • 이 배열의 몇 번째 공간( * ) 호출 - 그 공간에 저장되어있는 함수 주소 값으로 - 함수 호출하는 것

 

 

이와 같은 형식으로! 

  • 가상 함수가 있고 - virtual void print( ) , virtual void print2( )
  • 함수의 주소를 저장하는 변수가 있고 - _vfptr - 가상 함수 포인터 변수
  • _vfptr ( 가상 함수의 주소를 저장하는 변수 )를 배열의 형식으로 가지고 있는 가상 함수 테이블이 있고- ref.vfptr[ ]
  • 이 배열의 몇 번째 공간 호출 - 그 공간에 저장되어있는 가상 함수 주소 값으로 - 가상함수 호출하는 것
  • pfunc --> _vfptr
  • *pfuncArray[ 0 ] ( ) --> ref._vfptr[ 0 ]( ) ;

 

 

## Check Point 

  • 가상 함수가 클래스에 하나라도 만들어지면, 
  • 가상 함수 테이블 - (((   ref . _vfptr[ ]  ))) 이 자동으로 만들어진다.
  • 가상 함수의 주소 값을 저장하는 변수 - (((  _vfptr   ))) - 이 자동으로 만들어진다.( 자동으로 )

 

 

 

 


 

 

 

 

 

review


virtual 만 붙여주면 가상 함수가 되고
가상 함수는 들어오는 데이터 타입에 맞춰
함수를 호출해 준다.
사용해봤던 개념이라 이해하기도 사용하기도 쉬웠다.

But.

함수를 가상 함수로 만들어주면 왜 그렇게 되는 것인가?
몰랐다.

이해한 내용을 정리해보자면
가상 함수가 하나라도 만들어지면
가상 함수 주소를 가리키는 변수인 함수 포인터 변수가
자동으로 생긴다 _vfptr
그 변수들을 저장하는 배열인 함수 포인터 배열 - 테이블이
자동으로 생기고 여기에 저장된다. _vfptrArray

이 가상함수 테이블은 상속받는 클래스 모두에게 똑같이 생긴다.
다만 자식 클래스에서 가상 함수를 재정의하면 
테이블에 저장되어있던 부모의 가상 함수 대신
덮어쓰기의 개념으로 테이블에 저장된다.

함수 호출 시
이렇게 각 클래스마다 가지고 있는 가상 함수 테이블에 저장되어있는
그 클래스의 가상 함수가 호출되는 것이다!
A:: B:: C:: 의 가상 함수들 호출
재정의한 부분 있으면 그 클래스의 것으로
재정의하지 않았으면 부모의 것 그대로 호출

흠. 이해한 내용을 글로 설명하려니까 어렵긴 하다.
이런 부분도 연습이 필요할 것 같다.ㅎㅎ

가상 함수의 작동원리를 알 수 있는
좋은 시간이었다.