프로그래밍백엔드 개발자 (C++)

가상 함수란 무엇이며 C++에서 지연 바인딩 메커니즘은 어떻게 작동합니까?

Hintsage AI 어시스턴트로 면접 통과

답변.

질문 배경:

C++는 현대 언어에 기본이 되는 객체 지향 프로그래밍 지원을 도입했습니다. 다형성을 구현하기 위해 가상 함수가 사용되었습니다. 이것은 상속을 가진 아키텍처에 중요하게 작용하는 방법을 컴파일 단계가 아닌 실행 단계에서 호출할 수 있게 해주었습니다.

문제점:

일반적인 오류는 정적 호출과 동적 호출 사이의 혼동, 잊혀진 가상 소멸자, 잘못된 상속 처리(예: 객체 슬라이스, 기본 버전 호출 대신 오버라이드 호출)입니다. 진정한 다형성이 작동할 때에 대한 혼동이 자주 발생합니다.

해결책:

가상 함수는 기본 클래스에서 virtual 키워드로 선언되며 파생 클래스에서 오버라이드될 수 있습니다. 기본 클래스의 포인터나 참조를 통해 함수를 호출하면 파생 클래스의 구현 버전이 실행됩니다.

코드 예제:

struct Base { virtual void foo() { std::cout << "Base::foo "; } }; struct Derived : Base { void foo() override { std::cout << "Derived::foo "; } }; void call(Base& b) { b.foo(); } int main() { Derived d; call(d); // 출력: Derived::foo }

주요 특징:

  • 지연 바인딩(dynamic dispatch): 메서드 버전의 선택은 실행 시간에 발생합니다.
  • 기본 클래스에 대한 포인터와 참조를 통해 작동합니다.
  • 함수의 올바른 오버라이드는 C++11부터 가능해진 override 키워드를 필요로 합니다.

함정 질문.

객체를 값으로 전달할 때 다형성이 작동합니까?

아니요. 값으로 전달하면 "슬라이스"가 발생하여 매개변수 유형에 해당하는 부분(보통 기본 클래스)만 복사되므로 다형성이 비활성화됩니다.

코드 예제:

void call(Base b) { b.foo(); } // 항상 Base::foo 호출

기본 클래스에서 소멸자를 가상으로 선언해야 합니까?

예, 기본 클래스에 대한 포인터를 통해 파생 객체를 삭제할 경우 그렇습니다. 그렇지 않으면 메모리 누수나 자원 해제가 발생합니다.

코드 예제:

struct Base { virtual ~Base() {} };

자식 클래스에서 override 키워드를 사용하지 않으면 어떤 일이 발생합니까?

파생 클래스에서 override를 지정하지 않고 함수의 시그니처를 잘못 변경하면(예: const를 빼먹거나 파라미터를 잘못 지정하면) 함수는 가상 함수를 오버라이드하지 않고 새롭게 생성되므로 다형성이 예상대로 작동하지 않습니다.

일반적인 실수 및 안티패턴

  • 가상 소멸자를 선언하지 않음
  • 잘못 구현된 오버라이드(오버라이드 없음, 시그니처 변경)
  • 슬라이스를 초래하는 값 매개변수 사용 대신 참조/포인터 사용

실제 사례

부정적인 경우

프로그래머가 기본 클래스의 소멸자를 가상으로 선언하지 않았습니다; 기본 포인터를 통해 객체 배열을 삭제하면 메모리 누수가 발생했습니다.

장점:

  • 객체 삭제까지의 올바른 작동

단점:

  • 자원 해제되지 않고 메모리 누수 발생

긍정적인 경우

가상 소멸자가 선언되었고 기본 유형에 대한 포인터/참조만 사용되었습니다. 다형성이 올바르게 작동했습니다.

장점:

  • 안전한 메모리 해제
  • 깔끔하고 확장 가능한 코드

단점:

  • 가상 테이블로 인한 약간의 메모리 및 실행 시간 증가