09.02 - TIL

김혁·2025년 9월 2일

TIL

목록 보기
9/84

오늘의 코드카타

미로 탈출 명령어

오늘의 공부

진행한 과제

  • 9번 과제
    • 기본 숫자 야구 오류 수정
    • 플레이어 턴 로직 추가
    • 턴마다 시간을 주고, 시간 안에 입력 안 할 시 기회 소진
  • 깃허브 : https://github.com/americanotwoshot/HW09

오늘의 CS

vtable이란?

  • C++에서 가상 함수를 지원하기 위해 컴파일러가 내부적으로 사용하는 함수 포인터 테이블
  • OOP의 특성인 다형성을 제공해줌

동작과정

  1. 함수 내부에 virtual 키워드를 가진 가상 함수가 존재하는지 확인
  2. 가상 함수가 존재한다며 컴파일러는 클래스마다 vtable을 하나씩 만듦
  3. 객체마다 vptr(가상 테이블 포인터)를 멤버로 추가
    -> vptr은 해당 객체의 vtable을 가리킴
  4. 해당 함수를 호출하게 되면 vptr을 통해 vtable을 확인한 후, 해당 주소의 함수를 실행

예시

struct A {
    virtual void Foo() { std::cout << "A::Foo\n"; }
    virtual void Bar() { std::cout << "A::Bar\n"; }
};

struct B : public A {
    void Foo() override { std::cout << "B::Foo\n"; }
    // Bar는 override 안 함
};
A의 vtable
[0] → &A::Foo
[1] → &A::Bar

B의 vtable
[0] → &B::Foo   // 오버라이딩됨
[1] → &A::Bar   // 오버라이딩 안 됨 → 부모 함수 그대로 사용

단점

  • 함수 호출 시, 간접 참조 1번 (vptr -> vtable -> 함수) 오버헤드
  • 메모리 사용량 증가 (객체마다 vptr 필요, 클래스마다 vtable 필요)

순수 가상 함수를 만들면 vtable 내부 구조

  • 순수 가상 함수의 경우, vtable에는 단순히 추상 함수 자리로 채워짐
  • 그 클래스가 추상 클래스라면 해당 함수 슬롯을 직접 호출 불가하게 함
    • gcc/Clang은 null을 표현해주는 특수 슬롯을 넣음
    • MSVC는 __purecall이라는 런타임 핸들러 함수 포인터를 넣음
  • MSVC는 __purecall 핸들러를 넣은 이유
    • 그냥 segfault 같은 호출보다는 구체적인 진단 메세지를 주기 위해 설계
    • 디버깅 친화적인 설계를 위해서
  • visual studio 2022에서 테스트 해본 결과, 실제 메모리 주소에 purecall 핸들러가 들어가있음
  • 테스트 코드
#include <iostream>
#include <cstdint>
#include <iomanip>

using namespace std;

extern "C" int __cdecl _purecall(void); // CRT의 실제 시그니처와 맞춤

struct Base {
    virtual void Foo() = 0;
    virtual void Bar() { std::cout << "Base::Bar\n"; }
};

struct Derived : public Base {
    void Foo() override { std::cout << "Derived::Foo\n"; }
    void Bar() override { std::cout << "Derived::Bar\n"; }
};


int main() {
    Derived d;
    Base* b = &d;

    // 함수 포인터로 취득
    auto fptr = reinterpret_cast<void(*)()>(&_purecall);
    // 함수 포인터 → 정수로 변환(주소 출력)
    std::uintptr_t addr = reinterpret_cast<std::uintptr_t>(fptr);
    std::cout << "_purecall addr = 0x"
        << std::hex << std::setw(sizeof(void*) * 2) << std::setfill('0')
        << addr << std::dec << std::endl;

    b->Foo();
    b->Bar();

    return 0;
}

순수 가상 함수를 구현하지 않았을 때 발생하는 오류

1. 인스턴스화하지 않는 경우

  • 아무 에러가 발생하지 않음

2. 자손 클래스에서 선언하지 않은 경우

struct Base {
    virtual void Foo() = 0; // 순수 가상 함수
};

struct Derived : public Base {
};

int main() {
    Derived d;  
}
  • MSVC : 추상 클래스를 인스턴스화할 수 없다는 컴파일 에러 발생
  • gcc compiler, Clang : 추상 클래스를 인스턴스화할 수 없다는 컴파일 에러 발생


3. 자손 클래스에서 선언만 하고, 정의하지 않은 경우

struct Base {
    virtual void Foo() = 0; // 순수 가상 함수
};

struct Derived : public Base {
    virtual void Foo() override;
};

int main() {
    Derived d;  
}
  • MSVC : 정의되지 않은 함수 포인터를 불렀기 때문에 링크 에러 발생
  • gcc compiler, Clang : 정의되지 않은 함수 포인터를 불렀기 때문에 링크 에러 발생


4. 자손 클래스에서 구현했지만, 추상 클래스 생성자/소멸자에서 직접 호출

struct Base {
    Base() { Foo(); }
    virtual void Foo() = 0; // 순수 가상 함수
};

struct Derived : public Base {
    virtual void Foo() override { cout << "Derived::Foo\n"; }
};

int main() {
    Derived d;  
    d.Foo();
}
  • MSVC : 정의되지 않은 함수 포인터를 불렀기 때문에 링크 에러 발생
  • gcc compiler, Clang : pure virtual function call 경고 + 정의되지 않은 함수 포인터를 불렀기 때문에 링크 에러 발생


5. 자손 클래스에서 구현했지만, 추상 클래스 생성자/소멸자에서 다른 함수에서 호출

struct Base {
    Base() { Init(); }
    void Init() { Foo(); }
    virtual void Foo() = 0; // 순수 가상 함수
};

struct Derived : public Base {
    virtual void Foo() override { cout << "Derived::Foo\n"; }
};

int main() {
    Derived d;  
    d.Foo();
}
  • MSVC : abort() 런타임 에러 발생
  • gcc compiler, Clang : pure virtual method called 런타임 에러 발생
profile
게임 개발자를 향해..

0개의 댓글