CPP에는 컴파일 타임 다형성과 런타임 다형성이 존재한다.
먼저 컴파일과 런타임의 정의는 완전히 다르다.
컴파일은 소스 코드를 기계어 코드 또는 중간 코드로 변환하는 과정입니다. 이 과정에서 컴파일러는 코드의 문법을 검사하고, 코드의 오류를 찾고, 최적화를 수행합니다.
컴파일 타임 다형성은 컴파일 시점에 타입 결정이 이루어지는 방식입니다. 이 개념은 주로 함수 오버로딩과 템플릿을 사용하여 구현됩니다.
문법 검사: 코드가 문법적으로 올바른지 확인합니다.
타입 검사: 변수와 함수의 타입이 올바른지 확인합니다.
코드 변환: 소스 코드를 목적 파일(Object File) 또는 실행 파일(Executable File)로 변환합니다.
템플릿 인스턴스화: 템플릿을 사용하는 경우, 컴파일 시점에 구체적인 타입으로 변환됩니다.
함수 오버로딩 해결: 여러 함수가 오버로딩된 경우, 어떤 함수를 호출할지 컴파일 타임에 결정합니다.
런타임은 프로그램이 실행되는 동안의 시간입니다. 이 시간 동안 프로그램의 로직이 실제로 수행됩니다.
런타임 다형성은 객체의 실제 타입에 따라 메서드를 동적으로 결정하는 방식입니다. 이 개념은 주로 상속과 가상 함수를 사용하여 구현됩니다.
역할
동적 메모리 할당: new 연산자를 사용하여 동적으로 메모리를 할당합니다.
런타임 다형성: 객체의 실제 타입에 따라 어떤 메서드가 호출될지 결정합니다. 이 경우, 메서드 호출은 런타임에 결정됩니다.
예외 처리: 예외가 발생하는 경우 런타임에 예외 처리가 이루어집니다.
파일 입출력: 파일을 읽고 쓰는 작업이 런타임에 수행됩니다.
Class는 상속과 가상 함수를 이용해 이중 런타임 다형성에 관여한다.
상속(Inheritance)은 객체 지향 프로그래밍(OOP)의 중요한 개념 중 하나로, 기존의 클래스(부모 클래스 또는 기반 클래스, 슈퍼클래스)의 속성과 동작(메서드)을 새로운 클래스(자식 클래스 또는 파생 클래스, 서브클래스)에 물려주는 기능을 말합니다.
// 상속 클래스 예시
class Parent {
public:
void show() {
std::cout << "Parent class" << std::endl;
}
};
class Child : public Parent {
};
가상 함수(Virtual Function)는 객체 지향 프로그래밍에서 다형성을 구현하는 중요한 개념입니다. 가상 함수를 사용하면, 부모 클래스의 포인터나 참조를 통해 자식 클래스의 오버라이딩된 메서드를 호출할 수 있습니다. 이를 통해 런타임에 어떤 메서드가 호출될지 결정할 수 있는 동적 바인딩(Dynamic Binding)을 가능하게 합니다.
가상 함수는 virtual 키워드와 override를 사용한다.
#include <iostream>
class Parent {
public:
virtual void Show() { // 가상 함수
std::cout << "Parent's Show" << std::endl;
}
};
class Child : public Parent {
public:
void Show() override { // 가상 함수를 오버라이딩
std::cout << "Child's Show" << std::endl;
}
};
int main() {
Parent* p = new Child(); // 부모 클래스 포인터로 자식 클래스 객체를 가리킴
p->Show(); // Child의 Show()가 호출됨
delete p;
return 0;
}
Vtable : 시스템의 포인터 크기와 같으며, 가상 함수의 포인터를 저장합니다.
클래스가 가상 함수를 하나라도 가지고 있으면, 컴파일러는 해당 클래스의 모든 가상 함수를 가리키는 함수 포인터들을 포함하는 테이블을 생성합니다. 이 테이블을 가상 함수 테이블(vtable)이라고 합니다.
각 클래스는 하나의 vtable을 가지며, vtable은 해당 클래스의 객체들이 사용할 가상 함수들의 주소를 가지고 있습니다.
가상 함수가 있는 클래스의 객체는 vtable을 가리키는 포인터(vptr)를 가집니다. 이 포인터는 객체마다 존재하며, 객체가 생성될 때 초기화됩니다.
가상함수 테이블의 크기는 보통 포인터의 크기와 동일합니다. 즉, 32비트 시스템에서는 4바이트, 64비트 시스템에서는 8바이트입니다. 그리고 각 Class 가 가지고 있는 멤버 변수의 크기를 합쳐주면 각 Class를 가진 객체를 만들었을때 선언되는 바이트 크기가 정해집니다.
AA 클래스의 크기 : 12
BB 클래스의 크기 : 16
하지만 이건 C++에서 메모리 정렬을 제어하는 지시문인 Pargma pack(1)로 메모리 패딩을 최소화 했기에 가능하다. 만약 메모리 정렬 지시문을 사용하지 않으면 각 클래스에 할당되는 바이트 크기는
AA 클래스의 크기 : 16
BB 클래스의 크기 : 24
가 된다.
클래스 AA:
멤버 변수: int m_A: 4바이트 (4바이트 정렬 필요)
가상 함수 테이블 포인터 (vptr): 64비트 시스템에서 8바이트 (8바이트 정렬 필요)
패딩: int m_A 다음에 vptr이 오기 때문에, vptr이 8바이트 경계에서 정렬되도록 4바이트의 패딩이 추가될 수 있습니다.
총합: int (4바이트) + 패딩 (4바이트) + vptr (8바이트) = 16바이트
클래스 BB:
상속 : AA의 메모리 레이아웃 (16바이트 포함)
추가 멤버 변수 : int m_B: 4바이트
패딩 : int m_B가 추가되며, vptr과 m_B의 정렬을 맞추기 위해 추가 패딩이 필요할 수 있습니다.
총합 : AA (16바이트) + int m_B (4바이트) + 패딩 (4바이트) = 24바이트
#include <iostream>
#pragma pack(1)
class AA
{
private:
int m_A;
public:
virtual void OutputData()
{
std::cout << m_A << std::endl;
}
virtual void NewVirtualFunc()
{
std::cout << m_A << std::endl;
}
// 순수 가상함수(Pure Virtual Function)
virtual void Abstract() {}
public:
AA()
: m_A(0)
{}
virtual ~AA()
{}
};
class BB
: public AA
{
private:
int m_B;
public:
// 부모 클래스에 구현된 가상함수를 오버라이딩하면, 자식쪽에서도 가상함수로 취급된다.
// 부모 클래스의 맴버함수를 오버라이딩 한 경우에만 override 키워드를 붙일 수 있다.
void OutputData()
{
AA::OutputData();
std::cout << m_B << std::endl;
}
virtual void Abstract() override
{
}
public:
BB()
: m_B(0)
{}
virtual ~BB()
{}
};
int main()
{
int size = sizeof(AA);
size = sizeof(BB);
return 0;
}