지난번 클래스 구조의 생성자 & 소멸자에 대하여 공부 하였다.
이번에는 가상함구 & 재정의 에 대하여 조사를 진행한다.
● 가상 함수 ( Virtual Function )
우선 가상 함수 ( Virtual Function ) 이란 무엇인지 알아보도록 하겠다.
이름만 보았을때 Virtual 이라는 것이 제일 먼저 눈에 띈다. VR 게임이 제일 먼저 떠오르기는 했으나 함수?와 어떤 연관이 있는지는 잘 떠오르지 않았다.
가상 함수란 ?
C++ 에서 가상함수는 부모 클래스(class)에서 상속받을 클래스에서 재정의할 것으로 기대하고 정의해놓은 함수 라고한다.
virtual 이라는 예약어를 함수 앞에 붙여서 생성할 수 있으며 생성된 가상함수는 파생 클래스(class)에서 재정의하면 이전에 정의 되어있던 내용들이 모두 새롭게 정의한 내용들로 바뀐다.
가상 함수를 사용해야 하는 이유 ?
컴파일러는 함수를 호출할 때 매우 복잡한 과정을 거친다.
때문에 컴파일러는 함수를 호출하는 코드는 프로그램실행 중이아닌 컴파일 타임에 고정된 메모리 주소로 변환을 시킨다.
이 과정을 정적바인딩( 정적 결합 / static binding ) 또는 초기바인딩 ( early binding ) 이라고 한다.
일반 함수의 경우 모두 이러한 정적 바인딩의 과정을 거친다.
하지만 가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없다.
왜냐하면, 가상 함수는 프로그램이 실행될 때 객체를 결정하므로, 컴파일 타임에 해당 객체를 특정할 수 없기 때문!
그렇기때문에 가상 함수의 경우에는 런 타임에 올바른 함수가 실행될 수 있도록 해야만 한다.
이 것을 동적바인딩( 동적 결합 / dynamic binding ) 또는 지연바인딩 ( late binding ) 이라고 한다.
하지만 가상 함수도 결합하는 타입이 분명할 때에는 일반 함수와 같이 정적 바인딩을 한다.
이러한 가상 함수는 기초 클래스 타입의 포인터나 참조를 통하여 호출될 때만 동적 바인딩을 하게 된다.
[ 가상함수 코드 예제 1 ]
#include<iostream>
using namespace std;
class Parent
{
public:
void print()
{
cout << "이것은 Parent class 입니다." << endl;
}
};
class Child : public Parent
{
public:
void print()
{
cout << "이것은 Child class 입니다." << endl;
}
};
int main()
{
Parent* p = new Parent; // 동적할당
Child* c = new Child; // 동적할당
p->print();
p = c;
p->print();
return 0;
}
[ 가상함수 코드 예제 1 : 실행 결과 ]
이것은 Parent class 입니다.
이것은 Parent class 입니다.
위의 [코드 예제1]을 보면 Parent 타입으로 선언된포인터 p에 Child 객체의 주소를 넣고 함수를 호출시켰으나
Parent클래스의 함수가 호출된 것을 볼 수 있다.
p포인터의 주소를 child로 바꾸어주었음에도 정적바인딩으로 인하여(컴파일 당시 호출될 함수의 주소가 이미 결정 되었기 때문에) 부모의 함수가 호출 되는 것이다.
이를 해결하기 위해서는 정적바인딩이 아닌 동적바인딩을 해야 한다.
동적바인딩을 하려면 일반 함수들을 가상함수로 바꾸어 주면 된다.
가상함수로 선언하면 포인터의 타입이 아닌 포인터가 가리키는 객체의 타입에 따라 멤버 함수를 선택하게 된다.
가상함수 virtual 의 사용방법
[ 가상함수 코드 예제 2 ]
virtual 반환형식 메서드명 // 문법
virtual void PrintName(); // ex
#include<iostream>
using namespace std;
class Parent
{
public:
virtual void print() // virtual 함수의 선언
{
cout << "이것은 Parent class 입니다." << endl;
}
};
class Child : public Parent
{
public:
virtual void print() // virtual 함수의 선언
{
cout << "이것은 Child class 입니다." << endl;
}
};
int main()
{
Parent* p = new Parent; // 동적할당
Child* c = new Child; // 동적할당
p->print();
p = c;
p->print();
return 0;
}
[ 가상함수 코드 예제 2 : 실행 결과 ]
이것은 Parent class 입니다.
이것은 Child class 입니다.
부모의 print() 함수를 가상함수로 선언해주어 동적바인딩을 한다면 Child class 가 출력이 되는것을 볼 수 있다.
한번 가상 함수로 선언된 함수는 따로 virtual 키워드를 추가적으로 명시하지않아도 가상함수로 인식이 된다.
(단, 가독성을 위하여 가상함수 부분에는 명시적으로 virtual 을 기입하여 두는것도 좋다!)
가상함수 테이블 ( virtual function table / vtbl )
C++ 에서는 가상 함수의 정의와 동적 방식만을 규정하고 있다.
그에 따른 구현은 컴파일러마다 다르다.
하지만 컴파일러가 가상 함수를 다루는 가장 일반적인 방식은 가상 함수 테이블을 이용하는 것이다.
C++ 컴파일러는 각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기위한 숨겨진 멤버를 하나씩 추가한다.
이와 함께 가상 함수를 단 하나라도 가지는 클래스에 대해서 가상 함수 테이블을 작성 한다.
작성된 가상함수 테에블에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되게 된다.
가상함수를 호출하면 C++ 프로그램은 가상함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아 호출하게된다.
가상함수를 사용하면 이처럼 함수의 호출 과정이 복잡해지게 되므로 메모리와 실행속도 측면에서 약간의 부담이 있다.
그렇기 때문에 C++ 에서 기본 바인딩은 정적 바인딩이고 필요한 경우에만 가상 함수로 선언하도록 하고 있다.
가상 소멸자
클래스 공부때 생성자 소멸자에 대하여 공부였는데 여기서도 또 소멸자가 나왔다.
가상 소멸자는 C++ 에서 기초 클래스의 소멸자는 반드시 가상으로 선언해야 한다.
// Parent 클래스와 파생 클래스인 Child 클래스가 있다고 가정하에
Parent* pc = new Child;
...
delete pc;
위의 에제에서 Parent 클래스는 Child 클래스의 기초 클래스이므로, pc 라는 Child 객체가 동적으로 할당된다.
하지만 마지막 구문의 delete 키워드는 ~Child() 소멸자를 호출하지 않고, ~Parent() 의 소멸자를 호출할 것이다.
그러므로 Child 객체에 동적으로 할당된 메모리는 정상적으로 해제되지 않을 것이다.
하지만 Parent 클래스의 소멸자를 가상으로 선언한다면, 위의 구문은 정상적으로 ~Child() 소멸자를 호출한다.
따라서 기초 클래스는 명시적으로 소멸자를 선언할 필요가 없더라도, 아무 일도 하지 않는 가상 소멸자를 선언해야한다.
● 재정의 ( overriding )
이번에는 재정의에 대하여 알아보도록 하겠다. 오버라이딩... 오버로딩... 오버로드 뭔가 비슷한건가? 싶었다.
재정의 란 ?
우선 재정의 간단히 이해하자면 재 + 정의 로 기존에 함수 정의를 새롭게 정의 하는 것이다.
재정의는 상속관계에서 사용이될수 있다고 한다.
상속관계 즉, 부모클래스에 정의된 함수를 같은 이름으로 자식클래스에서 재정의 할때 발생하며
이과정에서 부모 클래스의 함수는 모두 가려진다고 한다.
재정의 할때 주의 할점
재정의를 할때 주의할점은 멤버 함수의 [이름], [반환형], [매개변수의 갯수], [데이터 타입] 이 일치하여야 한다는점이다.
기존 부모 클래스의 함수를 재정의 하는 작업이기 때문에 형태가 유지 되어야 한다.
[ 재정의 코드 예제 1 ]
#include<iostream>
using namespace std;
class Parent
{
public:
int a = 10;
};
class Child : public Parent
{
public:
int a = 20;
};
int main()
{
Parent p;
Child c;
cout<< "p.a 출력" << p.a << endl;
cout<< "c.a 출력" << c.a << endl;
return 0;
}
[ 재정의 코드 예제 1 : 실행 결과 ]
p.a 출력10
c.a 출력20
위의 코드를 보자면 Parent 라는 class 에 int형 a의 값을 10으로 정의하였으며
상속관계인 Child 라는 class 에 같은 자료형 in형의 a의 값을 20으로 정의 하였다.
메인에서는 Parent 는 p로 선언을, Child 는 c로 선언하였다.
메인에서 출력되는값은 p.a 그리고 c.a 이다.
같은 자료형의 같은이름의 a 이지만 재정의를 통하여 출력값이 바뀌는 것을 볼 수 있다.
'공부' 카테고리의 다른 글
C++ 복사생성자 & 팩토리패턴 (0) | 2022.11.28 |
---|---|
C++ 상점.인벤토리(추상화,가상함수 사용하기) (0) | 2022.11.27 |
C++ 코드작동 확인하는 참고 사이트 (0) | 2022.11.24 |
C++ 생성자 & 소멸자 (0) | 2022.11.23 |
C++ 문자>값으로 / 값>문자로 변형 참고 (0) | 2022.11.23 |
댓글