728x90
728x90
상속받는 클래스가 존재하는 경우 가상 소멸자를 사용하는 것이 중요하다

 

생성자는 부모 클래스의 생성자가 먼저 불려지고,
소멸자는 자식 클래스의 소멸자가 먼저 불려지고 나서 부모 클래스의 소멸자가 불려진다. 

다형성을 위해 부모 클래스의 포인터로부터 자식 클래스를 호출할 때
가상 함수로 정의되지 않은 자식 클래스의 오버라이딩된 함수를 호출하면 부모 클래스의 멤버 함수가 호출된다
소멸자도 자식클래스에서 오버라이딩된 함수의 일종이라 부모포인터로 객체를 삭제하면 부모클래스 소멸자가 호출된다

 

소멸자가 가상함수로 안 선언되어 있으면 자식클래스의 소멸자는 호출되지 않고
가상함수 키워드 virtual이 사용되었으면 자식 클래스에서 재정의될 수 있음을 뜻하여
포인터 종류에 상관없이 항상 자식 클래스의 메서드가 호출된다.
=> 자식클래스의 소멸자가 호출되고나서 부모클래스의 소멸자가 호출된다.

상속관계가 있는 클래스고 소멸자에서 리소스를 해제해야 하는 경우,
반드시 부모 클래스 안의 소멸자를 가상함수로 선언해야 한다.

 

 

클래스의 virtual 소멸자 예제

소멸자 앞에 virtual이 붙어있어 동적바인딩이 된다.
자바는 기본이 동적바인딩이만, C++은 기본이 정적바인딩이여서 virtual 키워드를 사용해야 동적바인딩을 구현할 수 있다. virtual이 붙는 함수가 곧 가상함수이다. 

 

#include <iostream> 
using namespace std;

class classA
{
public:
	classA();
	virtual ~classA();
};

class classB : public classA
{
public:
	classB();
	~classB();
};

// 부모 생성자 -> 자식 생성자 > 
// 자식 소멸자 -> 부모 소멸자가 호출된다.

classA::classA()
{
	cout << "A" << endl;
}
classA::~classA()
{
	cout << "~A" << endl;
}
classB::classB()
{
	cout << "B" << endl;
}
classB::~classB()
{
	cout << "~B" << endl;
}
int main()
{
	cout << "START" << endl;
	classB* B = new classB;
	classA* A = B;
	delete A;
	return 0;
}

deleteB를 안하고 A를 해도 가상함수 소멸자로 작성되어있으면 타고 들어가서 다 지운다.

 

 

 

정적바인딩, 동적바인딩

정적바인딩 : 컴파일 시에 메모리를 할당(바인드)한다. 일반함수, 일반변수 할당
동적바인딩 : 실행 시에 메모리를 할당(바인드)한다. 가상함수, 변수 동적 할당

다른 얘기로 정적바인딩은 클래스타입의 포인터에 따른 결합, 동적바인딩은 클래스타입의 포인터가 가르키는 대상체에 대한 결합 (출처)

컴파일 중 가상함수면 건드리지 않고, 가상함수가 아니라면 메모리공간에 함수를 할당시켜 놓는다.

프로그램 실행 중 가상함수인데 재정의된 함수가 없으면 가상함수를 올리고,
가상함수인데 재정의된 함수가 있으면 재정의된 함수를 올린다.
가상함수가 아니면 이미 할당된 함수를 갖고 온다.

 

 

아래는 부모 클래스의 소멸자 앞에 virtual 키워드를 지워본 예제이다. 두 코드를 비교해보자.

#include <iostream>
using namespace std;

class classA
{
public:
	classA();
	/*virtual ~classA();*/
	~classA();
};

class classB : public classA
{
public:
	classB();
	~classB();
};

// 부모 생성자 -> 자식 생성자 > 
// 자식 소멸자 -> 부모 소멸자가 호출된다.

classA::classA()
{
	cout << "A" << endl;
}
classA::~classA()
{
	cout << "~A" << endl;
}
classB::classB()
{
	cout << "B" << endl;
}
classB::~classB()
{
	cout << "~B" << endl;
}
int main()
{
	cout << "START" << endl;
	classB* B = new classB;
	classA* A = B;
	delete A;
	return 0;
}

 

 

 

 

결론

클래스의 virtual 소멸자의 역할은
상속관계가 있는 클래스고 소멸자에서 리소스를 해제해야 하는 경우,

반드시 부모 클래스 안의 소멸자를 가상함수로 선언이 필요해서이다.

 

virtual 키워드를 사용하면 가상함수 테이블 포인터가 생기고, 가상함수 테이블에 등록된다.
메모리 공간을 쓰게 되기 때문에 필요할 때만 사용한다. 라고 하지만 
실제 개발에서는 클래스 구조가 바뀔 수도 있기 때문에 virtual 키워드를 아낌 없이 써주는 것도 괜찮다

또한 가상소멸자가 존재하지 않는 기본 기능들(std::string) 등을 상속받아 클래스를 구현하고 업캐스팅해 사용한다면 가상소멸자가 없기 때문에 상속받은 클래스의 메모리가 해제되지 않음도 유의해야 한다.

 

 

가상함수 테이블 & 가상함수 테이블 포인터

가상함수 테이블 : 가상함수들의 주소목록. 테이블은 배열을 뜻하니 함수 포인터 배열이라고 보면 됨

가상함수 테이블 포인터 (vfptr) : 가상함수테이블을 가리키는 함수포인터변수.
가상함수가 하나라도 있으면 컴파일러가 만들어준다. 

 

 

 

추가 예제
#include <iostream> 
typedef struct _basestruct
{
	int m_a;
	int m_b;
	int m_c;
	int m_d;
	int m_e;

	char m_pchara;
	char m_pcharb;
	char m_pcharc;

}basestruct;

class classbase
{
public:
	classbase();
	~classbase();

	basestruct* m_pbasestruct;

};

classbase::classbase()
	:m_pbasestruct(NULL)
{
	m_pbasestruct = new basestruct();
	//m_pbasestruct->m_a;
}

classbase::~classbase()
{
	if (m_pbasestruct)
	{
		delete m_pbasestruct;
	}
}

using namespace std;

class classA
{
public:
	classA();
	/*virtual ~classA();*/
	virtual ~classA();

	classbase* m_nclassbase_A = NULL;
};

class classB : public classA
{
public:
	classB();
	virtual ~classB();

	 classbase* m_pclassbase_B = NULL;
};

// 부모 생성자 -> 자식 생성자 > 
// 자식 소멸자 -> 부모 소멸자가 호출된다.

classA::classA()
{
	cout << "A" << endl;
	m_nclassbase_A = new classbase();
	
}
classA::~classA()
{
	cout << "~A" << endl;
	if (m_nclassbase_A)
	{
		delete m_nclassbase_A;
	}
}
classB::classB()
{
	cout << "B" << endl;
	m_pclassbase_B = new classbase();
}
classB::~classB()
{
	cout << "~B" << endl;
	if (m_pclassbase_B)
	{
		delete m_pclassbase_B;
	}
}
int main()
{
	cout << "START" << endl;
	classB* B = new classB;
	classA* A = B;
	delete A;

	return 0;
}

 

 

 

참고 : 1

참고 : dydtjr1128

참고 : HyacinthWiki 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,

v