안녕하세요 오늘은 virtual 생성자와 생성자 안에 virtual 함수를 부르면 어떤 현상이 일어나는지에 대해 알아보도록 하겠습니다.
1. 개요
보통 virtual이 선언된 함수나 상속은 RTTI를 사용하여 현재의 객체의 타입 정보를 알수있는 효과를 가지고있습니다.
RTTI나 virtual에 대해서는 아래 링크에서 자세히 포스팅 해놓았으니 읽어 보시길 바랍니다.
https://chogyujin-study.tistory.com/25
https://chogyujin-study.tistory.com/23
https://chogyujin-study.tistory.com/61
가상 함수나 가상 상속을 할 경우에는 가상함수 테이블을 통해 현재의 객체에 맞게 테이블안에 포인터로 저장을 해줍니다.
하지만 이러한 가상함수를 만약 생성자에 선언을 할경우 어떠한 결과를 얻을수있을가요?
2. virtual constructor
가상 생성자는 클래스 내부에서 선언을 할경우 컴파일러 상에서 오류를 뿜고 빌드가 안됩니다.
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class A
{
private:
string s;
public:
virtual A(string s) : s(s) // 생성자는 virtual 선언을 할수가 없습니다.
{
cout <<s<< " 초기 생성자" << endl;
}
A(const A& a)
{
this->s = a.s;
cout <<s<< " 복사 생성자" << endl;
}
A(const A&& a)
{
this->s = a.s;
cout <<s<< " 이동 생성자" << endl;
}
~A()
{
cout <<s<< "소멸자" << endl;
}
};
int main()
{
return 0;
}
생성자 타입에는 virtual 을 사용할수 없다면서 빨간줄이 그어집니다.
2. 생성자에서 virtual 함수 부르기
그렇다면 생성자에서 가상 함수를 부르는 행위는 어떠한가요?
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class A
{
private:
string s;
public:
A(string s) : s(s)
{
cout <<s<< " 초기 생성자" << endl;
OutPrint();
}
A(const A& a)
{
this->s = a.s;
cout <<s<< " 복사 생성자" << endl;
}
A(const A&& a)
{
this->s = a.s;
cout <<s<< " 이동 생성자" << endl;
}
~A()
{
cout <<s<< "소멸자" << endl;
}
virtual void OutPrint()
{
cout << "부모의 OutPrint()" << endl;
}
};
int main()
{
A a("A");
return 0;
}
현재로서는 문제가 없어 보입니다.
하지만 자식클래스를 선언하고 재정의를 할경우 어떠한 현상이 벌어질가요?
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class A
{
public:
string s;
A(string s) : s(s)
{
cout <<s<< " 초기 생성자" << endl;
OutPrint();
}
A(const A& a)
{
this->s = a.s;
cout <<s<< " 복사 생성자" << endl;
}
A(const A&& a)
{
this->s = a.s;
cout <<s<< " 이동 생성자" << endl;
}
~A()
{
cout <<s<< "소멸자" << endl;
}
virtual void OutPrint()
{
cout << "부모의 OutPrint()" << endl;
}
};
class B : public A
{
public:
B(string s) : A(s)
{
cout << s << " 초기 생성자" << endl;
}
B(const A& a) : A(a)
{
this->s = a.s;
cout << s << " 복사 생성자" << endl;
}
B(const B&& a) : A(a)
{
this->s = a.s;
cout << s << " 이동 생성자" << endl;
}
~B()
{
cout << s << "소멸자" << endl;
}
void OutPrint()
{
cout << "자식의 OutPrint()" << endl;
}
};
int main()
{
B a("A");
return 0;
}
이런경우에는 B의 객체를 생성하여 분명 부모의 생성자를 타고 들어갈거고 virtual로 선언했으니 자식의 OutPrint가 호출이 되야합니다.
하지만
왠걸 부모의 OutPrint()가 출력이됬습니다.
이것이 도대체 무슨일이냐 하면 보통 자식객체로 선언을 할경우 자식부터 메모리에 올리지않고 맨 위에 부모부터 생성자를 통해 메모리에 올립니다. 그때 OutPrint()를 호출할경우 자식의 대한 OutPrint()가 없으므로 자연스럽게 실행할때 컴퓨터는 부모의 함수를 호출하게 됩니다.
그렇다면 힙으로 받아도 이렇게 될가요? 흔한 업캐스팅을 한번 해보겠습니다.
똑같다는걸 볼수있습니다.
응? 뭔가 이상하지 않나요? 왜 소멸자가 하나죠??
번외 소멸자 virtual
생성자와 다르게 소멸자는 가상함수로 등록이 됩니다.
이러한 코드를 한번 보시죠
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class A
{
public:
string s;
A(string s) : s(s)
{
cout <<s<< " 초기 생성자" << endl;
OutPrint();
}
A(const A& a)
{
this->s = a.s;
cout <<s<< " 복사 생성자" << endl;
}
A(const A&& a)
{
this->s = a.s;
cout <<s<< " 이동 생성자" << endl;
}
~A()
{
cout <<s<< "부모 소멸자" << endl;
}
virtual void OutPrint()
{
cout << "부모의 OutPrint()" << endl;
}
};
class B : public A
{
public:
B(string s) : A(s)
{
cout << s << " 초기 생성자" << endl;
}
B(const A& a) : A(a)
{
this->s = a.s;
cout << s << " 복사 생성자" << endl;
}
B(const B&& a) : A(a)
{
this->s = a.s;
cout << s << " 이동 생성자" << endl;
}
~B()
{
cout << s << "자식 소멸자" << endl;
}
void OutPrint()
{
cout << "자식의 OutPrint()" << endl;
}
};
int main()
{
A* a = new B("a");
delete(a);
return 0;
}
현재 A객체 포인터가 B를 가르키면서 업캐스팅을 하고있습니다.
하지만 이렇게 될경우 A* a는 현재 자기자신의 맴버 변수나 함수밖에 모릅니다.
그래서 가상함수 테이블의 힘을 통해 현재 가르키고 있는 객체가 B이니 B의 함수 주소를 콜할수있는것인데
소멸자는 가상 함수가 아니므로 소멸자에 가상함수를 넣어주면 해결이 가능합니다.
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class A
{
public:
string s;
A(string s) : s(s)
{
cout <<s<< " 초기 생성자" << endl;
OutPrint();
}
A(const A& a)
{
this->s = a.s;
cout <<s<< " 복사 생성자" << endl;
}
A(const A&& a)
{
this->s = a.s;
cout <<s<< " 이동 생성자" << endl;
}
virtual ~A()
{
cout <<s<< "부모 소멸자" << endl;
}
virtual void OutPrint()
{
cout << "부모의 OutPrint()" << endl;
}
};
class B : public A
{
public:
B(string s) : A(s)
{
cout << s << " 초기 생성자" << endl;
}
B(const A& a) : A(a)
{
this->s = a.s;
cout << s << " 복사 생성자" << endl;
}
B(const B&& a) : A(a)
{
this->s = a.s;
cout << s << " 이동 생성자" << endl;
}
~B()
{
cout << s << "자식 소멸자" << endl;
}
void OutPrint()
{
cout << "자식의 OutPrint()" << endl;
}
};
int main()
{
A* a = new B("a");
delete(a);
return 0;
}
이렇게 말이지요
보통 생성자는 부모->자식 순이라면 소멸자는 자식->부모 순입니다.
'개발자 면접 공부 > C-C++' 카테고리의 다른 글
정렬 알고리즘(퀵, 병합) (2) | 2023.05.14 |
---|---|
정렬 알고리즘 (버블, 삽입, 선택) (0) | 2023.05.13 |
RVO vs NRVO (0) | 2023.05.11 |
Constexpr VS Const (0) | 2023.05.09 |
const int* p 와 int * const p의 차이점 (2) | 2023.05.07 |