가상 함수
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.
1. 개요
가상 함수는 객체 지향 프로그래밍에서 파생 클래스가 기본 클래스를 상속할 때, 기본 클래스 포인터 또는 참조를 통해 파생 클래스 객체를 참조할 경우, 런타임에 호출될 함수를 결정하는 데 사용되는 함수이다. C++에서 `virtual` 키워드를 사용하여 선언되며, 이를 통해 컴파일 시점에 알 수 없는 함수를 호출할 수 있다. 가상 함수는 동적 디스패치를 가능하게 하며, 추상 클래스와 순수 가상 함수, 가상 소멸자와도 관련이 있다.
객체 지향 프로그래밍(OOP)에서 상속 관계가 있을 때, 기초 클래스(기본 클래스) 타입의 포인터나 참조를 통해 유도 클래스(파생 클래스) 객체를 다루는 경우가 흔하다. 이때 파생 클래스에서 기본 클래스의 메서드를 재정의(override)했다면, 해당 포인터나 참조로 메서드를 호출했을 때 어떤 버전의 메서드(기본 클래스 또는 파생 클래스)가 실행될지 결정해야 하는 문제가 발생한다.
2. 목적
가상 함수는 이 문제를 해결하기 위한 핵심 메커니즘이다. 가상 함수를 사용하면 메서드 호출이 컴파일 시점이 아닌 프로그램 실행 시점(런타임)에 객체의 실제 타입을 기준으로 결정된다(이를 후기 바인딩 또는 동적 디스패치라고 한다). 즉, 기본 클래스 포인터로 파생 클래스 객체를 가리키더라도, 가상 함수를 호출하면 파생 클래스에서 재정의한 메서드가 실행되도록 보장한다.
이러한 후기 바인딩을 통해 가상 함수는 다형성을 효과적으로 구현할 수 있게 한다. 프로그램은 실행 중에 결정되는 객체의 구체적인 타입에 맞춰 적절한 동작을 수행할 수 있으며, 이는 코드의 유연성과 확장성을 크게 향상시킨다. 결과적으로 코드 재사용성이 높아지고 유지보수가 용이해지는 장점을 가진다. 또한, 가상 함수를 이용하면 프로그램이 컴파일될 당시에는 존재하지 않았던 파생 클래스의 메서드도 호출하는 것이 가능해진다.
2. 1. 동적 디스패치
객체 지향 프로그래밍(OOP)에서는 상속 관계에서 파생 클래스(유도 클래스)의 객체를 기본 클래스(기초 클래스) 타입의 포인터나 참조를 통해 다룰 수 있다. 이때, 파생 클래스에서 기본 클래스의 메서드를 재정의(override)했다면, 포인터나 참조를 통해 호출되는 메서드가 어떤 버전(기본 클래스 버전 또는 파생 클래스 버전)으로 실행될지 결정해야 한다.
이 결정 방식에는 두 가지가 있다.
가상 함수(Virtual Function)는 바로 이 후기 바인딩(동적 디스패치)을 가능하게 하는 핵심 메커니즘이다. 기본 클래스에서 특정 함수를 가상 함수로 선언하면, 해당 함수 호출은 런타임에 객체의 실제 타입에 따라 결정된다. 즉, 기본 클래스 포인터나 참조를 사용하더라도, 실제 객체가 파생 클래스의 인스턴스라면 파생 클래스에서 재정의한 버전의 함수가 호출되는 것이다. 만약 함수가 가상 함수가 아니라면, 초기 바인딩 방식으로 동작하여 포인터나 참조의 선언된 타입(기본 클래스)의 함수가 호출된다.
가상 함수를 사용하면 코드가 컴파일되는 시점에는 존재하지 않을 수도 있는 메서드를 프로그램에서 호출할 수 있다.
C++에서는 기본 클래스에서 함수의 선언 앞에 `virtual` 키워드를 붙여 가상 함수를 선언한다. 이 `virtual` 속성은 파생 클래스로 상속되므로, 파생 클래스에서 해당 함수를 재정의하면 자동으로 가상 함수가 되어 후기 바인딩 방식으로 동작한다. 기본 클래스 내에서 가상 함수를 호출하는 경우에도, 실제 객체 타입에 따라 파생 클래스의 재정의된 함수가 대신 호출될 수 있다.
참고로, 오버라이딩(Overriding)은 파생 클래스에서 기본 클래스와 동일한 이름과 매개변수를 가진 메서드를 재정의하는 것을 의미하며, 이는 동적 디스패치(동적 함수 매핑)와 관련이 깊다. 반면, 오버로딩(Overloading)은 하나의 클래스 내에서 메서드 이름은 같지만 매개변수 목록이 다른 여러 메서드를 정의하는 것으로, 함수 매칭이라고도 불린다.
3. 예시

가상 함수는 객체 지향 프로그래밍에서 다형성을 구현하는 중요한 메커니즘 중 하나이다. 예를 들어, `Animal`이라는 기초 클래스가 있고, 이 클래스에 `eat`이라는 가상 함수가 있다고 가정해 보자. `Animal` 클래스를 상속받는 하위 클래스인 `Llama`는 `eat` 함수를 자신만의 방식으로 재정의할 수 있다.
#include
class Animal {
public:
// 비가상 함수: 컴파일 시점에 호출될 함수 결정 (정적 바인딩)
void move() {
std::cout << "이 동물은 어떤 방식으로 움직입니다" << std::endl;
}
// 가상 함수: 런타임에 호출될 함수 결정 (동적 바인딩)
virtual void eat() {
// 기초 클래스에서도 기본 구현을 제공할 수 있다.
std::cout << "동물이 무언가를 먹습니다." << std::endl;
}
// 가상 소멸자는 파생 클래스 객체를 기초 클래스 포인터로 다룰 때 중요하지만, 여기서는 설명을 생략한다.
virtual ~Animal() {}
};
class Llama : public Animal {
public:
// eat 함수를 재정의 (C++11부터는 override 키워드 사용 권장)
void eat() override {
std::cout << "라마는 풀을 먹습니다!" << std::endl;
}
};
이렇게 정의된 클래스들을 사용할 때, `Animal` 타입의 포인터나 참조 변수가 실제로는 `Llama` 인스턴스를 가리키고 있더라도, 해당 포인터를 통해 `eat()` 함수를 호출하면 `Llama` 클래스에서 재정의된 `eat()` 함수가 실행된다. 이는 `eat` 함수가 가상 함수이기 때문에 런타임에 실제 객체의 타입을 확인하여 해당 타입에 맞는 함수를 호출하기 때문이다(이를 동적 바인딩이라고 한다). 반면, `move` 함수처럼 가상 함수가 아닌 경우에는 포인터 변수의 타입(`Animal`)에 해당하는 클래스의 함수가 컴파일 시점에 결정되어 호출된다(이를 정적 바인딩이라고 한다).
이러한 특징 덕분에 프로그래머는 여러 종류의 `Animal` 객체들(예: `Llama`, `Wolf` 등)을 `Animal` 포인터 배열이나 리스트에 담아 일관된 방식으로 처리할 수 있다. 각 객체의 구체적인 타입을 일일이 알 필요 없이 단순히 `eat()` 함수를 호출하면, 실제 객체 타입에 맞는 동작이 수행되는 것이다. 이는 코드의 유연성과 확장성을 크게 향상시킨다.
가상 함수는 C++, 자바 등 여러 객체 지향 언어에서 지원하며, C 언어와 같이 직접적인 지원이 없는 경우에도 함수 포인터 등을 이용하여 유사한 메커니즘을 구현할 수 있다. 각 언어별 구체적인 구현 방식과 특징은 아래 하위 섹션에서 자세히 다룬다.
3. 1. C++
C++에서는 기초 클래스에서 함수의 선언 앞에 `virtual` 키워드를 붙여 가상 함수를 선언한다. 이 키워드는 해당 함수가 파생 클래스에서 재정의될 수 있으며, 호출될 함수가 런타임에 결정된다는 것을 의미한다. 한번 `virtual`로 선언된 함수는 파생 클래스에서 명시적으로 다시 `virtual` 키워드를 사용하지 않아도 가상 함수로 취급되며, 이 속성은 계속 상속된다.
예를 들어, `Animal`이라는 기초 클래스에 `eat`이라는 가상 함수를 정의할 수 있다. `Animal`을 상속받는 `Llama` 클래스는 `eat` 함수를 자신만의 방식으로 구현할 수 있다. 이때 `Animal` 타입의 포인터나 참조를 통해 `Llama` 객체의 `eat` 함수를 호출하면, 실제로 실행되는 것은 `Llama` 클래스에서 재정의된 `eat` 함수이다. 이는 객체의 실제 타입에 따라 호출될 함수가 동적으로 결정되기 때문이다. 반면, `virtual` 키워드가 없는 함수(예: `move`)는 포인터나 참조의 타입에 따라 컴파일 시점에 호출될 함수가 결정된다.
아래는 C++ 코드 예시이다.
#include
class Animal {
public:
// 비가상 함수: 컴파일 시점에 호출될 함수 결정
void move() {
std::cout << "이 동물은 어떤 방식으로 움직입니다" << std::endl;
}
// 가상 함수: 런타임에 호출될 함수 결정
virtual void eat() {
std::cout << "동물이 무언가를 먹습니다" << std::endl; // 기본 구현 제공 가능
}
// 순수 가상 함수 예시 (선언만 있고 구현은 없음, 파생 클래스에서 반드시 구현해야 함)
// virtual void speak() = 0;
// 가상 소멸자: 기초 클래스 포인터를 통해 파생 클래스 객체 삭제 시 필요
virtual ~Animal() {}
};
class Llama : public Animal {
public:
// eat 함수를 재정의 (override 키워드는 선택 사항이지만 권장됨)
void eat() override {
std::cout << "라마는 풀을 먹습니다!" << std::endl;
}
};
int main() {
Animal* animalPtr; // 기초 클래스 포인터
Llama myLlama; // 파생 클래스 객체
animalPtr = &myLlama; // 포인터에 파생 클래스 객체의 주소 할당
animalPtr->move(); // Animal::move() 호출 (비가상 함수)
animalPtr->eat(); // Llama::eat() 호출 (가상 함수)
return 0;
}
이러한 가상 함수의 특징 덕분에 프로그래머는 다양한 `Animal` 파생 클래스 객체들을 `Animal` 포인터 배열이나 리스트에 담아 관리할 수 있다. 각 객체의 구체적인 타입이 무엇인지, 또는 각 동물이 어떻게 먹는지 일일이 알 필요 없이, 단순히 각 포인터에 대해 `eat()` 함수를 호출하면 해당 객체의 실제 타입에 맞는 `eat()` 동작이 실행된다. 이는 코드의 유연성과 확장성을 높이는 중요한 객체 지향 프로그래밍의 개념이다.
3. 2. C
C 언어는 객체 지향 프로그래밍에서 사용되는 가상 함수를 직접적으로 지원하지 않는다. 하지만 함수 포인터와 구조체를 활용하여 가상 함수와 유사한 메커니즘을 구현하는 것이 가능하다.
다음은 C 언어에서 가상 함수를 구현하는 예시이다. 객체를 나타내는 구조체(`Animal`)와 가상 함수 테이블 역할을 하는 구조체(`AnimalVTable`)를 정의한다. 객체 구조체는 가상 함수 테이블을 가리키는 포인터(`vtable`)를 멤버로 가진다.
#include
/* 객체 구조체 정의. 가상 함수 테이블 포인터를 포함한다. */
struct Animal {
const struct AnimalVTable *vtable;
};
/* 가상 함수 테이블 구조체 정의. 함수 포인터를 멤버로 가진다. */
struct AnimalVTable {
void (*Eat)(struct Animal *self); // '가상' 함수 포인터
};
/*
void Move(const struct Animal *self) {
printf("
}
/*
void Eat(struct Animal *self) {
const struct AnimalVTable *vtable = *(const void **)(self); // 객체에서 vtable 주소를 가져온다.
if (vtable->Eat != NULL) {
(*vtable->Eat)(self); // vtable에 등록된 Eat 함수를 호출한다.
} else {
fprintf(stderr, "'Eat' 가상 메서드가 구현되지 않았습니다\n");
}
}
/* Llama 타입의 Eat 함수 구현. Animal 구조체를 인자로 받는다. */
static void _Llama_eat(struct Animal *self) {
printf("
}
/* 각 타입별 가상 함수 테이블 초기화 */
const struct AnimalVTable Animal = { NULL }; // 기본 Animal 타입은 Eat을 구현하지 않는다.
const struct AnimalVTable Llama = { _Llama_eat }; // Llama 타입은 _Llama_eat 함수를 Eat으로 사용한다.
int main(void) {
/* 각 타입의 객체(인스턴스) 생성 및 초기화 */
struct Animal animal = { &Animal }; // Animal 객체 생성 및 Animal vtable 연결
struct Animal llama = { &Llama }; // Llama 객체 생성 및 Llama vtable 연결
Move(&animal); // Animal 타입의 Move 함수 호출 (일반 함수)
Move(&llama); // Llama 타입이지만 Animal의 Move 함수 호출 (상속과 유사)
Eat(&animal); // Animal 객체의 Eat 호출 -> vtable에 NULL이므로 "Not Implemented" 출력
Eat(&llama); // Llama 객체의 Eat 호출 -> vtable의 _Llama_eat 함수 호출
return 0;
}
위 코드에서 `Eat` 함수는 객체(`self`)가 가지고 있는 `vtable` 포인터를 통해 실제 호출할 함수를 런타임에 찾아서 실행한다. `animal` 객체는 `Animal` 가상 함수 테이블을 가리키고, 이 테이블의 `Eat` 포인터는 `NULL`이므로 "Not Implemented" 메시지가 출력된다. 반면 `llama` 객체는 `Llama` 가상 함수 테이블을 가리키고, 이 테이블의 `Eat` 포인터는 `_Llama_eat` 함수를 가리키므로 해당 함수가 실행되어 "Llamas는 풀을 먹습니다!"가 출력된다. 이처럼 함수 포인터를 이용하여 C 언어에서도 다형성(polymorphism)과 유사한 효과를 얻을 수 있다.
3. 3. Java
자바에서는 기본적으로 모든 메서드가 가상 함수처럼 동작한다. 즉, 하위 클래스에서 메서드를 재정의하면, 상위 클래스 타입의 참조 변수를 사용하더라도 실제 객체의 재정의된 메서드가 호출된다. 다만, `final` 키워드로 선언된 메서드, `static` 키워드로 선언된 정적 메서드, 그리고 `private` 접근 제한자를 가진 메서드는 가상 함수처럼 동작하지 않고 재정의될 수 없다.
또한 자바는 인터페이스라는 별도의 메커니즘을 제공한다. 인터페이스는 추상 메서드 (구현이 없는 메서드)들의 집합을 정의하는 데 사용될 수 있으며, 클래스는 인터페이스를 '구현(implements)'함으로써 인터페이스에 선언된 모든 추상 메서드를 반드시 구현해야 한다. 이는 C++에서 다중 상속을 지원하지 않는 대신, 순수 가상 함수만 가진 추상 클래스를 인터페이스처럼 사용하는 것과 유사한 역할을 수행하기 위해 제공되는 기능이다.
4. 추상 클래스와 순수 가상 함수
'''순수 가상 함수'''(pure virtual function영어) 또는 '''순수 가상 메서드'''(pure virtual method영어)는 하위 클래스에서 반드시 구현해야 하는 특별한 종류의 가상 함수이다. 만약 하위 클래스가 추상적이지 않다면, 이 함수를 반드시 구현해야 한다.
하나 이상의 순수 가상 함수를 포함하는 클래스를 '''추상 클래스'''(abstract class영어)라고 부른다. 추상 클래스는 설계도와 같은 역할을 하여 직접 객체(인스턴스)를 만들 수 없다. 즉, 직접 인스턴스화될 수 없다. 오직 추상 클래스를 상속받고 모든 순수 가상 함수를 구현한 하위 클래스만이 인스턴스화될 수 있다. 일반적으로 순수 가상 함수는 함수의 이름과 매개변수 등 선언(시그니처)만 가지고 있으며, 구체적인 동작 방식(구현)은 정의되지 않는다.
예를 들어, '도형'이라는 추상 클래스가 '넓이 계산'이라는 순수 가상 함수를 가질 수 있다. '원', '사각형'과 같은 하위 클래스들은 각자의 방식대로 '넓이 계산' 함수를 구체적으로 구현해야 한다. '도형' 자체는 추상적인 개념이므로 '넓이 계산'을 직접 정의할 수 없다.
순수 가상 함수는 클래스가 어떤 기능을 반드시 가져야 하는지를 명시하는 인터페이스 역할을 하기도 한다. 이는 자바와 같은 언어에서 `interface` 키워드를 사용하는 것과 유사하다. 특히 다중 상속을 지원하지 않는 언어에서는 이러한 인터페이스 메커니즘이 중요하게 사용된다.
4. 1. C++ 에서의 추상 클래스
C++에서 '''순수 가상 함수'''(pure virtual function영어) 또는 '''순수 가상 메서드'''(pure virtual method영어)는 파생 클래스가 추상적이지 않은 경우, 해당 파생 클래스에서 반드시 구현해야 하는 가상 함수이다. 하나 이상의 순수 가상 함수를 포함하는 클래스는 "추상 클래스"라고 불리며, 직접 인스턴스화될 수 없다. 서브클래스는 자신 또는 상위 클래스로부터 상속받은 모든 순수 가상 함수를 구현해야만 직접 인스턴스화가 가능하다. 순수 가상 함수는 일반적으로 선언(시그니처)만 가지며, 정의(구현)는 가지지 않는다.예를 들어, 추상 기본 클래스
MathSymbol은 순수 가상 함수 doOperation()을 제공하고, 파생 클래스인 Plus와 Minus는 구체적인 연산을 정의하기 위해 doOperation()을 구현해야 한다. MathSymbol 자체는 추상적인 개념이므로 doOperation()을 구현하는 것이 의미가 없다. 마찬가지로, MathSymbol의 서브클래스도 doOperation() 구현 없이는 완전하다고 할 수 없다.일반적으로 순수 가상 함수는 선언된 클래스 내에 구현을 가지지 않지만, C++에서는 필요한 경우 선언 클래스 내에 구현을 포함하는 것을 허용한다. 이는 파생 클래스가 해당 구현을 기본 동작이나 대비책으로 사용할 수 있게 한다.[5][6]
순수 가상 함수는 메서드 선언을 통해 인터페이스를 정의하는 데 사용될 수도 있다. 이는 자바의 `interface` 키워드와 유사한 역할을 한다. 이러한 방식으로 사용될 때, 파생 클래스는 인터페이스에 정의된 모든 기능을 구현해야 한다. 특정 디자인 패턴에서는 인터페이스 역할을 하는 추상 클래스가 오직 순수 가상 함수만을 포함하고, 데이터 멤버나 일반 메서드는 갖지 않기도 한다. C++에서는 이처럼 순수 추상 클래스를 인터페이스처럼 활용하는 것이 가능한데, 이는 C++가 다중 상속을 지원하기 때문이다. 반면, 많은 다른 객체 지향 프로그래밍 언어는 다중 상속을 지원하지 않기 때문에, 자바처럼 별도의 인터페이스 메커니즘을 제공하는 경우가 많다.
5. 생성과 소멸 사이의 행위
객체의 생성자나 소멸자가 실행되는 동안 가상 함수를 호출할 때, 프로그래밍 언어마다 동작 방식이 다를 수 있다. 몇몇 언어, 예를 들어 C++에서는 객체의 생성과 소멸 과정에서 가상 디스패치 메커니즘이 일반적인 상황과 다르게 동작하므로 생성자나 소멸자 안에서 가상 함수를 호출하는 것은 일반적으로 권장되지 않는다.[12]
반면, Java나 C#과 같은 다른 언어에서는 생성 과정에서도 파생 클래스의 구현(메서드)이 호출될 수 있다.[11] 이러한 특징은 추상 팩토리 패턴과 같은 일부 디자인 패턴에서 의도적으로 활용되기도 한다.[11]
이처럼 언어별로 동작 방식에 차이가 존재하므로, 생성자나 소멸자 내에서 가상 함수를 사용할 때에는 해당 언어의 명세를 정확히 이해하고 주의 깊게 접근해야 한다.
5. 1. C++ 에서의 생성/소멸 시 행위
C++에서는 객체의 생성자나 소멸자 실행 중 가상 함수를 호출할 때 다른 언어와 다른 방식으로 동작한다. 이러한 이유로 생성자나 소멸자 안에서 가상 함수를 호출하는 것은 일반적으로 권장되지 않는다.[12]구체적으로, 생성자나 소멸자 내에서 가상 함수를 호출하면, 현재 생성 또는 소멸 중인 클래스에 정의된 함수가 호출된다. 즉, 상속 계층 구조에서 더 하위에 있는 파생 클래스의 재정의된 함수가 호출되는 일반적인 동적 디스패치 메커니즘이 적용되지 않는다.[7][8][9] 이를 해당 클래스 수준에서의 "기저(base)" 함수가 호출된다고 설명하기도 한다.
만약 호출되는 함수가 순수 가상 함수라면, 정의되지 않은 동작(undefined behavior)이 발생한다.[7][8] 설령 해당 클래스 내에 순수 가상 함수에 대한 구현이 존재하더라도, 명시적으로 해당 구현을 지정하여 호출하지 않는 이상 이는 동일하게 적용된다.[10] C++ 구현체(컴파일러, 링커 등)는 컴파일 타임이나 링크 타임에 이러한 순수 가상 함수 호출을 반드시 감지하지는 못하며, 감지하는 것이 불가능한 경우도 많다. 일부 런타임 시스템은 프로그램 실행 중 이러한 호출을 감지하면 오류를 발생시키기도 한다.
5. 2. Java 및 C# 에서의 생성/소멸 시 행위
Java와 C#에서는 객체의 생성자나 소멸자 실행 중에 가상 함수(메서드)를 호출하면, 파생된 클래스의 구현(오버라이드된 메서드)이 호출된다.[11] 이는 C++에서 생성자나 소멸자 실행 시점에는 해당 클래스 타입에 맞는 함수가 호출되는 것과는 다른 동작 방식이다.[7][8][9]하지만 Java와 C# 방식에서는 주의할 점이 있다. 가상 함수 호출 시점에는 아직 파생 클래스의 생성자가 완전히 실행되지 않았을 수 있으므로, 파생 클래스에서 선언된 필드들이 기대하는 값으로 초기화되지 않고 기본값(예: 숫자형의 경우 0, 객체 참조의 경우 null) 상태일 수 있다.[11] 이 때문에 초기화되지 않은 필드에 접근하여 예기치 않은 동작이 발생할 위험이 있다.
그럼에도 불구하고, 추상 팩토리 패턴과 같은 일부 디자인 패턴에서는 생성 과정에서 파생 클래스의 메서드를 호출하는 이러한 기능을 의도적으로 활용하여 객체 생성을 유연하게 처리하도록 권장하기도 한다.[11]
6. 가상 소멸자
객체 지향 프로그래밍 언어는 일반적으로 객체가 생성 및 소멸될 때 메모리 관리를 자동으로 처리한다. 자동 메모리 관리 환경에서는 객체의 실제 타입에 맞는 소멸자(때로는 'finalizer'라고도 함)가 호출되는 것이 보장된다. 예를 들어, Animal 클래스를 상속하는 Wolf 클래스가 있고 두 클래스 모두 소멸자를 가질 때, `Wolf` 객체가 소멸되면 `Wolf`의 소멸자가 호출된다.
그러나 C++와 같이 수동 메모리 관리를 사용하는 언어에서는 개발자가 소멸자 호출을 직접 관리해야 하며, 특히 상속 관계와 포인터를 사용할 때 문제가 발생할 수 있다. 기본 클래스 포인터로 파생 클래스 객체를 가리키고 있다가 해당 포인터를 통해 객체를 삭제할 경우, 기본 클래스의 소멸자가 가상(virtual)으로 선언되지 않았다면 파생 클래스의 소멸자가 호출되지 않을 수 있다. 이는 자원 누수 등 예기치 않은 문제를 일으킬 수 있으며, 프로그래밍 오류의 흔한 원인이 된다. 따라서 상속 구조에서 다형성을 활용하며 객체를 소멸시켜야 할 때는 가상 소멸자의 사용이 중요하다.
6. 1. C++ 에서의 가상 소멸자
일반적으로 객체 지향 프로그래밍 언어는 객체가 생성되거나 소멸될 때 자동으로 메모리를 할당하고 해제하는 기능을 제공한다. 하지만 C++와 같이 수동으로 메모리를 관리하는 언어에서는 개발자가 직접 소멸자를 구현하고 호출 시점을 관리해야 하는 경우가 있다.특히 수동 메모리 관리 환경에서 정적 디스패치와 결합될 때 소멸자 호출과 관련하여 문제가 발생할 수 있다. 예를 들어, Animal이라는 기본 클래스를 상속받는 Wolf라는 파생 클래스가 있다고 가정해 보자. 두 클래스 모두 각자의 소멸자를 가지고 있을 때, `Wolf` 타입의 객체를 생성한 뒤 이를 `Animal` 타입의 포인터로 가리키게 하고, 이 `Animal` 포인터를 사용하여 객체를 삭제(`delete`)하면 문제가 생길 수 있다. 만약 `Animal` 클래스의 소멸자가 가상(`virtual`)으로 선언되지 않았다면, 컴파일러는 포인터의 타입(`Animal`)에 해당하는 소멸자만 호출한다. 즉, `Animal`의 소멸자만 실행되고 `Wolf`의 소멸자는 실행되지 않아, `Wolf` 객체를 위해 특별히 할당된 자원이 해제되지 않는 등의 문제가 발생할 수 있다.
이러한 문제를 방지하기 위해 C++에서는 기본 클래스의 소멸자를 `virtual` 키워드를 사용하여 가상 소멸자로 선언하는 것이 중요하다. 기본 클래스의 소멸자가 가상 소멸자이면, 기본 클래스 포인터를 통해 파생 클래스 객체를 삭제하더라도 프로그램 실행 시점에 객체의 실제 타입(`Wolf`)을 확인하여 해당 타입의 소멸자(`Wolf`의 소멸자)를 먼저 호출하고, 그 다음 기본 클래스(`Animal`)의 소멸자를 순서대로 호출한다. 이렇게 함으로써 파생 클래스 객체가 안전하게 소멸되고 관련된 모든 자원이 올바르게 해제될 수 있다. 기본 클래스에서 소멸자를 가상으로 선언하지 않는 것은 C++ 프로그래밍에서 흔히 발생하는 오류의 원인 중 하나이다.
참조
[1]
웹사이트
Polymorphism (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance)
https://docs.oracle.[...]
2020-07-11
[2]
웹사이트
9. Classes — Python 3.9.2 documentation
https://docs.python.[...]
2021-02-23
[3]
웹사이트
Writing Final Classes and Methods (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance)
https://docs.oracle.[...]
2020-07-11
[4]
웹사이트
PHP: Final Keyword - Manual
https://www.php.net/[...]
2020-07-11
[5]
웹사이트
Pure virtual destructors - cppreference.com
https://en.cpprefere[...]
[6]
웹사이트
abc — Abstract Base Classes: @abc.abstractmethod
https://docs.python.[...]
[7]
웹사이트
N4659: Working Draft, Standard for Programming Language C++
http://www.open-std.[...]
[8]
웹사이트
What is __purecall?
https://devblogs.mic[...]
2004-04-28
[9]
웹사이트
Never Call Virtual Functions during Construction or Destruction
http://www.artima.co[...]
2005-06-06
[10]
웹사이트
C++ corner case: You can implement pure virtual functions in the base class
https://devblogs.mic[...]
2013-10-11
[11]
웹사이트
Joy of Programming: Calling Virtual Functions from Constructors
https://www.opensour[...]
2011-08-01
[12]
웹인용
Never Call Virtual Functions during Construction or Destruction
http://www.artima.co[...]
2005-06-06
본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.
문의하기 : help@durumis.com