
지금까지 private으로 선언된 멤버변수들을 초기화하기 위해 InitMembers라는 함술르 정의하고 호출했다. 여간 귀찮은 일이 아니다. 하지만 '생성자'라는 것을 이용하면 객체도 생성과 동시에 초기화할 수 있다.
생성자의 이해

이러한 유형의 함수를 가리켜 '생성자'라고 하고, 다음의 형태를 가지고 있다.
생성자를 하나도 정의하지 않으면 컴파일러에 의해 "디폴트 생성자"라는 것이 삽입된다. 뒤에 가서 더 살펴보도록 하자.
생성자는 다음과 같이 작성하면 된다.

예제를 통해 다음 두 가지 사실을 추가로 알아보자
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

10행에 정의된 생성자를 이용해 객체를 생성하기 위해, 다음과 같이 문장을 구성하면 안된다.
SimpleClass sc1()
이는 두가지로 해석할 수 있다.
(1 : 객체의 생성, 2 : 함수의 원형선언)
컴파일러는 1인지 2인지 구분이 불가해서 위와 같이 문장을 구성하는 것은 불가능하다.
앞서 제시한 Point, Rectangle 클래스에 생성자를 적용해보자.


하지만 Rectangle 클래스의 생성자 정의는 조금 더 생각을 해야한다. Ractangle 클래스는 두 개의 Point 객체를 멤버로 지니고 있어서 Rectangle 객체가 생성되면, 두 개의 Point 객체가 함께 생성된다. 따라서 다음과 같은 생각을 하지 않을 수가 없다.
"Rectangle 객체를 생성하는 과정에서 Point 클래스의 생성자를 통해 Point 객체를 초기화할 수 없을까?
이는 '멤버 이니셜라이저(member initializer)'라는 것을 사용하면 된다.
'멤버 이니셜라이저(member initializer)'를 이용한 멤버 초기화

생성자는 직사각형을 이루는 두 점의 정보를 직접 전달할 수 있게 정의하였다. 물론 이 정보를 통해서 두 개의 Point 객체가 초기화되어야 한다. 그럼 이어서 Rectangle 클래스의 생성자 정의를 보이겠다.
Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2)
:upLeft(x1, y1), lowRight(x2, y2)
{
//empty
}
이 중에서 다음의 내용이 '멤버 이니셜라이저'이다.
:upLeft(x1, y1), lowRight(x2, y2)
이것을 의미하는 바는 각각 다음과 같다.
마지막으로 우리는 객체의 생성과정을 다음과 같이 정리할 수 있다.
'멤버 이니셜라이저(member initializer)'를 이용한 변수 및 const 상수(변수) 초기화
'멤버 이니셜라이저'는 객체가 아닌 멤버의 초기화에도 사용할 수 있다.

이니셜라이저를 통해서 멤버변수의 초기화도 가능하며, 이렇게 초기화 하는 경우 선언과 동시에 초기화되는 형태로 바이너리가 구성된다. 즉, 다음의 형태로 멤버 변수가 선언과 동시에 초기화된다고 볼 수 있다.
int num1 = n1;
반면, 생성자의 몸체부분에서 대입연산을 통한 초기화를 진행하면, 선언과 초기화를 각각 별도의 문장에서 진행하는 형태로 바이너리 코드가 생성된다. 그리고 우리는 이러한 사실로부터 다음의 가능성을 발견하게 된다.
"const 멤버변수도 이니셜라이저를 이용하면 초기화가 가능하다!"
const로 선언된 멤버변수도 초기화가 가능하다! 선언과 동시에 초기화가 되는 형태이기 때문이다.
그럼 이니셜라이저를 이용하는 형태로 이 예제를 재구현해보자.
FruitSaleSim3.cpp
#include <iostream>
using namespace std;
class FruitSeller
{
const int APPLE_PRICE;
int numOfApples;
int myMoney;
public:
FruitSeller(int price, int num, int money)
: APPLE_PRICE(price), numOfApples(num), myMoney(money)
{
}
int SaleApples(int money)
{
int num=money/APPLE_PRICE;
numOfApples-=num;
myMoney+=money;
return num;
}
void ShowSalesResult() const
{
cout<<"남은 사과: "<<numOfApples<<endl;
cout<<"판매 수익: "<<myMoney<<endl<<endl;
}
};
class FruitBuyer
{
int myMoney;
int numOfApples;
public:
FruitBuyer(int money)
: myMoney(money), numOfApples(0)
{
}
void BuyApples(FruitSeller &seller, int money)
{
numOfApples+=seller.SaleApples(money);
myMoney-=money;
}
void ShowBuyResult() const
{
cout<<"현재 잔액: "<<myMoney<<endl;
cout<<"사과 개수: "<<numOfApples<<endl<<endl;
}
};
int main(void)
{
FruitSeller seller(1000, 20, 0);
FruitBuyer buyer(5000);
buyer.BuyApples(seller, 2000);
cout<<"과일 판매자의 현황"<<endl;
seller.ShowSalesResult();
cout<<"과일 구매자의 현황"<<endl;
buyer.ShowBuyResult();
return 0;
}
이니셜라이저의 이러한 특징은 멤버변수로 참조자를 선언할 수 있게 합니다
const 변수와 마찬가지로 '참조자'도 선언과 동시에 초기화가 이뤄져야 한다. 따라서 이니셜라이저를 이용하면 참조자도 멤버변수로 선언될 수 있다. 이러한 특성을 파악하기 위한 예제를 보자.
ReferenceMember.cpp
#include <iostream>
using namespace std;
class AAA
{
public:
AAA()
{
cout<<"empty object"<<endl;
}
void ShowYourName()
{
cout<<"I'm class AAA"<<endl;
}
};
class BBB
{
private:
AAA &ref;
const int #
public:
BBB(AAA &r, const int &n)
: ref(r), num(n)
{
}
void ShowYourName()
{
ref.ShowYourName();
cout<<"and"<<endl;
cout<<"I ref num "<<num<<endl;
}
};
int main(void)
{
AAA obj1;
BBB obj2(obj1, 20);
obj2.ShowYourName();
return 0;
}

이니셜라이저의 초기화는 선언과 동시에 초기화되는 형태이므로, 참조자의 초기화도 가능하다!
디폴트 생성자(Default Constructor)
메모리 공간의 할당 이후에 생성자의 호출까지 완료되어야 '객체'라 할 수 있다. 즉, 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다. 생성자를 정의하지 않는 클래스에는 C++ 컴파일러에 의해서 디폴트 생성자라는 것이 자동으로 삽입된다.
그런데 디폴트 생성자는 인자를 받지 않으며, 내부적으로 아무런 일도 하지 않는 생성자이다. 따라서 다음과 같이 정의된 클래스는,

디폴트 생성자가 삽입되므로 다음과 완전히 동일하다.

생성자를 정의하지 않으면 인자를 받지 않고, 하는 일이 없는 디폴트 생성자라는 것이 컴파일러에 의해서 추가된다.
따라서 모든 객체는 무조건 생성자의 호출 과정을 거쳐서 완성된다.
생성자 불일치
매개변수가 void형으로 선언되는 디폴트 생성자는, 생성자가 하나도 정의되어 있지 않을 때에만 삽입이 된다. 즉, 다음과 같이 정의된 클래스에는 디폴트 생성자가 삽입되지 않는다.

따라서 다음의 형태로는 객체생성이 가능하나,

다음의 형태로는 객체생성이 불가능하다. 다음 문장에서 요구하는 생성자가 정의되지도, 자동으로 삽입되지도 않았기 때문이다.

이런식으로 인자를 받지 않는 void형 생성자의 호출은 불가능하다.
따라서 위의 형태로 객체를 생성하려면, 다음의 형태로 생성자를 추가해야 한다.
SoSimple() : num(0) { }
private 생성자
앞서 보인 생성자들을 모두 public으로 선언되었다. 객체의 생성이 클래스의 외부에서 진행되기 때문에 생성자는 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

AAA 클래스의 멤버함수 내에서도 AAA 클래스의 객체 생성이 가능하다! 생성자가 private이라는 것은 외부에서의 객체 생성을 허용하지 않겠다는 뜻이다!
소멸자의 이해와 활용
객체생성시 반드시 호출되는 것이 생성자라면, 객체소멸시 반드시 호출되는 것은 소멸자이다. 소멸자는 다음의 형태를 갖는다.

이러한 소멸자는 대게 생성자에서 할당한 리소스의 소멸에 사용된다. 예를 들어서 생성자 내에서 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];
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!

문자열 길이에 맞게 메모리 공간을 할당하기 위해 동적할당을 이용했다.
이는 언제 소멸되어야 하는가?
객체가 소멸될 때 이 name도 소멸되어야 한다.