프로그래밍 언어에서 하나의 기능을 수행하는 명령어의 집합을 만들고 이에 이름을 붙여 필요할 때마다 호출해서 사용할 수 있는 구현방법을 subroutine이라고합니다. 우리가 C언어에서 만드는 함수나 자바의 메서드는 모두 이러한 subroutine의 개념이 언어에서 구현된 기능이죠. 

이때 함수의 이름이나 메서드의 이름은 중복될 수 없습니다. (이후에는 함수와 메서드를 통칭해서 함수라 하겠습니다)  함수의 이름은 추후 컴파일이 되면 호출될 함수의 주소를 나타내기 때문입니다. 마치 배열의 이름이 주소를 나타내는 것과 같은 의미라 생각하시면 됩니다. 함수의 이름이 주소라는 것은 그 함수가 수행하여야 하는 명령어의 집합(코드 블럭)의 위치를 나타내고, 함수가 호출되면 해당 주소로 제어가 이동하는 것입니다. 

그런데 우리는 자바나 자바스크립트, C++등에서 코딩을 하다보면 같은 함수의 이름이 매개변수만 다른 경우 다른 함수처럼 사용할 수 있게 구현하였습니다. 이를 오버로딩(overloading) 이라고 하는데, 컴파일러가 컴파일을 하기 전 프리컴파일 과정에서 함수의 이름을 매개 변수에 기반 하여 조금씩 변경하는 작업이 이루어지기 때문입니다. 가령 printf() 라는 이름의 함수가 여러개 정의 되었다고 할 때 precompile 단계에서 printf_s() 와 같이 함수의 이름을 조금씩 변경합니다. 

그런데 C++이나 자바에서 클래스 간의 상속관계에서도 함수의 이름이 같은 경우가 있습니다. 상위 클래스에 구현된 함수에 대해 하위 클래스에서 그 기능을 재정의하는 경우 오버라이딩(overriding)이라고 합니다. 이러한 오버라이딩이 구현되면 같은 이름의 함수가 상위 클래스와 하위 클래스에 두 개 존재하게 되는 것입니다. 

다음과 같은 간단한 C++ 코드를 생각해 보죠

Employee와 이를 상속받은 Engineer 클래스가 있습니다.

class Employee {

public:

        void work();

};

class Engineer : public Employee {

public:

        void work();

};

이에 대한 구현  cpp 파일이 다음과 같습니다.

using namespace std;

#include “Test.h”

#include <iostream>

void Employee::work() {

        cout << “Employee Work” << endl;

}

void Engineer::work() {

        cout << “Engineer Work” << endl;

}

객체 지향 프로그래밍의 클래스간 상속이 이루어지면 하위 클래스는 상위 클래스의 모든 속성과 함수를 사용할 수 있습니다. 즉, Engineer 에겐 work()라는 함수가 두 개 존재 하는 것이죠. 그럼 다음과 같이 테스트 코드를 작성해보겠습니다.

int main()

{

Employee* employee = new Employee(); //상위 클래스 타입의 변수에 상위 클래스 생성

employee->work();

Engineer* engineer = new Engineer(); // 하위 클래스 타입의 변수에 하위 클래스 생성

engineer->work();

Employee* emEngineer = new Engineer(); // 상위 클래스 타입의 변수에 하위 클래스 생성

emEngineer->work();

delete employee;

delete engineer;

delete emEngineer;

return 0;

}

위 main()의 결과는 다음과 같습니다.

Employee* emEngineer = new Engineer();

emEngineer->work();

의 결과가 Employee Work 인것을 알수 있습니다. 변수의 자료형이 Employee 이고 생성된 인스턴스가 Engineer 인 경우에도 자료형의 함수가 불려집니다. 이때 Engineer의 함수가 불려지게 하기 위해서는 상위 클래스(Employee)의 함수를 가상함수(Virtual function)로 선언하여야 합니다.  

Employee에서 가상함수로 선언한 후 결과를 보면…

class Employee {

public:

        virtual void work();

};

생성된 인스턴스의 함수가 불리는 것을 알 수 있습니다.

이처럼 가상함수로 선언된 경우는 상위 클래스의 타입으로 선언되었더라도 생성된 인스턴스의 함수가 호출됩니다. 객체지향 프로그래밍에서는 이를 활용하여 하나의 클래스를 여러 클래스가 상속을 받았을 경우 각각의 상속된 인스턴스의 함수가 호출되는 다형성(polymorphism)을 구현할 수 있습니다.

그럼 가상함수는 왜 인스턴스의 함수가 불리는 걸까요?

가상함수는 가상함수 테이블(virtual function table) 이라는 구조를 가지게 되는데 이는 함수의 주소를 저장합니다. 각 클래스가 가지는 함수에 대한 주소를 저장하고 있어서 해당 인스턴스에 대해 함수가 호출되었을 때 재정의된 경우 재정의 된 인스턴스의 함수가 호출 될 수 있습니다.

C++은 virtual 이라는 키워드를 써야 가상함수가 되지만, 자바는 vitual 키워드를 사용하지 않고 모든 메서드가 가상 메서드입니다. 따라서 상속관계에서 하위 클래스에 재정의된 메서드가 있고, 상위 클래스의 타입으로 선언되었더라도 하위 클래스의 인스턴스로 생성되고 해당 메서드가 호출되면 항상 재정의 된 메서드가 실행됩니다.

다음은 자바의 예로 상위 클래스가 Customer이고 하위 클래스가 VIPCustomer 인 경우의 가상함수 테이블을 간단히 그린 그림입니다. 제품의 가격을 계산하는 calcPrice()메서드는 하위 클래스인 VIPCustomer에서 재정의 되었으므로 상위 클래스와 다른 메서드의 주소를 가지게 됩니다. 하지만, showCustomerInfo()는 재정의 되지 않았으므로 같은 주소를 가지게 되는 것을 알수 있습니다.  

가상 함수 테이블의 예

가상함수는 객체지향에서 상속이나 인터페이스 구현을 통한 다형성을 구현하는 기본 기술이랍니다~