임베디드 면접 용어 사전
임베디드 C++가상 함수 테이블

vtable

가상 함수 주소를 담은 표. 객체마다 포인터가 하나 늘고 호출이 간접화되어 오버헤드가 생긴다.

C++에서 가상 함수 호출을 런타임에 해결하기 위한 함수 포인터 표 (virtual table).

구조

class Sensor {
public:
    virtual int read() = 0;
    virtual void init() { }
};
class TempSensor : public Sensor {
    int read() override { return 25; }
};
TempSensor 객체
┌───────────────┐
│ vptr ─────────┼──► TempSensor의 vtable
│ (멤버 변수들) │       ┌───────────────────────┐
└───────────────┘       │ [0] read  → 0x8001234 │
                        │ [1] init  → 0x8001300 │
                        └───────────────────────┘

객체마다 vptr(4바이트)이 붙고, 클래스마다 vtable 하나가 생긴다.

호출 과정

Sensor *s = get_sensor();
s->read();
  • ① 객체에서 vptr 을 읽는다 (메모리 접근 1)
  • ② vtable에서 read의 주소를 읽는다 (메모리 접근 2)
  • ③ 그 주소로 간접 점프

직접 호출보다 메모리 접근 2번 + 간접 분기가 더 든다.

C의 함수 포인터 구조체와 같다

// C로 같은 것을 만들면
typedef struct { int (*read)(void); void (*init)(void); } SensorOps;
typedef struct { const SensorOps *ops; int data; } Sensor;

sensor->ops->read();     // 정확히 vtable 과 동일한 동작

C++의 virtual은 이 패턴을 컴파일러가 자동화한 것이다. 이 대응을 알면 "가상 함수가 느리다"는 말의 의미가 구체적으로 이해된다.

비용 — 임베디드에서 따져야 할 것

  • ① RAM : 객체마다 vptr 4바이트
    • 작은 객체를 수천 개 만들면 무시할 수 없다
  • ② 플래시: 클래스마다 vtable (가상 함수 개수 × 4바이트)
  • ③ 속도 : 간접 호출. 인라인 불가. 분기 예측 실패 가능
  • ④ 예측성: 호출 대상이 런타임에 정해져 WCET 분석이 어려워진다

④가 하드 실시간 시스템에서 문제가 된다. 어느 구현이 불릴지 모르면 최악 실행 시간을 계산할 수 없다.

언제 쓰고 언제 피하나

✅ 쓸 만한 경우

    • 드라이버 추상화 (여러 센서를 같은 인터페이스로)
    • 런타임에 구현을 바꿔야 할 때
    • 호출 빈도가 낮은 곳

❌ 피할 경우

    • ISR 안에서
    • 초당 수만 번 호출되는 핫 패스
    • 구현이 컴파일 시점에 정해져 있을 때

대안 — CRTP (컴파일 시점 다형성)

template <typename Derived>
class SensorBase {
public:
    int read() { return static_cast<Derived*>(this)->read_impl(); }
};

class TempSensor : public SensorBase<TempSensor> {
public:
    int read_impl() { return 25; }
};

vtable이 없고 인라인까지 된다. 가상 함수와 같은 코드 구조를 얻으면서 런타임 비용이 0이다.

한계 — 런타임에 타입을 바꿀 수 없다. 컴파일 시점에 정해진 다형성만 표현할 수 있다.

반드시 지킬 것 — 가상 소멸자

class Base { public: ~Base() { } };                 // ✗
class Base { public: virtual ~Base() { } };         // ✓

Base *p = new Derived();
delete p;      // 가상이 아니면 Derived의 소멸자가 안 불린다 → 자원 누수

다형적으로 삭제할 클래스에는 가상 소멸자가 필수다.

생성자·소멸자 안에서는 가상이 동작하지 않는다

class Base {
public:
    Base() { init(); }              // ✗ Base::init 이 불린다
    virtual void init() { }
};

생성 중에는 vptr이 아직 Base를 가리킨다. 파생 클래스 부분이 초기화되지 않았으므로 언어가 이렇게 규정한 것이다. C++ 특유의 함정이며, 자바에서는 반대로 동작해 더 혼란스럽다.

확인 방법

arm-none-eabi-g++ -fdump-lang-class …     # vtable 레이아웃 출력
arm-none-eabi-nm firmware.elf | grep vtable

_ZTV7Sensor 같은 심볼이 vtable이다. 맵 파일에서 크기를 확인할 수 있다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 임베디드 C++·코드 품질 — vtable·예외/RTTI·동적할당·MISRA·volatile