본문 바로가기
Language&Framework&Etc/C++

클래스의 완성(04-3) 생성자(Constructor)와 소멸자(Destructor)-1

by 머리올리자 2020. 12. 25.

지금까지는 객체를 생성하고 객체의 멤버변수 초기화를 목적으로 init라는 이름의 함수를 정의하고 호출했다.

 

정보은닉을 목적으로 멤버변수들을 private으로 선언했으니 이는 어쩔 수 없는 일

 

그러나 이는 불편한 부분이 있어 '생성자'라는 것을 이용하면 객체도 생성과 동시에 초기화 할 수 있다.

 


생성자의 이해

클래스 정의 예제

class simpleclass
{
private:
	int num;
public:
	simpleclass(int n) // 생성자(constructor)
	{
		num = n;
	}
	int getnum() const
	{
		return num;
	}
};

위의 클래스 정의에서 다음의 형태를 띠는 함수

  • 클래스의 이름과 함수의 이름이 동일
  • 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.

이러한 유형의 함수를 가리켜 '생성자(constructor)'라 하며, 이는 다음의 특징을 갖는다.

 

"객체 생성시 딱 한번 호출된다."

 

이전에 생성자를 정의하지 않았을 때, 우리는 다음과 같은 방식으로 객체 생성

simpleclass sc;                          // 전역, 지역 및 매개변수의 형태
simpleclass * ptr = new simpleclass;     // 동적 할당의 형태

그러나 생성자가 정의되었으니, 객체생성과정에서 자동으로 호출되는 생성자에게 전달할 인자 정보를 다음과 같이 추가

simpleclass sc(20);                          // 생성자에 20을 전달
simpleclass * ptr = new simpleclass(30);     // 생성자에 30을 전달
  • 생성자도 함수의 일종이니 오버로딩이 가능
  • 생성자도 함수의 일종이니 매개변수에 '디폴트 값'을 설정할 수 있다.
#include <iostream>
using namespace std;

class simpleclass
{
private:
	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 s1;
	s1.showdata();

	simpleclass s2(100);
	s2.showdata();

	simpleclass s3(100, 200);
	s3.showdata();
	return 0;
}

결과를 통해 생성자의 오버로딩이 가능하다는 사실을 확인.

#include <iostream>
using namespace std;

class simpleclass
{
private:
	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 = 10, int n2 = 20)
	{
		num1 = n1;
		num2 = n2;
	}

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

int main(void)
{
	simpleclass s1;
	s1.showdata();

	/*simpleclass s2(100);
	s2.showdata();

	simpleclass s3(100, 200);
	s3.showdata();*/
	return 0;
}

디폴드 값 설정도 가능하다.

 

그렇다면 모두 주석을 해제한 아래 결과는?

#include <iostream>
using namespace std;

class simpleclass
{
private:
	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 = 10, int n2 = 20)
	{
		num1 = n1;
		num2 = n2;
	}

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

int main(void)
{
	simpleclass s1;
	s1.showdata();

	simpleclass s2(100);
	s2.showdata();

	simpleclass s3(100, 200);
	s3.showdata();
	return 0;
}

위와 같이 에러가 발생

 

원인

 

만약 아래와 같이 객체를 생성

simpleclass sc2(100);

그러면 아래처럼 정의된 생성자도 호출이 가능하고

simpleclass(int n)

아래의 생성자도 호출이 가능하다

simpleclass(int n1 = 10, int n2 = 20)

이들 중 호출할 생성자를 결정 못해서 발생하는 에러이다.

 

설명.

simpleclass(int n)

위 생성자는 int형 데이터를 인자로 요구하므로, 생성자를 이용해서 객체를 생성하려면 다음과 같이 문장 구성

simpleclass sc2(100);                          // 전역 및 지역변수의 형태
simpleclass * ptr2 = new simpleclass(100);     // 동적 할당의 형태

다음과 같이 정의된 생성자를 이용해서 객체를 생성하려면 다음과 같이 문장을 구성

simpleclass(int n1 = 10, int n2 = 20)
simpleclass sc3(100, 200);                          // 전역 및 지역변수의 형태
simpleclass * ptr3 = new simpleclass(100, 200);     // 동적 할당의 형태

그러나, 아래와 같이 정의된 생성자를 이용해서 객체를 생성하기 위해

simpleclass()

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

simpleclass s1();                       // (X) 

대신 다음과 같이 구성

simpleclass s1;                         // (O)
simpleclass * ptr1 = new simpleclass;   // (O)
simpleclass * ptr1 = new simpleclass(); // (O)

매개변수가 선언되지 않기 때문에, 소괄호의 생략 허용 이해

simpleclass * ptr1 = new simpleclass;   // (O)

그렇다면, 다음 문장은 왜 허용안함?

simpleclass s1();                       // (X) 

예제로 참고

#include <iostream>
using namespace std;

class simpleclass
{
private:
	int num1;
	int num2;

public:
	simpleclass(int n1 = 0, int n2 = 0)
	{
		num1 = n1;
		num2 = n2;
	}
	void showdata() const
	{
		cout << num1 << ' ' << num2 << endl;
	}
};

int main(void)
{
	simpleclass sc1();  // 밑에 있는 함수의 원형 선언
	simpleclass mysc = sc1(); // sc1 함수를 호출하여, 반환되는 값으로 mysc 객체 초기화
	mysc.showdata();
	return 0;
}

simpleclass sc1()
{
	simpleclass sc(20, 30);
	return sc;
}

보통 함수의 원형은 전역적으로(함수 밖에) 선언,

 

그러나 함수 내에 지역적으로도 선언가능.

 

그리고 simpleclass sc1(); 코드는 함수의 원형에 해당.

 

즉, 이 문장을 void형 생성자의 호출문으로 인정해 버리면, 컴파일러는 이러한 문장을 만났을 때, 이것이 객체생성문인지 함수의 원형선언인지를 구분할 수 없게 된다.

 

그래서 이러한 유형의 문장은 객체생성이 아닌, 함수의 원형선언에만 사용

 


이전 예제에 대한 생성자의 활용

#include <iostream>
using namespace std;

class fruitseller
{
private:
	int apple_price;
	int num_of_apples;
	int mymoney;
public:
	/*이전 void init 대신 생성자*/
	fruitseller(int price, int num, int money)
	{
		apple_price = price;
		num_of_apples = num;
		mymoney = money;
	}
	int saleapples(int money)
	{
		int num = money / apple_price;
		num_of_apples -= num;
		mymoney += money;
		return num;
	}

	void salesresult() const
	{
		cout << "남은 사과 : " << num_of_apples << endl;
		cout << "판매 수익 : " << mymoney << endl << endl;
	}
};

class fruitbuyer 
{
private:
	int mymoney;
	int num_of_apples;
public:
	fruitbuyer(int money)
	{
		mymoney = money;
		num_of_apples = 0;
	}

	void buyapples(fruitseller& seller, int money)
	{
		num_of_apples += seller.saleapples(money);
		mymoney -= money;
	}
	void buyresult() const
	{
		cout << "현재 잔액 : " << mymoney << endl;
		cout << "사과 개수 : " << num_of_apples << endl << endl;
	}
};

int main(void)
{
	fruitseller seller(1000, 20, 0); // 생성자 초기화
	fruitbuyer buyer(5000);
	buyer.buyapples(seller, 2000);

	cout << "과일 판매자 현황" << endl;
	seller.salesresult();

	cout << "과일 구매자 현황" << endl;
	buyer.buyresult();
}

Point와 rectangle 예제

 

여기서 Rectangle 클래스의 생성자 정의는 조금 더 생각을 해야 한다.

 

Rectangle 클래스는 두 개의 Point 객체를 멤버로 지니고 있어서 Rectangle 객체가 생성되면, 두 개의 Point 객체가 함께 생성된다.

 

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

 

멤버 이니셜라이저(member initializer)라는 것을 사용하면 원하는 것을 할 수가 있다.

 

이전 예제에 대한 생성자의 활용

생성자가 추가된 rectangle class의 선언

class rectangle
{
private:
	point upleft;
	point lowright;
public:
	rectangle(const int& x1, const int& y1, const int& x2, const int& y2);
	void showrecinfo() const;
};

생성자는 직사각형을 이루는 두 점의 정보를 직접 전달할 수 있게 정의.

 

물론 이 정보를 통해서 두 개의 point 객체가 초기화되어야 한다.

 

다음은 rectangle 클래스의 생성자 정의

rectangle::rectangle(const int& x1, const int& y1, const int& x2, const int& y2):upleft(x1, y1), lowright(x2, y2)

다음이 멤버 이니셜라이저

:upleft(x1, y1), lowright(x2, y2)

아래가 의미하는 바

"객체 upleft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라"
"객체 upright의 생성과정에서 x2와 y2을 인자로 전달받는 생성자를 호출하라"

 

전체 예제

 

point.h

#ifndef __POINT_H__
#define __POINT_H__

class point
{
private:
	int x;
	int y;
public:
	point(const int& xpos, const int& ypos);
	int getx() const;
	int gety() const;
	bool setx(int xpos);
	bool sety(int ypos);
};

#endif

point.cpp

#include <iostream>
#include "point.h"

using namespace std;

point::point(const int& xpos, const int& ypos)
{
	x = xpos;
	y = ypos;
}

int point::getx() const { return x; }
int point::gety() const { return y; }

bool point::setx(int xpos)
{
	if (xpos < 0 || xpos > 100)
	{
		cout << "벗어난 범위의 값 전달" << endl;
		return false;
	}
	x = xpos;
	return true;
}

bool point::sety(int ypos)
{
	if (ypos < 0 || ypos > 100)
	{
		cout << "벗어난 범위의 값 전달" << endl;
		return false;
	}
	y = ypos;
	return true;
}

rectangle.h

#include <iostream>
#include "point.h"

using namespace std;

point::point(const int& xpos, const int& ypos)
{
	x = xpos;
	y = ypos;
}

int point::getx() const { return x; }
int point::gety() const { return y; }

bool point::setx(int xpos)
{
	if (xpos < 0 || xpos > 100)
	{
		cout << "벗어난 범위의 값 전달" << endl;
		return false;
	}
	x = xpos;
	return true;
}

bool point::sety(int ypos)
{
	if (ypos < 0 || ypos > 100)
	{
		cout << "벗어난 범위의 값 전달" << endl;
		return false;
	}
	y = ypos;
	return true;
}

rectangle.cpp

#include <iostream>
#include "rectangle.h"
using namespace std;

/*
:upleft(x1, y1), lowright(x2, y2) => 멤버 이니셜라이져
객체 upleft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라
객체 upright의 생성과정에서 x2와 y2을 인자로 전달받는 생성자를 호출하라
*/
rectangle::rectangle(const int& x1, const int& y1, const int& x2, const int& y2) :upleft(x1, y1), lowright(x2, y2)
{
	// empty
	// 멤버 이니셜라이저를 사용하다 보면, 생성자의 몸체 부분이 그냥 비는 일이 종종 발생, 문제 없음
}

void rectangle::showrecinfo() const
{
	cout << "좌 상단: " << "[" << upleft.getx() << ", ";
	cout << upleft.gety() << "]" << endl;
	cout << "우 상단: " << "[" << lowright.getx() << ", ";
	cout << lowright.gety() << "]" << endl << endl;
}

main.cpp

#include <iostream>
#include "point.h"
#include "rectangle.h"

using namespace std;

int main(void)
{
	rectangle rec(1, 1, 5, 5);
	rec.showrecinfo();
	return 0;
}

객체생성 과정

1단계 : 메모리 공간의 할당

2단계 : 이니셜라이저를 이용한 멤버변수(객체)의 초기화

3단계 : 생성자의 몸체부분 실행

 

C++의 모든 객체는 위의 세가지 과정을 순서대로 거쳐서 생성이 완성된다.

 

물론 이니셜라이저가 선언되지 않았다면, 메모리 공간의 할당과 생성자의 몸체부분의 실행으로 객체생성은 완성

 

"그럼, 생성자도 정의되어 있지 않으면, 메모리 공간의 할당만으로 객체생성이 완료?"

 

아님!

 

생성자는 이니셜라이저처럼 선택적으로 존재하는 대상이 아니다.

 

생성자는 반드시 호출이 된다. 

 

우리가 생성자를 정의하지 않으면, '디폴트 생성자(default constructor)'라는 게 자동으로 삽입되어 호출

 

참고 : [윤성우 열혈 C++ 프로그래밍] - 대부분의 내용 및 코드는 이 책에서 개인 공부 정리 목적으로 참고하였습니다.