[C++] 생성자(constructor)와 소멸자(destructor)

2022. 7. 1. 11:26♣ C++

이번 포스팅에선 생성자와 소멸자에 대해 알아보겠습니다.

생성자: 클래스 객체를 만들 때 매개 변수를 초기화하는 함수

소멸자: 객체의 수명이 끝나면 정리해주는 멤버 함수

생성자는 클래스의 시작, 소멸자는 클래스의 끝이라고 보면 되겠다.

 

개발 환경 : VSCode, Windows 10

 

 

 

 

Contents

📌 생성자 정의 및 선언

📌  디폴트 생성자

📌 복사 생성자

📌  소멸자

 

 

 

 

 

기본 개발 세팅은 다음과 같습니다.

#include <iostream>
#include <string.h>
#include <string>
using namespace std;

int main()
{
	return 0;
}

 

 

 

 

 

📌 생성자 정의 및 선언


앞서 배웠던 클래스는 거의 자신만의 매개 변수를 가졌습니다.

따라서 클래스 객체를 생성할 땐 매개 변수도 무조건 초기화해줘야 합니다. 

 

생성자는 매개 변수 초기화만을 수행하는 public 함수이며, 객체를 선언하는 동시에 불러옵니다.

 

[생성자를 선언할 때의 규칙]

(1) 초기화를 위한 데이터만을 인수로 받습니다.

(2) 매개 변수를 초기화하는 함수이므로, 반환 값은 없습니다. 하지만 void 선언을 하지 않습니다. 

(3) 초기화 방법이 여러개일 경우, 오버로딩 규칙에 의해 여러 개를 생성할 수 있습니다.

 

 

🎨 이제 직접 코드를 통해 생성자 선언과 사용 예시를 살펴보겠습니다.

먼저 class 선언입니다.

class player
{
	private:
		string name_;
		int height_;

	public:
		player(const string& name, int height);
		void Display();
};

운동선수 클래스를 만들었습니다. private 제어 지시자에 매개 변수를 선언하고, public엔 생성자를 선언했습니다. 앞서 말했던 대로, 반환 값이 없지만 void 선언을 하지 않은 것을 볼 수 있습니다. 매개 변수들을 보여줄 Display() 함수도 선언했습니다.

 

 

함수 정의를 해줍니다.

생성자는 void와 같은 선언이 없습니다.

player::player(const string& name, int height)
{
	name_ = name;
	height_ = height;
}

void player::Display()
{
	cout << "선수의 이름 : " << name_ << endl;
	cout << "선수의 키 : " << height_ << endl;
}

인수를 받아서 그대로 매개 변수 초기화를 진행합니다. Display() 함수는 일정 양식에 맞게 선수의 이름과 키를 보여주도록 합니다.

 

 

main() 코드

int main()
{
	// 1
	player nadal("나달", 183);
	nadal.Display();

	// 2
	player federer = player("페더러", 184);
	federer.Display();
	return 0;
}

두 명의 선수 객체를 선언했습니다. 테니스계에서 유명한 나달과 페더러입니다. 그런데 이들 객체를 생성하는 방법이 조금 다릅니다. 페더러 객체는 명시적으로 호출을 이용하여 선언했습니다. 이 코드들의 결과는 어떨까요?

 

 

결과

선수의 이름 : 나달  
선수의 키 : 183     
선수의 이름 : 페더러
선수의 키 : 184

이쁘게 잘 나온 것을 확인했습니다. 생성자는 클래스 매개 변수를 초기화하는 함수임을 인지하시고, 이제 디폴트 생성자로 넘어가겠습니다.

 

 

 

 

 

 

 

📌 디폴트 생성자


디폴트 생성자는 매개 변수의 초깃값을 미리 정의해두는 것을 말합니다. 만약 사용자가 객체를 선언하며 초깃값을 명시하지 않으면 컴파일러에 의해 자동적으로 제공하도록 합니다. 단, 클래스에서 생성자가 단 하나도 정의되지 않았을 때만 자동으로 제공합니다. 생성자가 있다면 오류를 발생시킵니다.

 

초깃값을 명시하지 않고 객체를 생성하고 싶을 때 디폴트 생성자를 정의합니다.

 

 

[디폴트 생성자 정의]

 

(1) 디폴트 인수를 이용합니다.

=> 클래스에서 함수를 정의할 때 디폴트 인수를 미리 선언합니다.

// 클래스 선언
class player
{
	private:
		string name_;
		int height_;

	public:
		player(const string& name = "나달", int height = 183);
		void Display();
};



// 함수 정의
player::player(const string& name, int height)
{
	name_ = name;
	height_ = height;
}

void player::Display()
{
	cout << "선수의 이름 : " << this->name_ << endl;
	cout << "선수의 키 : " << this->height_ << endl;
}

player(const string& name = "나달", int height = 183); 처럼 함수 원형을 정의할 때부터 디폴트 인수를 깔아놓고 시작하면 이것이 디폴트 생성자가 됩니다.

 

 

 

 

(2) 함수 오버로딩을 이용합니다.

=> 함수 오버로딩 규칙을 응용하여 여러 개의 생성자를 만드는 것입니다. 그중 하나의 생성자를 디폴트 생성자처럼 이용하는 것이 핵심입니다.

// 클래스 선언
class player
{
	private:
		string name_;
		int height_;

	public:
		player(const string& name, int height);
		player();
		void Display();
};


// 함수 정의
player::player(const string& name, int height)
{
	name_ = name;
	height_ = height;
}

player::player()
{
	name_ = "쿠쿠롱루삥뽕";
	height_ = 120;
}

하나는 name, height 매개 변수가 있으며, 다른 하나는 없습니다. C++은 이 둘을 오버로딩으로 인식하며, 사용자가 만약 디폴트 생성자를 사용하고 싶다면 매개 변수를 넣지 않고 객체를 정의하면 됩니다.

 

 

main() 함수입니다.

int main()
{
	// 1
	player nadal("나달", 183);
	nadal.Display();

	// 2
	player federer = player();
	federer.Display();
	return 0;
}

 

 

결과

선수의 이름 : 나달
선수의 키 : 183
선수의 이름 : 쿠쿠롱루삥뽕
선수의 키 : 120

 

 

 

 

[디폴트 생성자를 가지는 객체의 선언]

=> class이름 객체(); 는 자칫하면 함수로 인식될 수 있습니다. 따라서 디폴트 생성자로 객체를 선언할 땐 다음 3가지의 형태를 따릅니다.

 

// (1)
player nadal;

// (2)
player nadal = player();

// (3) 
player *ptr_nadal = new player;

다만, 마지막 세번째 방법을 이용하고 난 후에는 delete를 통해 꼭 메모리 할당 해제를 해줘야 합니다.

 

 

 

 

 

📌 복사 생성자


객체가 다른 객체를 복사하고 싶다면 어떻게 해야 할까요?

player nadal;
player federer = player("페더러", 184);

두 개의 객체가 생성되었습니다. 하나는 디폴트 생성자로 객체를 생성했고, 하나는 명시적으로 매개 변수를 정의했습니다.

그런데 nadal 객체에 federer 객체의 내용을 복사하고 싶습니다.

 

 

대입 연산자를 이용했습니다.

nadal = federer

이렇게 복사하는 것을 얕은 복사라고 합니다. 값을 복사하는 것이 아니라 값을 가리키는 포인터를 복사하는 것입니다. 객체 대입을 얕은 복사로 진행하면 문제가 생길 수 있습니다. 따라서 깊은 복사로 객체 대입을 진행해야 합니다.

 

 

깊은 복사란? 값 자체를 복사하는 것을 말합니다.

클래스에서는 이런 깊은 복사를 가능케 하는 복사 생성자를 정의할 수 있습니다. 

복사 생성자는 자신과 같은 클래스 타입의 객체에 대한 참조를 인수로 받아서 자신을 초기화하는 생성자입니다.

 

 

다음은 복사 생성자를 정의한 클래스입니다.

// 복사 생성자는 클래스 객체를 인수로 받습니다.
player(const player&);


// 클래스 정의
class player
{
	private:
		string name_;
		int height_;

	public:
		player(const string& name, int height);
		player();
		player(const player&);
		void Display();
};

 

 

복사 생성자는 다음과 같은 방식으로 정의됩니다.

이를 보면 알 수 있듯이, 복사 생성자는 값을 복사하는 것에 초점을 맞추고 있습니다.

player::player(const player& another)
{
	name_ = another.name_;
	height_ = another.height_;
}

 

 

main() 함수입니다.

int main()
{
	// 1
	player federer = player("페더러", 184);
	federer.Display();

	// 깂 복사
	player nadal(federer);
	nadal.Display();
	return 0;
}

페더러 객체를 생성한 후, 나달 객체를 생성할 땐 복사 생성자를 이용했습니다.

이에 따라 페더러 객체의 정보가 모두 나달 객체에도 복사되었습니다.

 

 

결과

선수의 이름 : 페더러
선수의 키 : 184
선수의 이름 : 페더러
선수의 키 : 184

 

 

 

 

 

📌 소멸자


객체의 수명이 끝나면 정리해주는 멤버 함수입니다.

클래스 이름과 같지만, 앞에 ~을 붙여줘서 구분합니다.

인수, 반환값이 없고, void 선언도 하지 않습니다. 단 하나만 정의합니다.

 

 

💥 주의 사항

(1) 만약 new 키워드를 통해 동적 할당을 해서 객체를 생성했다면, delete 키워드를 이용해서 정리해줘야 합니다.

(2) 소멸자는 알아서 작동합니다.

 

 

class player
{
	private:
		string name_;
		int height_;

	public:
		player(const string& name, int height);
		player();
		player(const player&);
		~player();
		void Display();
};

~player(); 코드를 통해 소멸자를 정의해줍니다.

 

 

소멸자 정의

player::~player()
{
	cout << this->name_ << "객체가 소멸되었습니다." << endl;
}

소멸자는 그저 player::~player(){} 만 해도 되지만, 소멸되었다는 것을 알리기 위해 약간의 코드를 첨가했습니다.

 

 

main() 함수입니다.

int main()
{
	// 1
	player federer = player("페더러", 184);
	federer.Display();

	// 값 복사
	player nadal(federer);
	nadal.Display();
	return 0;
}

 

 

결과

선수의 이름 : 페더러
선수의 키 : 184
선수의 이름 : 페더러
선수의 키 : 184
페더러객체가 소멸되었습니다.
페더러객체가 소멸되었습니다.

 

 

 

 

지금까지 생성자와 소멸자에 대해 알아보았습니다.

클래스 객체를 생성함에 있어 편리한 도구가 될 것이니 잘 활용하시길 바랍니다.

수고하셨습니다.