[C++] 5.클래스의 완성


생성자(Constructor)



지금까지는 객체를 생성하고 객체의 멤버변수의 초기화를 목적으로

초기화 함수 Init을 정의하고, 호출하였습니다~!

정보은닉을 목적으로 멤버변수들을 private로 선언했으니, 어쩔 수 없지만,

정말 불편합니다.

다행히 ‘생성자’라는 것을 이용하면 객체도 생성과 동시에 초기화할 수 있습니다~!

생성자는 

클래스의 이름과 함수의 이름이 동일합니다.

반환형이 선언되어 있지 않으며, 실제로 반환하지 않습니다.

객체 생성 시 딱 한 번 호출됩니다.



예를 들어보겠습니다.


class example
{
private:
	int num;
public:
	example(int n) //생성자
	{
		num=n;
	}
};



생성자가 추가되었으므로 객체생성과정에서 자동으로 호출되는 생성자에게 전달할 인자의 정보를

다음과 같이 추가해야합니다.

example ex(20); //생성자에 20을 전달

example *ex=new example(20); //생성자에 20을 전달



그리고 생성자도 함수의 일종이니 오버로딩이 가능하고,

매개변수에 ‘디폴트 값’을 설정할 수 있습니다.


//오버로딩 된 생성자


class example2
{
private:
	int num1;
	int num2;
public:
	example2() //생성자1
	{
		num1=0;
		num2=0;
	}
	
	example2(int n1) //생성자2
	{
		num1=n1;
		num2=0;
	}
	
	example2(int n1,int n2) //생성자3
	{
		num1=n1;
		num2=n2;
	}
};

//객체 생성은 다음과 같습니다.
int main(void)
{
	example2 ex1; //생성자 1 호출
	
	example2 ex2(100); //생성자 2 호출
	
	example3 ex3(100,200); //생성자 3 호출
	
	return 0;
}




//단, 조심할게 생성자 1을 호출하여 객체를 생성할 때는,

//example2 ex1(); 라고 하면 안 됩니다.

//왜냐하면 컴파일러는 main함수 내에서 함수원형을 지역적으로 선언했다고 생각하기 때문입니다.

example2 ex1(); (X)

example2 ex1; (O)

example2 *ex1=new example2; (O)

example2 *ex1=new example2(); (O)



멤버 이니셜라이저를 이용한 멤버 초기화



생성자를 이용하여 멤버변수를 초기화 시킬 수 있음을 확인하였습니다.

그럼 다음과 같은 멤버변수는 어떻게 초기화 시킬까요??

//클래스 Point
class Point
{
private:
	int x;
	int y;
public:
	Point(const int &xpos,const int &ypos);
};

//Point를 멤버변수로 갖는 클래스 Set

class Set
{
private:
	Point p1;
	Point p2;
public:
	...
};



생성자를 이용하여 Set의 멤버변수를 초기화하려면,

멤버 이니셜라이저(Member Initializer)를 이용해야 합니다.


//Point를 멤버변수로 갖는 클래스 Set

class Set
{
private:
	Point p1;
	Point p2;
public:
	Set(const int &x1,const int &y1,const int &x2,const int &y2)
			:p1(x1,y1), p2(x2,y2){} //멤버 이니셜라이저
};



여기서 :p1(x1,y1),p2(x2,y2){//empty}

이 부분이 멤버 ‘이니셜라이저’인데,

의미하는바는 다음과 같습니다.

객체 p1 생성 과정에서 x1,y1을 인자로 전달받는 생성자를 호출하고,

객체 p2 생성 과정에서 x2,y2를 인자로 전달받는 생성자를 호출하여라!


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


class example
{
private:
	int num1;
	int num2;
public:
	example(int n1,int n2) :num1(n1),num2(n2){} //멤버이니셜라이저를 이용한 초기화
	
	....
};



이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성됩니다.


class example
{
private:
	int num;
public:
	example(int n) //초기화가 생성자 몸체에서 이뤄진 경우
	{
		num=n;
	}
	
	example(int n) :num(n){} //이니셜라이저를 이용한 초기화
};


//초기화가 생성자 몸체에서 이뤄진 경우에는 두 문장에 비유할 수 있습니다.

int num;
num=n;

//이니셜라이져를 이용한 경우에는 한 문장에 비유할 수 있습니다

int num=n;



위와 같은 특징으로 이니셜라이져를 이용하면, const멤버변수와 참조자변수도 초기화 가능합니다!


const변수와 참조자변수는 선언과 동시에 초기화가 되어야하는데,

이니셜라이져를 이용하면 생성자를 이용해서 초기화 가능합니다.

class example
{
private:
	const int NUM;
	char &ch;

public:
	example(int n,char &c) :NUM(n),ch(c){}
};

int main(void)
{
	char c='A';
	example ex(30,c); //이니셜라이져를 이용해서 생성자로 const변수와 참조자변수 초기화
	
	return 0;
}



초기화의 대상을 명확하게 인식할 수 있고,

성능에 약간의 이점이 있다는 점에서,

많은 프로그래머들은 이니셜라이저를 이용하는 초기화 방법을 사용합니다!



디폴트 생성자



객체의 생성과정은 다음과 같이 정리할 수 있습니다.

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

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

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



생성자가 정의되지 않으면, 메모리 공간의 할당만으로 객체생성이 된다고 착각할 수 있는데,

생성자는 객체 생성시 반드시 호출됩니다!

생성자가 정의되지 않을 때는 디폴트 생성자가 호출됩니다~

디폴트 생성자는 인자를 받지 않으며, 내부적으로는 아무일도 하지 않는 생성자입니다.

따라서, 다음과 같이 정의된 클래스는


class example
{
private:
	int num;
public:
	... //생성자 정의 X
};

디폴트 생성자가 삽입됩니다.

class example
{
private:
	int num;
public:
	example(){} //디폴트 생성자
};



모든 객체는 한번의 생성자 호출을 동반합니다.

이는 new 연산자를 이용한 객체의 생성에도 해당하는 이야기입니다.

하지만 C언어의 malloc함수를 대신 이용하면 생성자는 호출되지 않습니다!

이게 바로 객체 생성 시 반드시 new함수를 써야한다는 이유입니다.



pivate 생성자



앞서 보인 생성자는 모두 public으로 선언되었습니다.

하지만, 생성자를 private로 선언해도 됩니다!


class example
{
private:
	int num;
public:
	example& create(int n) const //객체생성함수
	{
		example *ptr=new example(n); 
		return *ptr;
	}
private: //private로 선언된 생성자
	example(int n) : num(n){}
};

int main(void)
{
	example ex;
	
	example &obj1=ex.create(3); //num을 3으로 초기화하는 객체 생성
	
	delete obj1;
	return 0;
}



객체 생성방법을 제한하고자 하는 경우에 private 생성자도 유용하게 사용됩니다~!



소멸자



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

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

클래스의 이름 앞에 '~' 가 붙은 형태의 이름을 갖는다.

반환형이 선언되어 있지 않으면, 실제로 반환하지 않는다.

매개변수 void형으로 선언되어야 하기 때문에 오버로딩, 디폴트 값 설정도 불가능하다.


~example(){...} //소멸자 객체 소멸시 자동 호출



따라서 아까 pivate 생성자 예제에서 다음과 같이 소멸자를 이용해 delete 할 수 있습니다.


~example()
{
	delete ptr;
}



클래스와 배열



클래스 배열은 구조체 배열과 유사합니다!

먼저, 객체 배열부터 알아보겠습니다.

객체 배열은 말그대로 객체를 여럿히 담을 수 있는 배열입니다.

선언은 다음과 같습니다.

example arr[10];

//동적으로 할당한 경우는 다음과 같은 형태입니다.

example *ptr=new example[10];



배열을 선언하는 경우에도 생성자는 호출 됩니다.

단, 배열의 선언과정에서 호출 할 생성자를 별도로 명시하지 못합니다.

따라서 객체 배열의 초기화는 다음과 같이 합니다.


class example
{
private:
	char *name;
public:
	example()
	{
		name=NULL; //먼저, null포인터로 초기화
	}
	
	void set_example(char *n) //값 세팅함수 
	{
		name=n;
	}
	~example()
	{
		delete []name;
	}
};

int main(void)
{
	example arr[3]; //객체배열
	char namestr[100];
	char *strptr;
	
	for(int i=0;i<3;i++)
	{
		cout<<"이름: "<<endl;
		cin>>namestr; //사용자로부터 문자열 입력받음
		len=strlen(namestr)+1;
		strptr=new char[len];
		strcpy(strptr,namestr); //포인터에 저장
		arr[i].set_example(strptr); //값 새로 셋팅
	}
	
	return 0;
}



다음은 객체 포인터 배열에 대해 알아보겠습니다.

객체 포인터 배열은 객체의 주소 값 저장이 가능한 포인터 변수로 이뤄진 배열입니다.

각각의 포인터가 가리키는 객체를 초기화시키는 방법은 다음과 같습니다.


class example
{
private:
	char *name;
public:
	example(char *n)
	{
		int len=strlen(n)+1;
		name=new char[len];
		strcpy(name,n);
	}
	
	~example()
	{
		delete []name;
	}
};

int main(void)
{
	example *parr[3]; //객체 포인터 배열
	char namestr[100];
	
	for(int i=0;i<3;i++)
	{
		cout<<"이름: "<<endl;
		cin>>namestr; //사용자로부터 문자열 입력받음
		parr[i]=new example(namestr); //객체 생성
	}
	
	delete parr[0];
	delete parr[1];
	delete parr[2];
	return 0;
}



객체배열은 먼저 모든 객체를 NULL이나 0으로 초기화 한 다음에,

set함수로 값을 다시 설정하는 반면,

객체포인터배열은 선언 후 객체를 생성할 때 생성자를 이용해서 초기화가 가능합니다.

따라서 객체배열과 객체포인터배열을 둘 다 사용가능하게 하려면,

다음과 같이 정의해야합니다.


class example
{
private:
	char *name;
public:
	example()
	{
		name=NULL; 
	}
	
	example(char *n)
	{
		int len=strlen(n)+1;
		name=new char[len];
		strcpy(name,n);
	}
	
	void set_example(char *n)  
	{
		name=n;
	}
	
	~example()
	{
		delete []name;
	}
};



this 포인터



멤버함수 내에는 this라는 이름의 포인터를 사용할 수 있는데,

이는 객체 자신을 가리키는 용도로 사용되는 포인터입니다.

this 포인터는 멤버변수이름과 멤버함수의 지역변수의 이름을 같게 하는데

많이 사용됩니다.

그러면 변수의 이름을 짓는 고통을 줄일 수가 있습니다~

class example
{
private:
	int num;
public:
	...
	...
	void function(int num) //매개변수와 멤버변수 이름을 같게한다.
	{
		this->num=207; //멤버변수의 값에 207 저장
		num=105; // 매개변수의 값을 105로 변경
	}
};



마지막으로 this를 이용해서 객체 자신을 참조할 수 있는 참조자의 반환문을 구성할 수 있습니다.


class self
{
private:
	int num;
public:
	self(int n) : num(n){}
	
	self& Add(int n) //객체 자신을 참조할 수 있는 참조자 반환함수
	{
		num+=n;
		return *this;
	}
};


int main(void)
{
	self s(1); //객체생성
	self &ref=s.Add(2); // s.num에 2를 더한 후 자기 자신을 참조하는 참조자 반환
	
	
	//이런 것도 가능합니다~
	ref.Adder(1).Adder(2).Adder(3);
	return 0;
}