[기술 질문] #15_가상 함수
C++의 가상 함수에 대해 알아보겠습니다.
Overview
- 개념
- 가상 소멸자
- 추상 클래스
- 인터페이스
#0. 개념
1. 가상 함수
- [정의] : C++의 가상 함수는 기본 클래스에서 virtual 예약어와 함께 선언되며, 파생 클래스에서 오버라이드(재정의)할 수 있습니다. 컴파일러는 해당 함수를 가상 함수로 처리하고 객체의 타입에 따라 해당 함수의 호출 대상을 동적으로 결정할 수 있습니다. 결과적으로, 가상 함수의 사용은 객체의 타입에 관계없이 일관된 인터페이스를 제공하며, 파생 클래스의 재정의를 통해 유연하게 확장할 수 있도록 해줍니다.
- [특징] : 가상 함수는 다형성을 구현하는 중요한 메커니즘입니다. 가상화된 멤버 함수는 파생 클래스에서 오버라이딩 할 수 있으며, 같은 이름의 멤버 함수가 하나의 클래스 유형에 귀속되지 않는 다형성을 제공함과 동시에 코드 작성의 유연함과 확장성 또한 제공합니다.
2. 가상화 조건
- 가상 함수는 기본 클래스에서 virtual 예약어와 함께 선언해야 합니다. virtual 키워드 없이 선언된 비 가상 멤버 함수는 동적 바인딩 되지 않기 때문에, 객체의 타입에 관계없이 항상 기본 클래스의 것으로 호출됩니다.
- 가상 함수는 파생 클래스에서 오버라이딩 할수 있습니다. 이때, 함수 시그니처의 변경은 불가능합니다.
https://webddevys.tistory.com/72
3. 가상 테이블, vtable
- [개념] : 가상 함수는 가상 함수 테이블(vtable)을 통해 구현됩니다. 가상 함수 테이블은 가상 함수를 포함하는 클래스의 인스턴스마다 생성됩니다. 이 테이블은 가상 함수의 주소들을 저장하는 배열입니다. 컴파일러는 가상 함수 호출 시 해당 함수의 주소를 vtable에서 검색하여 호출 대상 함수를 동적으로 결정합니다.
- [특징] : 가상 함수 테이블은 기본 클래스와 파생 클래스 모두에 독립적으로 생성됩니다. 가상 함수 호출 시 객체의 타입에 따라 동적으로 호출 대상 함수가 결정됩니다.
4. 예제 코드
#include <iostream>
class Animal {
public:
virtual void speak() {
std::cout << "Animal speaks!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Animal* animals[] = { new Animal(), new Cat(), new Dog() };
for (int i = 0; i < 3; i++) {
animals[i]->speak();
}
for (int i = 0; i < 3; i++) {
delete animals[i];
}
}
Details
- Animal 기본 클래스를 상속하는 Cat 클래스와 Dog 클래스는 각각 Animal 클래스의 가상 멤버 함수 speak()를 오버라이딩(재정의) 합니다.
- 위처럼 가상 함수는 객체 타입에 관계없이 일관된 인터페이스를 유지하며, 재정의 동작을 통해 유연하게 확장이 가능합니다.
4. 그 외 주의할 점
#1. 가상 소멸자
1. 가상 소멸자
#include <iostream>
class Animal {
public:
virtual ~Animal() {
std::cout << "Animal destroyed!" << std::endl;
}
};
class Cat : public Animal {
public:
~Cat() override {
std::cout << "Cat destroyed!" << std::endl;
}
};
int main() {
Animal* animal = new Cat();
delete animal;
}
Details
- [개념] : 가상 소멸자는 C++ 코드에서 다형성을 구현하는 데 중요한 개념 중 하나입니다. 가상 소멸자 사용을 통해 기본 클래스의 포인터가 파생 클래스의 객체를 가리킬 때, 파생 클래스의 소멸자가 호출되고, 기본 클래스의 소멸자가 정호출되어 정상적으로 객체의 완전한 소멸이 가능합니다. 가상 소멸자는 호출 시점에 다형성을 지원하는 기본 클래스를 상속하는 파생 클래스의 소멸자를 호출한 뒤에 부모 클래스의 소멸자를 호출하는, 일종의 순차적인 소멸자 호출을 지원합니다.
- [왜 순차적인 소멸자 호출이 필요할까?] : 만약, 기본 클래스의 포인터가 파생 클래스 객체를 가리키고 비 가상 소멸자가 호출될 경우, 기본 클래스의 소멸자만 호출되고 파생 클래스의 소멸자가 정상적으로 호출되지 않아 동적 할당받은 파생 클래스의 객체가 정상적으로 소멸되지 않습니다. 결과적으로 비 가상 소멸자는 메모리 누수의 원인이 될 수 있습니다.
https://webddevys.tistory.com/29
#2. 추상 클래스
1. 추상 클래스?
#include <iostream>
class Shape {
public:
Shape(const std::string& color): color(color) {}
virtual double getArea() const = 0; // 순수 가상 함수
protected:
std::string color;
};
class Rectangle : public Shape {
public:
Rectangle(const std::string& color, double width, double height): Shape(color), width(width), height(height) {}
double getArea() const { return width * height; }
private:
double width;
double height;
};
class Circle : public Shape {
public:
Circle(const std::string& color, double radius): Shape(color), radius(radius) {}
double getArea() const { return 3.14 * radius * radius; }
private:
double radius;
};
int main() {
// Shape shape("red"); // 에러: 추상 클래스는 객체를 생성할 수 없음
Rectangle rectangle("blue", 2.0, 3.0);
Circle circle("green", 2.5);
std::cout << "Rectangle color: " << rectangle.color << ", area: " << rectangle.getArea() << std::endl;
std::cout << "Circle color: " << circle.color << ", area: " << circle.getArea() << std::endl;
return 0;
}
Details
- [정의] : 추상 클래스는 하나 이상의 순수 가상 함수를 갖는 클래스를 말합니다. 순수 가상 함수는 함수의 선언만 존재하고, 함수의 정의는 없는 함수입니다.
- [특징] : (1) 순수 가상 함수 : 파생 클래스는 반드시 부모 클래스의 순수 가상 함수를 구현해야 합니다. (2) 추상 클래스의 생성 : 추상 클래스는 그 자체로 객체를 생성할 수 없습니다. 이를 상속하는 파생 클래스를 통해 객체를 생성해야만 합니다.
#3. 인터페이스
1. 인터페이스?
class Printable { // Printable 인터페이스
public:
virtual void print() const = 0; // 순수 가상 함수
};
class Document : public Printable { // Printable 인터페이스를 구현하는 Document 클래스
public:
void print() const override { // Printable 인터페이스의 순수 가상 함수를 오버라이드
// 문서를 출력하는 코드
}
};
Details
- [정의] : 인터페이스는 추상 클래스의 한 종류입니다. 인터페이스는 클래스가 제공하는 기능들을 명시하는 것으로, 일반적으로 순수 가상 함수만을 갖는 클래스입니다. 인터페이스 클래스를 상속하는 클래스들은 인터페이스가 제공하는 순수 가상 함수들을 반드시 구현해야합니다. 이를 통해, 클래스 간의 표준화된 통과 서로 다른 구현 간의 교체가 가능해집니다.
- [추상 클래스 vs 인터페이스 클래스] : (1) 일반 멤버 함수와 변수 여부 : 추상 클래스는 순수 가상 함수 외에도 일반 메버 함수나 변수를 갖는 반면, 인터페이스 클래스는 오직 순수 가상 함수만을 가집니다. (2) 역할 : 추상 클래스는 하위 클래스들의 공통된 구현을 정의하고 이를 확장하는 데 사용됩니다. 반면에, 인터페이스는 어떤 기능들을 제공하는지 명시하기 위함이며, 이를 다른 하위 클래스들이 구현하여 사용합니다.
'언어 > 기술 질문' 카테고리의 다른 글
[기술 질문]#17_Lambda (0) | 2023.03.27 |
---|---|
[기술 질문]#16_RTTI, 런타임 타입 정보 (0) | 2023.03.22 |
[기술 질문]#14_템플릿, Template (0) | 2023.03.12 |
[기술 질문]#13_6가지 디폴트 멤버 메서드 (0) | 2023.03.05 |
[기술 질문]#12_함수 포인터, Function Pointer (0) | 2023.02.22 |