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

2024. 4. 12. 18:22[C++]/C++ 언어 기초

'생성자'라는 것을 이용하면 객체도 생성과 동시에 초기화할 수 있다.


생성자의 이해

 

이러한 유형의 함수를 가리켜 '생성자'라고 하고, 다음의 형태를 가지고 있다.

  • 클래스의 이름과 함수의 이름이 동일하다.
  • 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
  • 객체 생성 시 딱 한번 호출된다.

생성자 호출

  • 생성자도 함수의 일종이니 오버로딩이 가능하다.
  • 생성자도 함수의 일종이니 매개변수에 '디폴트 값'을 설정할 수 있다.

 

Constructor1.cpp

#include <iostream>
using namespace std;

class SimpleClass
{
	int num1;
	int num2;

public:
	SimpleClass()
	{
		num1=0;
		num2=0;
	}
	SimpleClass(int n)
	{
		num1=n;
		num2=0;
	}
	SimpleClass(int n1, int n2)
	{
		num1=n1;
		num2=n2;
	}

	/*
	SimpleClass(int n1=0, int n2=0)
	{
		num1=n1;
		num2=n2;
	}
	*/

	void ShowData() const
	{
		cout<<num1<<' '<<num2<<endl;
	}
};

int main(void)
{
	SimpleClass sc1;
	sc1.ShowData();

	SimpleClass sc2(100);
	sc2.ShowData();

	SimpleClass sc3(100, 200);
	sc3.ShowData();
	return 0;
}
0 0
100 0
100 200
  • 위와 같이 오버로딩이 가능하다. 또한, 디폴트 값도 설정이 가능하다.

다음과 같이 문장을 구성하면 안된다.

SimpleClass sc1()

 

이는 두가지로 해석할 수 있다.

  1. 객체의 생성 : SimpleClass의 객체 sc1을 생성하고, 이 sc1은 인자를 받지 않는 생성자를 호출한다.
  2. 함수의 원형 선언 : Sc1은 함수의 이름이고, void형 함수이며 반환형은 SimpleClass이다.

컴파일러는 1인지 2인지 구분이 불가해서 위와 같이 문장을 구성하는 것은 불가능하다.


Point, Rectangle 클래스에 생성자 적용 예시

  • Rectangle 클래스의 생성자 정의는 조금 더 생각을 해야한다.
  • Ractangle 클래스는 두 개의 Point 객체를 멤버로 지니고 있어서 Rectangle 객체가 생성되면, 두 개의 Point 객체가 함께 생성된다. 

"Rectangle 객체를 생성하는 과정에서 Point 클래스의 생성자를 통해 Point 객체를 초기화할 수 없을까?


'멤버 이니셜라이저(member initializer)'를 이용한 멤버 초기화

class Rectangle{
    private:
        Point upLeft;
        Point lowRight;
    public:
        Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
        void ShowRecInfo() const;
};

Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2)
	:upLeft(x1, x2), lowRight(x2,y2)
{
	//empty
}

 

이 중에서 다음의 내용이 '멤버 이니셜라이저'이다.

:upLeft(x1, y1), lowRight(x2, y2)

 

이것을 의미하는 바는 각각 다음과 같다.

  • 객체 upLeft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라
  • 객체 lowRight의 생성과정에서 x2과 y2을 인자로 전달받는 생성자를 호출하라

마지막으로 우리는 객체의 생성과정을 다음과 같이 정리할 수 있다.

  • 1단계: 메모리 공간의 할당
  • 2단계: 이니셜라이저를 이용한 멤버변수(객체)의 초기화
  • 3단계: 생성자의 몸체부분 실행

'멤버 이니셜라이저(member initializer)'를 이용한 변수 및 const 상수(변수) 초기화

  • '멤버 이니셜라이저'는 객체가 아닌 멤버의 초기화에도 사용할 수 있다.

num1(n1)
  • num1을 n1 값으로 초기화하라는 뜻이 된다.
  • 일반적으로 멤버변수의 초기화에 있어서는 이니셜라이저를 선호하는 편이다.
    • 초기화의 대상을 명확히 인식할 수 있다.
    • 성능에 약간의 이점이 있다.
  • 이니셜라이저의 초기화는 선언과 동시에 초기화되는 형태이므로, 참조자의 초기화도 가능하다.


디폴트 생성자(Default Constructor)

  • 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다.
  • 생성자를 정의하지 않는 클래스에는 C++ 컴파일러에 의해서 디폴트 생성자라는 것이 자동으로 삽입된다.
  • 디폴트 생성자는 인자를 받지 않으며, 내부적으로 아무런 일도 하지 않는 생성자이다.

  • 생성자를 정의하지 않으면 인자를 받지 않고, 하는 일이 없는 디폴트 생성자라는 것이 컴파일러에 의해서 추가된다.
  • 따라서 모든 객체는 무조건 생성자의 호출 과정을 거쳐서 완성된다.

생성자 불일치

  • 매개변수가 void형으로 선언되는 디폴트 생성자는, 생성자가 하나도 정의되어 있지 않을 때에만 삽입이 된다.
  • 즉, 다음과 같이 정의된 클래스에는 디폴트 생성자가 삽입되지 않는다.


private 생성자

  • 지금까지 객체의 생성이 클래스의 외부에서 진행됐기 때문에 생성자는 public으로 선언되어야 한다.
  • 그러나 클래스 내부에서 객체를 생성한다면, 생성자를 private으로 선언해도 된다

PrivateConstructor.cpp

#include <iostream>
using namespace std;

class AAA
{
private:
	int num;

public:
	AAA() : num(0) {}
	AAA& CreateInitObj(int n) const
	{
		AAA * ptr=new AAA(n);
		return *ptr;
	}
	void ShowNum() const { cout<<num<<endl; }

private:
	AAA(int n) : num(n) {}
};

int main(void)
{
	AAA base;
	base.ShowNum();

	AAA &obj1=base.CreateInitObj(3);
	obj1.ShowNum();

	AAA &obj2=base.CreateInitObj(12);
	obj2.ShowNum();

	delete &obj1;
	delete &obj2;
	return 0;
}
0
3
12

 

 


소멸자의 이해와 활용

객체생성시 반드시 호출되는 것이 생성자라면, 객체소멸시 반드시 호출되는 것은 소멸자이다.

소멸자는 다음의 형태를 갖는다.

  • 클래스의 이름 앞에 '~'가 붙은 형태의 이름을 갖는다.
  • 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
  • 매개변수는 void형으로 선언되어야 하기 때문에 오버로딩도, 디폴트 값 설정도 불가능하다.

  • 이러한 소멸자는 대게 생성자에서 할당한 리소스의 소멸에 사용된다.
  • 예를 들어서 생성자 내에서 new 연산자를 이용해서 할당해 놓은 메모리 공간이 있다면, 소멸자에서는 delete 연산자를 이용해 메모리 공간을 소멸한다

 

Destructor.cpp

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
private:
	char * name;
	int age;
public:
	Person(char * myname, int myage) //생성자
	{
		int len=strlen(myname)+1;
		name=new char[len]; //생성자에서 new 키워드로 메모리 할당
		strcpy(name, myname);
		age=myage;
	}

	void ShowPersonInfo() const
	{
		cout<<"이름: "<<name<<endl;
		cout<<"나이: "<<age<<endl;
	}
	
	~Person() //소멸자
	{
		delete []name; //소멸자에서 메모리 해제
		cout<<"called destructor!"<<endl;
	}
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2("Jang dong gun", 41);
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}
이름: Lee dong woo
나이: 29
이름: Jang dong gun
나이: 41
called destructor!
called destructor!

 


실습 문제

 

문제 1

앞서 제시한 문제 4-2를 해결하였는가? 당시만 해도 생성자를 설명하지 않은 상황이기 때문에 별도의 초기화 함수를 정의 및 호출해서 Point, Circle, Ring 클래스의 객체를 초기화 하였다. 이 때 구현한 답에 대해서 모든 클래스에 생성자를 정의해보자

 

내 풀이

#include "stdafx.h"
#include <iostream>
#include <cstring>

using namespace System;
using namespace std;

class Point 
{
private:
	int xpos, ypos;
public:
	Point(int x, int y):xpos(x), ypos(y){}
	void ShowPointInfo() const
	{
		cout << "[" << xpos << ", "<< ypos << "]" << endl;
	}

};

class Circle{ //
private:
	Point pos;
	int radius;
public:
	Circle(int x, int y, int r) : pos(x, y), radius(r){	}
	void ShowCircleInfo() const{
		cout<<"radius: "<<radius << endl;
		pos.ShowPointInfo();
	}

};

class Ring{ 
private:
	Circle c1;
	Circle c2;
public:
	Ring(int x1, int y1, int r1, int x2, int y2, int r2):c1(x1, y1, r1), c2(x2, y2, r2){}
	void ShowRingInfo() {
		cout << "Inner Circle Info..." << endl;
		c1.ShowCircleInfo();
		cout << "Outer Circle Info..." << endl;
		c2.ShowCircleInfo();
	}
};


int main (void)
{
	Ring ring(1, 1, 4, 2, 2, 9);
	ring.ShowRingInfo();
	return 0;
}

 

문제 2

명함을 의미하는 NameCard 클래스를 정의해보자. 이 클래스에는 다음의 정보가 저장되어야 한다.

  • 성명
  • 회사이름
  • 전화번호
  • 직급

단, 직급 정보를 제외한 나머지는 문자열의 형태로 저장을 하되, 길이에 딱 맞는 메모리 공간을 할당 받는 형태로 정의하자 그리고 직급 정보는 int형 멤버변수를 선언해서 저장을 하되, 아래의 enum 선언을 활용해야 한다.

    enum{CLERK, SENIOR, ASSIST, MANAGER};

 

위의 enum 선언에서 정의된 상수는 순서대로 사원, 주임, 대리, 과장을 뜻한다.

그럼 다음 main 함수와 실행의 예를 참조하여, 이 문제에서 원하는 형태대로 NameCard 클래스를 완성해보자

int main(void)
{
        NameCard manClerk("Lee", "ABCEng", "010-1111-2222", COMP_POS::CLERK);
        NameCardman SENIOR("Hong", "OrangeEng", "010-3333-4444", COMP_POS::SENIOR);
        NameCard manAssist("Kim", "SoGoodComp", "010-5555-6666", COMP_POS::ASSIST);

        manClerk.ShowNameCardInfo();
        manSENIOR.ShowNameCardInfo();
        manAssist.ShowNameCardInfo();

        return 0;
}

 

내 풀이

#include "stdafx.h"
#include <iostream>
#include <cstring>

using namespace System;
using namespace std;

namespace COMP_POS
{
	enum{CLERK, SENIOR, ASSIST, MANAGER};

	void ShowPositionInfo(int pos){
		switch(pos){
		case CLERK:
			cout<<"사원"<<endl;
			break;
		case SENIOR:
			cout<<"주임"<<endl;
			break;
		case ASSIST:
			cout<<"대리"<<endl;
			break;
		case MANAGER:
			cout<<"과장"<<endl;
			break;
		}
	}
}

class NameCard{
private:
	char * name;
	char * company;
	char * phone;
	int position;
public:
	NameCard(char * _name, char * _company, char * _phone, int pos):position(pos){
		name = new char[strlen(_name)+1];
		company = new char[strlen(_company)+1];
		phone = new char[strlen(_phone)+1];
		strcpy_s(name, strlen(_name) + 1, _name);
		strcpy_s(company, strlen(_company) + 1, _company);
		strcpy_s(phone, strlen(_phone) + 1, _phone);
	}
	void ShowNameCardInfo(){
		cout<<"이름: "<<name<<endl;
		cout<<"회사: "<<company<<endl;
		cout<<"전화번호: "<<phone<<endl;	
		cout<<"직급: "; COMP_POS::ShowPositionInfo(position);
		cout<<endl;
	}
	~NameCard(){
		delete []name;
		delete []company;
		delete []phone;
	}
};


int main (void)
{
	NameCard manClerk("Lee", "ABCEng", "010-1111-2222", COMP_POS::CLERK);
	NameCard manSENIOR("Park", "OrangeEng", "010-3333-4444", COMP_POS::SENIOR);
	NameCard manAssist("Kim", "SoGoodComp", "010-5555-6666", COMP_POS::ASSIST);

	manClerk.ShowNameCardInfo();
	manSENIOR.ShowNameCardInfo();
	manAssist.ShowNameCardInfo();

	return 0;
}
반응형