상세 컨텐츠

본문 제목

가상 함수 - virtual Function

C++ 언어

by ChrisMare 2018. 3. 13. 16:46

본문

※ 가상함수(Virtual Function)


가상함수는 어떻게 동작하는가?

C++은 가상 함수들이 어떻게 동작해야 하는지를 규정하고 있다. 그러나 그것의 구현은 컴파일러 개발자의 몫이다......

가상 함수를 사용하기 위해 그것이 구현된 방법을 굳이 알 필요는 없지만 그것을 알면, 가상 함수의 개념을 좀 더 잘 이해할 수 있다.

그러므로 잠깐 살펴볼 필요가 있다.


컴파일러가 가상 함수를 다루는 일반적인 방법은,

 - 각각의 객체에 숨겨진 멤버를 하나씩 추가하는 것이다.

숨겨진 멤버는 함수의 주소들로 이루어진 배열을 지시하는 포인터를 저장한다.

=> 일반적으로 그 배열을 가상 함수 테이블(virtual function table ; vtbl)이라고 한다.

vtbl에는, 그 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되어 있다.

예를 들어, 기초 클래스의 한 객체는, 그 클래스를 위한 모든 가상 함수들의 주소로 이루어진 테이블을 지시하는 포인터를 가진다.

파생 클래스의 한 객체는, 가상 함수들의 주소로 이루어진 별개의 테이블을 지시하는 포인터를 가진다. 파생 클래스가 가상 함수에 대해 새로운 정의를 제공하면, vtbl은 새로 정의된 함수의 주소를 저장한다.


파생 클래스가 가상 함수를 다시 정의하지 않으면, vtbl은 그 함수의 오리지널 버전의 주소를 저장한다.

파생 클래스가 새로운 함수를 정의하고 그 함수를 가상으로 선언하면, 그 주소가 vtbl에 추가된다.

어떤 클래스에 대해 가상함수를 1개 정의하든 10개 정의하든 간에, 주소 멤버는 한 객체에 하나만 추가된다.

정의하는 가상 함수의 개수에 따라 테이블 크기만 변한다.


가상 함수를 호출하면, 프로그램은 객체에 vtbl 주소가 저장되어 있다는 것을 알게 되고, 함수 주소들로 이루어진 해당 테이블에 접근한다. 사용하는 함수가 클래스 선언에 정의된 첫 번째 가상 함수라면, 프로그램은 그 배열에 있는 첫 번재 주소를 사용하고, 그 주소에 있는 함수를 실행시킨다.

사용하는 함수가 클래스 선언에 있는 세 번째 가상 함수라면, 프로그램은 배열의 세 번째 원소에 주소가 저장되어 있는 함수를 사용한다.


다시 말해서, 가상 함수를 사용하면 메모리와 실행 속도 면에서 다음과 같은 약간의 부담이 따른다.


- 각 객체의 크기가 주소 하나를 저장하는 데 필요한 양만큼 커진다

- 각각의 클래스에 대해, 컴파일러는 가상 함수들의 주소로 이루어진 하나의 테이블(배열)을 만든다.

- 각각의 함수 호출에 대해, 실행할 함수의 주소를 얻기 위해 테이블에 접근하는 가외의 단계가 더 필요하다.


가상이 아닌 함수들은 가상 함수보다 조금 더 효율적이지만, 동적 결함을 제공하지 않는다는 사실을 명심해야된다.!!!!!




가삼 함수의 동작방식 예제)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Scientist
{
    ...
    char name[40];
public:
    virtual void show_name();
    virtual void show_all();
    ...
};
class Physicist : public Scientist
{
    ...
    char field[40];
public:
    void show_all();    // 다시 정의된 함수
    virtual void show_field();    // 새로 정의된 함수
    ...
};
 
void main()
{
    Physicist adam("Adam Crusher", "nuclear structure");
    Scientist * psc = &adam;
    psc->show_all();
}
cs




※ 가상 메서드에 대해 알아야 할 사항


가상 메서드에 관련된 중요한 사항들은 앞에서 이미 설명하였지만,


  •  기초 클래스에서 클래스 메서드를 선언할 때 키워드 virtual로 시작하면, 그 함수는 기초 클래스, 기초 클래스에서 파생된 크래스, 파생 클래스에서 다시 파생된 클래스 등 모든 클래스에 대해 가상이 된다.
  • 객체에 대한 참조를 사용하여 또는 객체를 지시하는 포인터를 사용하여 가상 메서드가 호출되면, 프로그램은 그 참조나 포인터형을 위해 정의된 메서드를 사용하지 않고, 객체형을 위해 정의된 메서드를 사용한다. 이것을 동적 결합(또는 말기 결합)이라 한다. 기초 클래스 포인터나 참조가 파생 클래스 객체를 지시하는 것은 항상 가능하기 때문에, 이와 같은 행동은 중요하다.
  • 상속을 위해 기초 클래스로 사용할 클래스를 정의할 때, 파생 클래스에서 다시 정의해야 하는 클래스 메서드들은 가상 함수로 선언해야 한다.
그 외에도 가상 메서드에 대해서 알아야 할 것이 몇 가지 더 있다!!

1. 생성자
생성자는 가상으로 선언할 수 없다. 파생 클래스 객체의 생성은, 기초 클래스 생성자가 아니라 파생 클래스 생성자를 호출한다. 그러고 나서 파생 클래스 생성자가 기초 클래스 생성자를 사용한다. 이 시퀀스는 상속 메커니즘과는 다르다. 그래서, 파생 클래스는 기초 클래스 생성자를 상속하지 않는다. 그러므로 가상으로 만들 이유가 없다.

2. 소멸자

클래스가 기초 클래스로 사용된다면, 소멸자는 가상으로 선언해야 한다. 예를 들어, Employee가 기초 클래스이고, Singer가 파생 클래스라고 가정하자. Singer에는 new에 의해 대입된 메모리를 지시하는 char * 멤버가 추가되었다. 그렇다면 Singer 객체의 수명이 다했을 때 ~Singer( )  소멸자가 호출되어 그 메모리를 해제하는 것이 매우 중요해진다.


예제 코드로 살펴보자)


Employee * p = new Singer;     // Employee는 Singer의 기초이므로 유효하다.

// 부모가 자식을 접근하는 동적바인딩

delete p;                               // ~Employee( ) 일까 ~Singer( )일까?


디폴트 정적 결합(즉, 정적 바인딩)이 적용된다면, delete 구문은 ~Employee( ) 소멸자를 호출한다.

그러면 이 코드는 Singer 객체에 새로 추가된 클래스 멤버들이 지시하는 메모리는 해제하지 않고, 

Singer 객체의 Employee 성분들이 지시하는 메모리만 해제한다.

그러나 소멸자가 가상이라면, 동일한 코드가 ~Singer( ) 소멸자를 호출한다. 그 소멸자는 Singer 성분이 

지시하는 메모리를 해제하고 나서, ~Employee( ) 소멸자를 호출하여 Employee 성분이 지시하는 메모리를 해제한다.


이것은, 부모 클래스가 명시적 소멸자의 서비스를 요구하지 않더라도 디폴트 소멸자에 의존하면 안 된다는 것을 의미한다.

그 대신에, 아무 일도 하지 않는 가상 소멸자를 제공해야 한다.


virtual ~SuperClass( ) { }


부모 클래스를 의도한 것이 아닐지라도 어떤 클래스가 가상 소멸자를 가지는 것은 에러가 아니다. 그것은 단지 효율성의 문제이다.


따라서, 소멸자가 필요 없는 부모 클래스라 하더라도 가상 소멸자를 제공해야 한다.


3. 프렌드

프렌드는 가상 함수가 될 수 없다. 그 이유는 멤버 함수만 가상 함수가 될 수 있는 데, 프렌드는 클래스 멤버가 아니기 때문이다. 이것 때문에 설계에 문제가 있다면, 그 프렌드 함수가 내부적으로 가상 멤버 함수를 사용하게 하여 문제를 해결할 수 있다.

관련글 더보기

댓글 영역