나만의 작은 도서관
[C++][Class] 오버라이딩과 가상 함수 본문
오버라이딩(overriding)이란?

- 오버라이딩은 기반 클래스에 정의된 함수를 파생 클래스에서 “재정의”하는 것을 의미한다.
- 오버라이딩한 함수는 부모 클래스의 함수와 다른 함수로 취급한다.
- 시그니처가 같아도 다른 클래스에 정의되어 있으면 서로 다른 함수로 취급하기 때문.
- 객체는 같은 시그니처를 가진 함수가 여러 개 정의되어 있는 경우, 가장 가까운 함수를 호출한다.
- 즉, 오버라이딩한 파생 클래스의 함수를 우선적으로 호출한다.
오버라이딩 하는 법
- 오버라이딩하는 방법은 간단하다. 기반 클래스에 정의한 함수의 시그니처와 일치하는 함수를 파생 클래스에 재정의하면 된다.
#include <iostream>
using namespace std;
class Base {
public:
void ShowString() {cout << str << '\\n';}
private:
string str = "Base"; // 같은 이름의 변수
};
class Derived : public Base {
public:
/* 함수 재정의(overriding) */
void ShowString() {cout << str << '\\n';}
private:
string str = "Derived"; // 같은 이름의 변수
};
int main(void){
Derived derived;
derived.ShowString(); // 실행 결과: Derived
return 0;
}
위 방식의 문제점 : 다형성 지원 X
- 위 예시처럼 오버라이딩 하면 다형성이 지원되지 않는 문제가 있다.
💡 다형성(polymorphism)은 하나의 메서드를 호출했음에도 불구하고 여러 가지 다른 작업들을 하는 것을 말합니다.
- 다형성이 지원되지 않는 경우, 여러 종류의 파생 클래스들을 업캐스팅하여 하나의 자료 구조로 관리할 때(vector <Base>와 같이) 파생 클래스의 멤버 함수를 호출하는 것이 굉장히 까다로워진다.
// 다형성 지원 O
vector<Animal*> animals = {new Dog(), new Cat(), ...};
for(Animal* animal : animals)
animal.speak(); // Dog::speak(), Cat::speak() 등 파생 클래스의 멤버 함수 호출
// 다형성 지원 X
vector<Animal*> animals = {new Dog(), new Cat(), ...};
for(Animal* animal : animals){
/* animal.speak()는 Animal::speak()만 호출함 */
// 해결 방법)
// 1. animal이 원래 어떤 파생 클래스였는지 파악
// 2. 해당 클래스 멤버 함수 호출
}
가상 함수(virtual Function)
virtual void func1(); // 가상 함수 선언
- 가상 함수는 다형성을 지원하는 함수로, 호출 시 해당 객체의 기존 클래스 타입에 오버라이딩한 함수를 실행한다.
- 기존 클래스에 오버라이딩 하지 않았다면 기존 클래스 기준으로 “가장 가까운 위치에 정의된 함수”를 실행한다.
- 가상 함수 호출 시 실행될 함수는 "런타임 시점"에 결정된다.
- 이와 같이 실행될 내용이 컴파일 시점이 아닌 런타임 시점에 결정되는 것을 “동적 바인딩(dynamic binding)”이라고 부른다. (반대로, 일반 함수처럼 컴파일 시점에서 실행될 내용이 결정되는 것을 “정적 바인딩(static binding)”이라고 부른다.)
- 가상 함수는 멤버 함수 앞에 virtual 키워드를 붙여 정의한다.
- 참고로, 가상 함수의 virtual은 "가상 세계에서는 상황에 따라 다른 모습을 가질 수 있다"는 개념을 가져왔을 뿐 실제로 존재하지 않는 함수를 의미하는 것이 아니다. 멀쩡히 존재하는 함수이며, 실행도 문제없이 가능하다.
- 가상 함수를 파생 클래스에서 오버라이딩한 함수는 자동으로 가상 함수로 취급된다.
- 즉, 파생 클래스에서 virtual 키워드를 생략해도 함수는 여전히 가상 함수이다.
class Derived : public Base
{
/* 함수 재정의(overriding) */
/* virtual이 생략되어 있을뿐 가상 함수인건 똑같다*/
/*virtual*/ void func1();
}
가상 함수 예시
#include <iostream>
#include <vector>
using namespace std;
class Animal {
public:
virtual void show() {cout << "Animal" << '\\n';}
};
class Dog : public Animal {
public:
virtual void show() {cout << "Dog" << '\\n';}
};
class Cat : public Animal {
public:
virtual void show() {cout << "Cat" << '\\n';}
};
int main(void){
vector<Animal*> animals = { new Dog(), new Cat() };
for (Animal* animal : animals)
animal->show(); // 동적 바인딩 적용
return 0;
}
실행 결과
Dog
Cat
다형성을 위해 모든 함수를 가상 함수로 선언한다면?
- 딱히 문제 될 건 없다고 한다. 실제로 자바는 모든 함수가 기본값으로 가상 함수가 선언된다.
- 그럼에도 불구하고 C++ 이 가상 함수를 사용자가 “직접” 선언하도록 한 이유는 가상 함수 선언 시 약간의 오버헤드가 발생하기 때문이다. (C++은 성능을 중요시하기 때문에 오버헤드 발생은 최대한 줄이려고 한다.)
가상 함수 테이블(vtable)
- 가상 함수 테이블은 가상 함수가 하나라도 정의된 클래스에 대해, 컴파일러가 자동으로 생성하는 테이블이다.
- 가상 함수 테이블은 가상 함수 호출 시 실행할 함수를 결정할 때 사용한다.
가상 함수 테이블을 이용한 가상 함수 구현 원리
- vtable 생성 (컴파일 타임에 실행)
출처. https://cosyp.tistory.com/228 - 가상 함수가 하나라도 정의된 클래스에 대해, 컴파일러가 자동으로 vtable을 생성한다.
- vtable은 현재 클래스에서 사용가능한 모든 가상 함수(기반 또는 현재 클래스에 정의한 가상 함수)와 가상 함수를 호출했을 때 실행할 함수 주소를 매핑한 데이터를 저장한다. vtable을 만드는 과정은 다음과 같다.
- 기반 클래스의 vtable 생성 과정
- 현재 클래스에 정의한 가상 함수에 대한 매핑 정보를 vtable에 추가한다.
- 파생 클래스의 vtable 생성 과정
- 기반 클래스의 vtable을 그대로 복사해 온다.
- 파생 클래스에서 오버라이딩했다면, 오버라이딩한 함수가 실행되도록 vtable에서 해당 함수의 매핑 정보를 갱신한다.
- 파생 클래스에서 새로운 가상함수를 정의했다면, 해당 함수의 매핑 정보를 vtable 마지막 부분에 추가한다.
- 기반 클래스의 vtable 생성 과정
- vptr(가상 함수 테이블 포인터) 추가 (런타임에 실행)
- 객체 생성 시 vptr(가상 함수 테이블 포인터)을 추가한다. vptr은 해당 객체 타입의 vtable을 가리킨다.
- 동적 바인딩 (런타임에 실행)
출처. https://cosyp.tistory.com/228
- 객체의 멤버 함수를 호출했을 경우, 호출한 함수의 타입에 따라 다음과 같이 처리한다.
- 일반 함수 호출 시: 추가 과정 없이 현재 객체 타입에 정의된 함수를 호출한다.
- 가상 함수 호출 시: vptr을 따라 vtable에 매핑된 함수 주소를 찾아 실행한다.
- 객체의 멤버 함수를 호출했을 경우, 호출한 함수의 타입에 따라 다음과 같이 처리한다.
추가 내용 : vptr은 항상 메모리 할당 시 객체 타입의 vtable을 가리킨다.
- 동적 할당은 상관없지만, 정적 할당의 경우 static_cast <Base>(derived)와 같이 업캐스팅을 해도 임시 객체의 타입이 Base이므로 임시 객체의 vptr은 Base의 vtable을 가리킨다.
- 참고로, 업캐스팅으로 자식 클래스의 추가 정보가 사라지는 것을 "객체 잘림(Object Slicing)"이라고 부른다.
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {cout << "Base" << '\\n';}
};
class Derived : public Base {
public:
void show() override {cout << "Derived" << '\\n';}
};
int main(void){
Derived derived;
static_cast<Base>(derived).show(); // 실행 결과 : Base
return 0;
}
override 키워드(C++11)
virtual void func1() override;
- override 키워드는 기반 클래스의 가상 함수를 파생 클래스에서 오버라이딩할 때 사용하는 키워드이다.
- override 키워드는 반드시 적어줘야 하는 키워드는 아니다. 하지만 아래 2가지 역할을 하므로 최대한 적어주는 것이 좋다.
- 실수 방지
- override 키워드를 적어주면 기반 클래스에 정의한 가상 함수 시그니처와 다를 경우 컴파일 오류를 발생시킨다. 이는 사용자가 오버라이딩을 의도했지만 시그니처 실수로 일반 가상 함수를 정의하는 상황을 방지할 수 있다.
- 가독성 향상
- override 키워드를 적어주면 “해당 함수는 오버라이딩 함수임”을 명시적으로 표현할 수 있다.
- 실수 방지
class Base {
public:
virtual void show(int x) {} // 가상 함수 정의
};
class Derived : public Base {
public:
/* 함수 재정의(overriding) */
void show() override {} // 컴파일 오류 : 기반 클래스와 함수 시그니처 불일치.
};
final 키워드(C++11)
virtual void func1() final;
- final 키워드는 해당 가상 함수가 더 이상 파생된 클래스에서 오버라이딩될 수 없도록 금지할 때 사용한다.
- final 키워드는 override 키워드와 비슷하게 1) 실수를 방지하고, 2) 가독성을 높이는 역할을 한다.
class Base {
public:
/* 오버라이딩 금지 선언*/
virtual void show() const final { std::cout << "Base Final Message\\n"; }
};
class Derived : public Base {
public:
void show() const override { // 컴파일 오류 : final 함수는 재정의 불가.
std::cout << "Derived Message\\n";
}
};
가상 소멸자
일반 소멸자의 문제점
- 객체를 소멸시킬 경우, 소멸자는 현재 객체 타입을 기준으로 소멸자 → 기반 소멸자 → 기반의 기반 소멸자 → … 순으로 호출된다.
- 여기서 유의해야 할 점은 소멸자가 “현재 객체 타입을 기준으로” 호출된다는 것이다.
- 즉, 동적 할당한 객체를 업캐스팅한 상태에서 소멸시킬 경우, 기존 객체 타입의 소멸자가 호출되지 않아 동적 할당한 메모리가 해제되지 않는, 즉, 메모리 누수가 발생한다. (동적 할당한 객체는 소멸자가 호출되어야 해당 객체의 메모리가 해제된다.)
+) 참고로 정적 할당한 객체는 위 문제와 관련 없다. 왜냐하면 정적 할당한 메모리들은 리턴 시 전부 자동으로 해제되기 때문. (소멸자도 문제없이 호출된다.)
일반 소멸자 문제점 예시
#include <iostream>
using namespace std;
class Base {
public:
Base() { std::cout << "Base 생성\\n"; }
~Base() { std::cout << "Base 소멸\\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 생성\\n"; }
~Derived() { std::cout << "Derived 소멸\\n"; }
};
int main() {
Base* derived = new Derived(); // 업캐스팅
delete derived; // 동적 메모리 해제
}
실행 결과
Base 생성
Derived 생성
Base 소멸
메모리 누수 문제를 해결하는 가상 소멸자
- 가상 소멸자는 위와 같이 소멸자가 제대로 호출되지 않는 문제를 해결하기 위해 사용한다.
- 가상 소멸자는 소멸자 앞에 virtual 키워드를 붙여 사용하며, 최상위 클래스 한 곳에만 적용하면 된다.
가상 소멸자 예시
#include <iostream>
using namespace std;
class Base {
public:
Base() { std::cout << "Base 생성\\n"; }
virtual ~Base() { std::cout << "Base 소멸\\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 생성\\n"; }
~Derived() { std::cout << "Derived 소멸\\n"; }
};
int main() {
Base* derived = new Derived(); // 업캐스팅
delete derived; // 동적 메모리 해제
}
실행 결과
Base 생성
Derived 생성
Derived 소멸
Base 소멸
가상 소멸자 동작 과정
- 업캐스팅된 객체를 delete로 삭제
- delete에 의해 객체의 소멸자가 호출된다.
- 동적 바인딩(가상 소멸자인 경우)
- 소멸자가 가상 소멸자이므로, 현재 객체의 vtpr이 가리키는 vtable에 가서 매핑된 함수를 실행한다.
- vptr은 객체의 원래 타입의 vtable을 가리키므로, 실행할 함수는 객체의 원래 타입 소멸자가 된다.
- 소멸자가 가상 소멸자이므로, 현재 객체의 vtpr이 가리키는 vtable에 가서 매핑된 함수를 실행한다.
- 소멸자 호출 시 현재 객체가 관리하는 메모리를 정리한 다음, 기반 클래스의 소멸자를 호출한다.
- 최상위 기반 클래스가 호출될 때까지 3번 과정을 반복한다.
순수 가상 함수
virtual void func2() = 0; // 순수 가상 함수 선언
- 순수 가상 함수는 “무엇을 하는지 정의하지 않은 가상 함수”를 의미한다. 정의가 없는 가상 함수이므로, 상속 시 반드시 오버라이딩 해야 하는 함수이다.
- 순수 가상 함수는 기존 가상 함수 뒤에 ‘ = 0’을 붙여 선언한다.
- 순수 가상 함수는 인터페이스 역할을 하는 것이 주목적이다. 클래스는 순수 가상 함수를 통해 파생 클래스들에게 인터페이스를 넘겨줌으로써 구현을 강제한다.
추상 클래스(Abstract Class)란?

- 추상 클래스는 순수 가상 함수를 1개 이상 포함하고 있는 클래스를 의미한다.
- 추상 클래스의 경우, 해당 클래스 타입으로 객체를 만들 수 없다(C++ 자체에서 금지). 만약 사용자가 추상 클래스 객체를 생성하려 한다면 컴파일 오류가 발생한다.
class A
{
public:
virtual void func2() = 0;
};
int main(void)
{
A a; // 컴파일 에러: 추상 클래스 형식 "A"의 개체를 사용할 수 없습니다.
}
- 하지만 추상 클래스로 업캐스팅하는 것은 가능하므로, 서로 다른 파생 클래스들을 추상 클래스로 업캐스팅하여 사용할 수 있다. (다형성 지원)
#include <iostream>
#include <vector>
using namespace std;
class Animal {
public:
virtual void show() = 0; // 순수 가상 함수
};
class Dog : public Animal {
public:
void show() override {cout << "Dog" << '\\n';}
};
class Cat : public Animal {
public:
void show() override {cout << "Cat" << '\\n';}
};
int main(void){
vector<Animal*> animals = { new Dog(), new Cat() };
for (Animal* animal : animals)
animal->show(); // 동적 바인딩 적용
return 0;
}
실행 결과
Dog
Cat
추상 클래스의 의의
- 일종의 "설계도" 역할을 한다.
- 즉, 추상 클래스를 사용하면 코드를 사용하는 사람에게 “이 기능은 일반적인 상황에서 정의 내리기 힘드니 네가 직접 특수화해서 사용해라”라고 의도를 남겨줄 수 있다는 것에 의미가 있다.
참고 자료
https://kr.pinterest.com/pin/abstract-class-in-c-language--786370784919328121/
https://www.geeksforgeeks.org/function-overriding-in-cpp/
https://junk-s.tistory.com/entry/C순수-가상함수와-추상-클래스-인터페이스 interface
https://gloomystudy.tistory.com/41
https://learn.microsoft.com/ko-kr/cpp/cpp/abstract-classes-cpp?view=msvc-170
'C++ > 문법 및 메소드(STL)' 카테고리의 다른 글
[C++] 캐스팅(Casting) (0) | 2025.03.28 |
---|---|
[C++][Class] 클래스 관련 기타 내용(explicit, mutable, friend 키워드 등...) (0) | 2025.03.26 |
[C++][Class] 상속(Inheritance) (0) | 2025.03.26 |
[C++][Class] 연산자 오버로딩(Operator Overloading) (0) | 2025.03.25 |
[C++][Class] 함수 / 생성자 오버로딩(overloading) (0) | 2025.03.25 |