본문 바로가기
C++

[C++] 생성자(Constructor) 소멸자(Destructor)

by rehtorb_s 2020. 7. 19.

생성자

객체가 생성되면서 멤버변수의 초기화나 멤버함수를 호출하는 등 사전에 필요한 준비작업을 하는 함수 입니다.

1. 반환 형은 없다.

2. 클래스 이름과 동일하게 선언을 한다.

3. 외부의 값으로 멤버변수를 초기화 해야할 경우 매개변수를 가진 생성자를 재정의(오버로딩) 할 수도 있다. 쉽게 말하자면 우리가 변수를 초기화 하듯이 객체가 생성됨과 동시에 객체를 초기화 시켜주는 역할을 합니다. 

 

그런데 이전 포스팅들을 보면 생성자가 정의되어 있지 않음에도 불구하고 에러없이 잘 돌아가는데 그건 컴파일러가 기본생성자를 만들어서 호출하기 때문입니다. 만약 오버로딩된 생성자를 정의 한다면 기본생성자는 컴파일러가 만들어주지 않습니다. 따라서 기본생성자도 구현을 해줘야 하죠.

 

//.h
class CBase
{
public:
	CBase();		//생성자 정의
	CBase(int value);	//생성자 오버로딩
	void Show();
private:
	int m_nData;
	int* m_pData;
	int m_nArr[5];
};
//.cpp
CBase::CBase()
{
	m_nData = 10;
	m_pData = new int(10);
	for (int i = 0; i < 5; i++)
		m_nArr[i] = 10 + i;
	std::cout << "기본클래스 기본 생성자 호출" << std::endl;
}

CBase::CBase(int value)
{
	m_nData = value;
	m_pData = new int(value);
	for (int i = 0; i < 5; i++)
		m_nArr[i] = value + i;
	std::cout << "기본클래스 오버로딩 생성자 호출" << std::endl;
}

void CBase::Show()
{
	std::cout << "m_nData = " << m_nData << std::endl;
	std::cout << "m_pData = " << *m_pData << std::endl;
	for (int i = 0; i < 5; i++)
		std::cout << m_nArr[i] << " ";

	std::cout << std::endl << std::endl;
}

//main
int main()
{
	CBase a;	//기본생성자CBase() 호출
	CBase b(100);	//오버로딩된CBase(int value) 생성자 호출

	a.Show();
	b.Show();
	system("pause");
	return 0;
}

 

각각의 멤버변수 값을 대입해 줌으로써 생성자의 역할이 끝났군요. 

그러나 우리는 "대입"이란 단어에 주목해야 합니다. 사실 대입은 생성자를 구현함에 있어 그닥 좋은 방법이 아닙니다. 대입연산을 하기 전 변수에는 메모리가 할당되고 쓰레기 값이 들어가기 때문이죠. 대입과 초기화는 엄연히 다른 영역인데, 생성자의 동작에 있어 컴파일러는 초기화를 먼저 진행하고, 구현 블록에 있는 대입연산을 진행합니다. 그럼 어떻게 초기화가 진행되느냐?

 

초기화 리스트를 사용합니다. 아래 코드를 보세요. 한눈에 봐도 블록 {} 보다 먼저 정의되어 있습니다.

//.cpp
CBase::CBase()
	:
	m_nData(10),
	m_pData(new int(10)),
	m_nArr{ 10,11,12,13,14 }
{
	std::cout << "기본클래스 기본 생성자 호출" << std::endl;
}

CBase::CBase(int value)
	:
	m_nData(value),
	m_pData(new int(value)),
	m_nArr{value, value+1, value+2, value+3, value+4}
{
	std::cout << "기본클래스 오버로딩 생성자 호출" << std::endl;
}

void CBase::Show()
{
	std::cout << "m_nData = " << m_nData << std::endl;
	std::cout << "m_pData = " << *m_pData << std::endl;
	for (int i = 0; i < 5; i++)
		std::cout << m_nArr[i] << " ";

	std::cout << std::endl << std::endl;
}

 

현재는 멤버변수에 기본자료형만 나열되어 있지만, 만약 다른 클래스가 선언되어 있다면 그 클래스의 생성자를 초기화리스트에 넣어주시면 되겠습니다.

 

기본클래스를 상속받은 파생클래스는 생성자를 어떻게 구현할까요? 똑같습니다. 단 초기화리스트에 기본클래스의 생성자를 명시적으로 호출을 해줘야겠지요. 코드예제를 봅시다.

 

//.h
class CBase
{
public:
	CBase();
	CBase(int value);
	void Show();
private:
	int m_nData;
	int* m_pData;
	int m_nArr[5];
};

class CDerived : public CBase
{
public:
	CDerived();
private:
	int m_nData2;
};
//.cpp
CDerived::CDerived()
	:
	CBase(1000),	//기본 클래스의 오버로딩된 생성자 호출, 주목!
	m_nData2(1000)
{
	std::cout << "파생클래스 생성자 호출" << std::endl;
}

파생클래스 객체를 선언하면 생성자의 호출순서는 기본클래스 생성자 -> 파생클래스 생성자 순으로 이뤄집니다. 그런데 만약 위의 CBase(1000) 처럼 명시적으로 호출해주지 않을 경우 컴파일러는 기본클래스의 기본생성자 CBase()를 암시적으로 호출 해줍니다. 항상 암시적인 무언가에 의존을 하게되면 나중에는 돌이킬 수 없는 사태가 발생하니.. 명시적으로 하는 습관을 들입시다..! 

 


소멸자(Destructor)

동적으로 생성된 객체에 대해 delete를 해주거나, 지역변수로서의 수명이 다할 때 등 객체의 생명주기가 끝날 때 자원반환을 위해 쓰이는 함수입니다.

1. 반환형이 없다.

2. 클래스와 같은 이름으로 선언하지만 ~을 붙인다.

3. 매개변수가 없다.  

 

백문이 불여일견 코드로 바로 보시죠.

//.h
class CBase
{
public:
	CBase();
	CBase(int value);
	~CBase();	//소멸자 선언
	void Show();
private:
	int m_nData;
	int* m_pData;
	int m_nArr[5];
};
//.cpp
CBase::~CBase()		//소멸자 정의
{
	delete m_pData;	//포인터 변수인 m_pData 자원해제
	std::cout << "기본클래스 소멸자 호출" << std::endl;
}
//main
int main()
{
	CBase a;
	a.Show();
	return 0;
}

main문의 블럭이 끝나고나서 소멸자가 호출된걸 확인할 수 있습니다. 소멸자 내에서는 m_pData에 대한 자원해제를 진행하는데요. 생성자에서 동적할당 되어 힙에 남아있기 때문에 직접 자원해제를 해주지 않으면 힙메모리에 남아있게 되겠죠?

 

 

상속관계에서는 어떨까요?

상속관계에서의 소멸자 호출순서는 파생클래스 소멸자 -> 기본클래스 소멸자입니다. 생성자가 호출되었으니 소멸자 역시 짝을 이뤄 1대1 대응하게 호출이 되어야 하겠죠? 파생클래스에서 직접 소멸자를 선언/정의 하시고 테스트를 해보세요. 

 

앞선 포스팅에서 가상함수 및 업&다운캐스팅에 대해 말씀을 드렸었는데요. 가상함수가 하나이상 포함된 상속관계의 클래스 간의 업캐스팅이 이루어진 상황이라면 파생클래스의 소멸자가 호출이 되지 않습니다. 코드 한번 보시죠. 뒤에 주석 처리된 코드 설명 부분만 보시면 됩니다~

 

//.h
class CBase
{
public:
	CBase();
	CBase(int value);
	~CBase();			//소멸자 선언
	virtual void Show();		//가상함수 선언(가상함수가 하나이상 존재한다)
private:
	int m_nData;
	int* m_pData;
	int m_nArr[5];
};

class CDerived : public CBase
{
public:
	CDerived();
	~CDerived();			//소멸자 선언
private:
	int m_nData2;
};
//.cpp
CBase::CBase()
	:
	m_nData(10),
	m_pData(new int(10)),
	m_nArr{ 10,11,12,13,14 }
{
	std::cout << "기본클래스 기본 생성자 호출" << std::endl;
}

CBase::CBase(int value)
	:
	m_nData(value),
	m_pData(new int(value)),
	m_nArr{value, value+1, value+2, value+3, value+4}
{
	std::cout << "기본클래스 오버로딩 생성자 호출" << std::endl;
}

CBase::~CBase()		//기본클래스 소멸자 정의
{
	delete m_pData;
	std::cout << "기본클래스 소멸자 호출" << std::endl;
}

void CBase::Show()
{
	std::cout << "m_nData = " << m_nData << std::endl;
	std::cout << "m_pData = " << *m_pData << std::endl;
	for (int i = 0; i < 5; i++)
		std::cout << m_nArr[i] << " ";

	std::cout << std::endl << std::endl;
}

CDerived::CDerived()
	:
	CBase(1000),
	m_nData2(1000)
{
	std::cout << "파생클래스 생성자 호출" << std::endl;
}

CDerived::~CDerived()	//파생클래스 소멸자 정의
{
	std::cout << "파생클래스 소멸자 호출" << std::endl;
}
//main
int main()
{
	CBase* pBase;
	CDerived* pDerived = new CDerived();

	pBase = pDerived;	//업 캐스팅
	delete pBase;
	return 0;
}

 

생성자는 두개가 호출되었는데.. 파생클래스 소멸자가 호출되지 않았습니다. 해제하는 포인터객체의 타입에 해당하는 소멸자만 호출한 상태인데 우리는 기본클래스에 virtual 키워드를 붙여줌으로써 동적바인딩을 통해! 해결이 가능합니다. 한번 시도해보세요..! (코드생략)

 

이제야 결과가 제대로 나오는군요.

 

소멸자에 virtual 키워드를 붙일때 주의할점이 있습니다. C++ std에서 제공하는 클래스나.. 본인이 수정할 수 없는 클래스를 상속받는 경우 기본클래스에 가상소멸자가 정의되어 있는지 확인을 꼭 하시고 코딩하시길 바랄게요~ 그렇지 않으면 업캐스팅 상황에서 파생클래스 영역이 메모리에 남아있을 수 있으니깐요^^

 

다음 포스팅은 복사생성자에 대해 알아봅시다.

 


포스팅 내용에 오류가 있거나 지적사항이 있다면 댓글로 달아주세요. 배움을 멈추지 않는 Good Programmer가 되겠습니다. 감사합니다.

댓글