가상 함수와 VTable
Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 104개 중 52번째.
virtual 키워드는 이전 레슨에서 살펴본 문제를 해결합니다. 메서드를 virtual로 선언하면 C++는 포인터 유형이 아니라 런타임의 실제 객체 유형을 기준으로 호출할 버전을 결정합니다.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // 출력: Woof!이제 올바른 메서드가 호출됩니다! override 키워드는 선택 사항이지만 권장됩니다. 이는 컴파일러에 가상 함수를 오버라이드하려는 의도를 알리고, 시그니처가 일치하지 않으면 오류를 감지합니다.
이것은 어떻게 작동하나요? 클래스에 가상 함수가 있으면 컴파일러는 가상 테이블(vtable)을 생성합니다. 이는 클래스의 가상 함수에 대한 포인터를 포함하는 숨겨진 조회 테이블입니다. 가상 함수가 있는 각 객체는 해당 클래스의 vtable을 가리키는 숨겨진 포인터(vptr)를 저장합니다.
포인터를 통해 가상 함수를 호출하면 프로그램은 런타임에 vtable에서 올바른 함수 주소를 조회합니다. 이를 동적 디스패치라고 합니다. 일반적인 함수 호출에 비해 약간의 오버헤드가 추가되지만, 강력한 다형성 동작을 가능하게 합니다.
중요한 규칙이 하나 있습니다. class에 가상 함수가 있고 base class로 사용될 경우, 해당 destructor도 virtual이어야 합니다. 이렇게 하면 base 포인터를 통해 파생 객체를 삭제할 때 적절한 정리가 이루어집니다.
class Animal {
public:
virtual ~Animal() {} // 가상 소멸자
virtual void speak() {}
};챌린지
쉬움가상 함수와 런타임 다형성의 강력한 기능을 보여 주는 미디어 플레이어 시스템을 만들어 보겠습니다. 각 미디어 유형이 서로 다르게 재생되는 계층 구조를 만들고, 기본 클래스 포인터를 통해 접근하더라도 가상 함수가 올바른 동작을 가능하게 하는 방식을 살펴봅니다.
코드를 세 개의 파일로 구성합니다:
Media.h: 재생 가능한 모든 미디어를 나타내는 기본Mediaclass를 Define합니다:- protected
std::string titlemember - title을 받아 저장하는 constructor
- virtual
play()method:Playing media: <title>를 출력 - virtual destructor:
Media [<title>] destroyed를 출력
- protected
AudioTrack.h:Media를 public하게 inherits하는AudioTrackclass를 Define합니다:- private
std::string artistmember - title과 artist를 받아 title을 base class에 전달하고 artist를 저장하는 constructor
overridekeyword를 사용하여play()를 Override하고,Playing audio: <title> by <artist>를 출력AudioTrack [<title>] destroyed를 출력하는 destructor
- private
main.cpp: 세 개의 input을 각각 별도의 줄에서 읽습니다:- Audio track title (string)
- Artist name (string)
- Video title (string)
Media를 public하게 inherits하는VideoClipclass를 main.cpp 안에 직접 Define합니다:- private
int durationmember (초 단위) - title과 duration을 받는 constructor (기본 duration은 120)
play()를 Override하여Playing video: <title> (<duration>s)를 출력VideoClip [<title>] destroyed를 출력하는 destructor
block scope 내부에 세 개의
Media*pointer로 이루어진 array를 Create합니다. 다음을 Dynamically allocate합니다:- title이 "Generic Media"인 기본
Mediaobject - input title과 artist를 사용하는
AudioTrackobject - input video title을 사용하는
VideoClipobject
array를 Loop하며 각 pointer에서
play()를 call합니다. 그런 다음 모든 object를 역순으로 Delete합니다. block 이후에Playback complete!를 출력합니다.
예를 들어 input이 Bohemian Rhapsody, Queen, Nature Documentary인 경우:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Media* pointer를 통해 play()를 call하면 virtual function 덕분에 올바른 derived class method가 호출된다는 점에 주목하세요. 또한 virtual destructor가 적절한 정리를 보장하는 방식도 살펴보세요. base pointer를 통해 Delete할 때 derived와 base destructor가 모두 실행됩니다.
직접 해보기
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: 여기에 Media를 공개 상속하는 VideoClip 클래스를 정의하세요
// - private int duration 멤버 (초 단위)
// - 생성자는 title과 duration을 받습니다 (기본 duration은 120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: 생성자를 구현하세요
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: duration을 저장하세요
}
// TODO: play() 메서드를 오버라이드하세요
void play() override {
// TODO: 이 메서드를 구현하세요
}
// TODO: 소멸자를 구현하세요
~VideoClip() {
// TODO: 이 소멸자를 구현하세요
}
};
int main() {
// 입력을 읽습니다
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: 중괄호로 블록 스코프를 만드세요
// 블록 내부:
// 1. Media* 포인터 세 개의 배열을 생성합니다
// 2. 동적으로 할당:
// - A base Media object with title "Generic Media"
// - 입력 제목과 아티스트로 AudioTrack 객체
// - 입력 비디오 제목으로 VideoClip 객체
// 3. 배열을 순회하며 각 포인터에 play()를 호출합니다
// 4. 모든 객체를 역순으로 삭제합니다
// TODO: After the block, print: Playback complete!
return 0;
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
직접 연습해 보세요: 온라인 C++ 컴파일러