본문 바로가기

C++

C++ 배우기 24(가상함수, 재정의)

가상 함수(virtual function)

가상 함수는 부모 클래스에게 상속받은 자식 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미한다.

class A
{
	virtual void Print();
};

class B : public A
{
	virtual void Print();
};

위의 예시처럼 virtual을 멤버함수의 원형 앞에 붙여주면 된다.

 

이런 가상함수를 왜 쓰는 걸까? 우선 바인딩이란 함수를 호출하는 코드에서 어디에 있는 함수를 실행하라는 의미이다.

그리고 여기서 바인딩은 정적 바인딩, 동적 바인딩으로 나눌 수 있다.

우선 정적 바인딩(초기 바인딩)은 컴파일러가 함수를 호출할 때 컴파일 타임에 고정된 메모리 주소로 변환시키는 것을 말한다. 일반적인 함수들은 모두 이런 정적 바인딩을 따르고 있다.

그런데 일반 함수를 오버 로딩하게되면 이 정적 바인딩으로 인해 문제가 발생한다.

 

가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야 할지 미리 알 수 없다. 가상 함수는 프로그램이 실행될 때

객체를 결정하여 컴파일 타임에 해당 객체를 특정할 수 없기 때문이다.

그래서 런 타임에 올바른 함수가 실행될 수 있도록 하는 것이 동적 바인딩(지연 바인딩)이라고 한다.

 

class Parent
{
	virtual void Print();
};

class Child : public Parent
{
	virtual void Print();
};

int main()
{
    Parent* p = new Parent;
    Child* c = new Child;
    
    p->Print();
    p = c;
    p->Print();
};

위의 예시처럼 Child는 Parent에 상속받았고, 서로 Print()함수가 가상 함수로 동적 바인딩되어있다.

따라서 첫 번째 Print() 함수는 Parent의 Print() 함수가 출력되지만, 중간에 p = c를 대입함으로써 두번째 출력은

Child의 Print()함수가 출력이 된다.


가상 함수 테이블(virtual Function Table)

컴파일러가 가상 함수를 다루는 가장 일반적인 방식이 가상 함수 테이블을 이용하는 것이다.

클래스에서 virtual이 선언되면 객체마다 가상함수 테이블을 가리키는 포인터 멤버를 하나씩 추가한다.

이때 가상 함수를 단  하나라도 가지는 클래스에 대해서 가상 함수 테이블을 작성한다.

그리고 가상 함수 테이블에는 해당 클래스의 객체들이 생성한 가상 함수들의 주소가 저장이 된다.

 

가상 함수를 호출하면 가상 함수 테이블에 접근하여 필요한 함수의 주소를 찾아 호출하게 된다.

하지만 이렇게 가상 함수를 사용하면 메모리와 실행 속도 면에서 약간 부담이 있을 수 있다.

따라서 필요한 경우에만 가상 함수로 선언하는 것이 좋다.


가상 소멸자

class Parent
{
    Parent();
    ~Parent();
	virtual void Print();
};

class Child : public Parent
{
    Child();
    ~Child();
	virtual void Print();
};

int main()
{
    Parent* p = new Child;
    
    delete p;
};

위의 예제를 보면 Parent클래스는 부모, Child클래스는 자식이라서, p라는 Child객체가 형 변환되어 해당 자료형에 맞게

동적 할당이 가능하다.

그리고 생성과 소멸의 순서를 보면 Parent생성 - Child생성 - Parent소멸만 되는 것을 볼 수 있다.

이는 new Child()로 Child객체를 선언했지만, 포인터 타입이 Parent* 이기 때문에 객체 p는 A클래스의 정보만 볼 수 있다.

 

따라서 이를 해결하기 위해서는 부모 클래스에 가상(virtual) 소멸자를 붙이는 것이다. (virtual ~Parent();)

부모 클래스에 virtual이 선언되면 자동으로 자식 클래스에도 만들어지기 때문에 가능해진다.


재정의

자식 클래스에서 부모 클래스와 동일한 형식의 함수를 재정의 하는 것을 말하며, 멤버 함수 오버 라이딩이 있다.

부모 클래스에서 이미 정의된 함수를 무시하고, 자식 클래스에 같은 이름으로 함수를 새롭게 정의하는 것을 말한다.

class Parent
{
    void Print();
};

class Child : public Parent
{
   void Print();
};

이러한 오버 라이딩은 자식 클래스에서 직접 오버로딩하는 방법과, 가상 함수를 통해 오버로딩하는 방법이 있다.

위 예시와 같은 것이 자식 클래스에서 직접 오버로딩 하는 경우이다.

하지만 이런 자식클래스 오버 로딩은 일반적인 상황에서는 잘 작동하지만, 포인터 변수를 사용하게 되면 예상치 못한

결괏값이 반환이 된다.

 

Parent* p_par;
Parent a(1, 2);
Child b(1, 2, 3);

p_par = &a;
p_par->Print();
p_par = &b;
p_par->Print();

위 예시의 결괏값으로 Parent클래스의 Print() 함수만 출력이 되는 것을 볼 수 있다.

이는 포인터 변수가 가리키는 객체의 타입을 기준으로 호출하는 것이 아니라, 해당 포인터의 타입(자료형)을 기준으로

함수를 호출하기 때문에 일어나는 문제이다.

그래서 이 문제를 해결하기 위해 자식 클래스의 Print() 함수에서 virtual을 사용하면 되겠다.

 

 

class Parent
{
    void Print();
};

class Child : public Parent
{
    virtual void Print();
};

이렇게 자식 클래스에서 상속받은 멤버 함수 Print()를 재정의하면, 포인터가 실제로 가리키는 객체에 따라서 호출하는

대상을 바꿀 수 있게 된다.

'C++' 카테고리의 다른 글

C++ 배우기 26(LValue / RValue)  (0) 2022.11.30
C++ 배우기 25(복사생성자, 팩토리 패턴)  (2) 2022.11.29
C++ 배우기 23(생성자, 소멸자)  (0) 2022.11.24
C++ 배우기 22(static)  (0) 2022.11.23
C++ 배우기 21(템플릿)  (0) 2022.11.17