Media Log


1. 생성자(Constructor)


오늘은 객체 생성/소멸시에 호출되는 생성자와 소멸자에 대해 알아보도록 하겠습니다. 우리는 바로 전 강좌에서, private로 지정된 필드(=멤버 변수)를 초기화 시키기 위하여 SetInfo 함수를 따로 만들어 초기화 시켜주었습니다. 그런데, 이것보다 더 편하게 객체 생성과 동시에 초기화 시켜주는 녀석이 있습니다. 그 녀석이 바로 생성자라는 녀석입니다. 아래는 생성자의 형식입니다.

class 클래스명 {
public:
   클래스명(매개변수..)
   {
       // ...
    }
    // ..
}

위의 형식을 보시면, 생성자를 정의할때 생성자의 이름이 클래스의 이름과 같습니다. 생성자도 함수와 같이 매개변수를 가질 수 있습니다. 그리고 반환형이 없습니다. 한번 SetInfo 함수 대신 생성자를 이용해서 예제를 작성해보도록 합시다.

#include <iostream>
 
using namespace std;
 
class student {
private:
    char * name;
    int age;
    char * hobby;
public:
	student(char * _name, int _age, char * _hobby);
    void ShowInfo();
    void Study();
    void Sleep();
};

student::student(char * _name, int _age, char * _hobby)
{
    name = _name;
    age = _age;
    hobby = _hobby;
}

void student::ShowInfo()
{
    cout << "이름: " << name << ", 나이: " << age << ", 취미: " << hobby << endl;
}
 
void student::Study()
{
    cout << "공부!" << endl;
}
 
void student::Sleep()
{
    cout << "잠!" << endl;
}
 
int main()
{
    student stu("김철수", 16, "컴퓨터 게임");
 
    stu.ShowInfo();
 
    //while(true) {
    //    stu.Study();
    //    stu.Sleep();
    //}
 
    return 0;
}

결과:

이름: 김철수, 나이: 16, 취미: 컴퓨터 게임

계속하려면 아무 키나 누르십시오 . . .


코드의 11행을 보시면 생성자가 보이시죠? 17~22행을 보시면 생성자가 정의되어 있는데, 하는 일은 SetInfo 함수와 같습니다. 41행을 보시면 객체 생성시 호출되는 생성자에게 인자를 넘겨 초기화를 시킵니다. SetInfo를 이용한 방법보다 간편하죠? 45~48행은 주석처리 해버렸습니다. 주석은 푸셔도 상관 없습니다.


생성자의 또다른 특징에는 생성자도 함수 중 하나니 함수 오버로딩이 가능하다는 점입니다. 아래와 같이 말이죠.

#include <iostream>
 
using namespace std;
 
class ExConstructor
{
public:
	ExConstructor()
	{
		cout << "ExConstructor() called!" << endl;
	}

	ExConstructor(int a)
	{
		cout << "ExConstructor(int a) called!" << endl;
	}

	ExConstructor(int a, int b)
	{
		cout << "ExConstructor(int a, int b) called!" << endl;
	}
};

int main()
{
	ExConstructor ec1;
	ExConstructor ec2(10);
	ExConstructor ec3(20, 10);
 
    return 0;
}

결과:

ExConstructor() called!

ExConstructor(int a) called!

ExConstructor(int a, int b) called!

계속하려면 아무 키나 누르십시오 . . .


각각 8, 13, 18행을 보시면 생성자가 오버로딩 된것을 확인하실 수 있습니다. 26~28행에서 넘겨주는 인자의 형식, 수에 따라 그에 맞는 생성자가 호출됩니다.


또 하나는, 위에서 말한대로 생성자가 객체 생성시 호출되는 함수라고 했는데 우리가 생성자를 구현하지 않으면 어떻게 될까요? 우리가 클래스 내에 생성자를 구현하지 않으면 C++ 컴파일러에서 그 클래스 내에 디폴트 생성자라는 것을 알아서 넣어줍니다. 아래와 같이 인자를 받지도 않고, 아무런 일도 하지않는 생성자를요.

클래스명() { }

이렇게, 객체가 만들어질때는 반드시 생성자 호출을 합니다. 이번에는, 복사 생성자에 대해 알아보도록 합시다.


2. 복사 생성자(Copy Constructor)


복사 생성자(Copy Constructor)는 자신과 같은 자료형의 객체를 인수로 전달하는 생성자입니다. 복사 생성자를 설명하기전, 변수와 참조자, 그리고 객체의 초기화를 먼저 다루도록 하겠습니다. 복사 생성자를 이해하시려면 참조자와 생성자를 이해하고 계셔야 합니다. 참조자 관련 게시글을 보시려면 아래 링크를 클릭하세요.

(참조자(Reference): http://blog.eairship.kr/170)


C와 C++ 스타일의 초기화 방식을 한번 비교해보도록 합시다.

int a(50); // C++ 스타일 초기화
int b = 40; // C 스타일 초기화

cout << "a: " << a << " b: " << b << endl;

위의 코드에서 a와 b를 출력해보면, a는 50, 예상하듯 b는 40을 출력합니다. C에서는 2행과 같이 초기화가 가능했으나, C++에서는 1행, 2행 방식 모두 초기화가 가능합니다. 그럼, 객체도 이렇게 초기화가 가능할까요? 아래의 코드를 한번 보도록 합시다.

#include <iostream>

using namespace std;

class MyClass
{
private:
	int num1;
	int num2;
public:
	MyClass(int a, int b)
	{
		num1 = a;
		num2 = b;
	}
	void ShowData()
	{
		cout << "num1: " << num1 << " num2: " << num2 << endl;
	}
};

int main()
{
	MyClass mc1(50, 40);
	MyClass mc2 = mc1;

	mc2.ShowData();
	return 0;
}

결과:

num1: 50 num2: 40

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면 MyClass라는 클래스가 정의되었고, 그 안에는 num1과 num2란 멤버 변수와, 생성자, 그리고 num1과 num2의 값을 출력하는 ShowData라는 함수가 존재합니다. 24행을 보시면, mc1라는 객체가 만들어지면서 생성자에게 50과 40이란 값을 넘겨주고 mc1 객체 내의 num1과 num2는 각각 a와 b의 값으로 초기화 됩니다. 그런데 다음 25행에서 등장하는 구문을 한번 보도록 합시다. 변수처럼, mc2 객체에 mc1 객체를 대입시키고 있습니다. 그리고 27행을 보시면 ShowData 함수로 mc2 객체 내의 num1 변수와 num2변수의 값을 출력하고 있습니다. (실제로는 자동으로 MyClass mc2 = mc1;이 MyClass mc2(mc1);로 변환이 됩니다.)


결과를 보시면 mc1 객체 내의 num1과 num2 멤버 변수의 값과 같음을 알 수 있습니다. 마치, 멤버별 복사가 이루어진것처럼 말이죠. 생각해보면, MyClass 객체를 인수로 받는 생성자를 구현하지 않았음에도, 오류가 나지않고 정상적인 결과를 출력합니다. 이는, 우리가 따로 복사 생성자를 정의하지 않아도 디폴트 생성자처럼, 디폴트 복사 생성자가 컴파일러에 의해 중간 삽입됩니다. 아래와 같이 말이죠.

...
	MyClass(int a, int b)
	{
		num1 = a;
		num2 = b;
	}
	MyClass(const MyClass& mc) // 디폴트 복사 생성자의 형태
	{
		num1 = mc.num1;
		num2 = mc.num2;
	}
...

위와 같이 멤버별 복사가 이루어지는 방식을 가르켜 '얕은 복사(Shallow Copy)'라고 합니다. 그런데, 이 얕은 복사에 문제점이 존재합니다. 한번 같이 어떠한 문제점이 살펴보기전, 간단히 소멸자에 대해 알아두고 넘어갑시다.


3. 소멸자(Destructor)


생성자가 객체 생성시 호출되는 함수라면, 소멸자는 객체 소멸시 호출되는 함수입니다. 아래는 소멸자의 형식입니다. 주로 소멸자는 객체 소멸시 자동 호출되기에, 객체의 메모리 반환 즉 할당한 리소스의 해제를 위해 사용합니다.

class 클래스명 {
public:
   ~클래스명()
   {
       // ...
    }
    // ..
}

생성자와는 달리, 클래스 이름 앞에 ~가 붙은 형태를 가집니다. 그리고 매개변수도 가질 수 없습니다. (소멸자 역시 반환형이 존재하지 않습니다. 또한 소멸자를 정의하지 않으면 컴파일러에서 디폴트 소멸자를 넣어줍니다.) 아래 예제에서 각각 생성자와 소멸자가 호출되면 호출되었다고 출력하도록 했습니다.

#include <iostream>
 
using namespace std;
 
class ExConstructor
{
public:
	ExConstructor()
	{
		cout << "ExConstructor() called!" << endl;
	}

	~ExConstructor()
	{
		cout << "~ExConstructor() called!" << endl;
	}
};

int main()
{
    ExConstructor ec;
 
    return 0;
}

결과:

ExConstructor() called!

~ExConstructor() called!

계속하려면 아무 키나 누르십시오 . . .


13~16행을 보시면 소멸자가 정의되었습니다. 객체의 소멸은 소멸자를 호출하고 나서 메모리를 반환하는 순서로, 객체가 소멸됩니다. 이 소멸자는 메모리 반환시에 반환되지 않은 메모리 공간을 명시적으로 반환하기 위해 사용합니다.


4. 얕은 복사의 문제점, 그리고 깊은 복사(Deep Copy)


이어서, 디폴트 복사 생성자(얕은 복사 방식)의 문제점을 보도록 하겠습니다. 아래를 우선 보시죠.

#include <iostream>

using namespace std;

class MyClass
{
private:
	char *str;
public:
	MyClass(const char *aStr)
	{
		str = new char[strlen(aStr)+1];
		strcpy(str, aStr);
	}
	~MyClass() {
		delete []str;
		cout << "~MyClass() called!" << endl;
	}
	void ShowData()
	{
		cout << "str: " << str << endl;
	}
};

int main()
{
	MyClass mc1("MyClass!");
	MyClass mc2 = mc1;

	mc1.ShowData();
	mc2.ShowData();
	return 0;
}

결과:

str: MyClass!

str: MyClass!

~MyClass() called!


위 코드를 한번 컴파일해보면, "~MyClass() called!"가 단한번만 출력되고 오류가 발생합니다. 생각해보면, mc1 선언과 동시에 생성자 내에서 str를 메모리에 할당합니다. 그리고 mc2 선언시에 디폴트 복사 생성자가 호출되고, 메모리를 할당하지 않고 str의 포인터만 복사합니다. 그런 뒤에, mc2 객체가 먼저 소멸되고 mc2의 소멸자가 호출되고 str를 메모리 공간에서 해제시킵니다. 그리고 mc1 소멸자가 호출되어 str 포인터가 가르키고 있는 메모리 공간을 해제하려 하나, 이미 mc2의 소멸자에 의해 해제되었으므로 오류가 발생합니다.


이를 해결하기 위해서는, 포인터로 참조하는 대상까지 복사하는 "깊은 복사(Deep Copy)"가 필요합니다. 깊은 복사는 위의 MyClass 생성자 내의 코드를 똑같이 구현하시면 됩니다. 한번 볼까요?


#include <iostream>

using namespace std;

class MyClass
{
private:
	char *str;
public:
	MyClass(const char *aStr)
	{
		str = new char[strlen(aStr)+1];
		strcpy(str, aStr);
	}
	MyClass(const MyClass& mc)
	{
		str = new char[strlen(mc.str)+1];
		strcpy(str, mc.str);
	}
	~MyClass() {
		delete []str;
		cout << "~MyClass() called!" << endl;
	}
	void ShowData()
	{
		cout << "str: " << str << endl;
	}
};

int main()
{
	MyClass mc1("MyClass!");
	MyClass mc2 = mc1;

	mc1.ShowData();
	mc2.ShowData();
	return 0;
}

결과:

str: MyClass!

str: MyClass!

~MyClass() called!

~MyClass() called!

계속하려면 아무 키나 누르십시오 . . .


이번에는 오류가 뜨지 않고 정상적으로 출력됨을 확인하실 수 있습니다. 메모리 공간 할당 후 문자열을 복사합니다. 그 다음에는 할당된 메모리의 주소를 str에 저장합니다. 이렇게, 깊은 복사와 얕은 복사, 그리고 복사 생성자를 추가로 설명하다 보니 이해하기 어려운 부분이 많아졌고 설명이 부족한 부분도 몇몇 보이네요. 이해가 되지 않는 부분은 덧글로 달아주세요.


이번 강좌는 여기서 마치도록 하겠습니다. 수고하셨고, 다음 강좌에서는 Bool, Inline에 대해 알아보도록 하겠습니다.

  1. 박상훈 at 2013.01.15 14:01 신고 [edit/del]

    저기 복사생성자 결과가 ..a:50 b:40이 아니라 num1:50 num2:40이 나올 것 같습니다..
    지금 C++강좌 보고있는데 책이 필요하지 않을 만큼 쉽게 설명해주셨네요.
    자주 애용하겠습니다 . 정말 감사합니다.

    Reply
  2. 카카루 at 2013.03.06 23:48 신고 [edit/del]

    안녕하세요 정말 잘 보고 있습니다^^ 얕은 복사 문제점 설명하실 때 자동으로 만들어지는 디폴트 복사 생성자 코드를 알 수 있을까요?? 머리가 나빠서 깊은 복사에서 직접 입력하신 복사생성자 코드랑 어떻게 다른지 알아야 str 포인터만 복사하는지 알 수 있을 것 같아요ㅠ

    Reply
    • BlogIcon EXYNOA at 2013.03.06 23:49 신고 [edit/del]

      내용이 텅텅 비어있는 생성자입니다. 만약 클래스 이름이 Student라면 아래와 같은 코드가 디폴트로 삽입되는 것입니다.

      Student() { }

    • 카카루 at 2013.03.09 21:03 신고 [edit/del]

      아까 예시로 적어주신 디폴트 복사 생성자는 텅텅 비어 있지 않았잖아요??
      MyClass(const MyClass& mc) // 디폴트 복사 생성자의 형태
      {
      num1 = mc.num1;
      num2 = mc.num2;
      }
      얕은복사에서 디폴트 복사 생성자 안이 텅텅 비어 있으면 어떻게 str의 포인터를 복사 할 수 있는지 알고 싶습니다.

    • BlogIcon EXYNOA at 2013.03.09 22:23 신고 [edit/del]

      아, 그런 말씀이셨군요. 제가 내용을 오해한것 같습니다.

      '따로 복사 생성자를 정의하지 않아도 디폴트 생성자처럼, 디폴트 복사 생성자가 컴파일러에 의해 중간 삽입됩니다'

  3. 이원준 at 2013.04.15 17:29 신고 [edit/del]

    포인터도 이해못해는 저인데 얼추 익혀나가고 있습니다. 감사합니다

    Reply
  4. 푸르름 at 2013.05.31 17:52 신고 [edit/del]

    4. 얕은 복사의 문제점, 그리고 깊은 복사(Deep Copy) 에서,
    MyClass mc1("MyClass!");
    MyClass mc2 = mc1;

    mc1.ShowData();
    mc2.ShowData();
    return 0;
    이렇게 코딩되어 있고,
    객체 소멸 순서가 mc2 ----> mc1 이렇게 된다고 하셨는데,
    동일한 객체를 호출하면 소멸 순서가 코딩과 역순으로 되는 것인가요?
    상식적으로 생각하면 함수를 호출할 때 코딩 순서대로 호출되고 사라지므로
    클래스 객체도 마찬가지일 것이라고 생각됩니다만...그렇지 않은가봐요?



    Reply
    • BlogIcon EXYNOA at 2013.05.31 18:45 신고 [edit/del]

      네 맞습니다. 객체의 소멸 순서는 생성 순서의 반대이며, 이는 함수의 연속적인 호출로도 예를 들수가 있습니다. 즉, 소멸자의 호출 순서는 생성자의 호출 순서의 역순이라고 말할 수 있습니다.

  5. 배움 at 2013.06.28 14:32 신고 [edit/del]

    안녕하세요.
    MyClass mc2 = mc1; 이 줄에서 두번째 생성자가 호출되는 건가요?
    MyClass(const MyClass& mc){ }

    Reply
  6. 감사 at 2013.11.05 16:08 신고 [edit/del]

    strlen 함수를 쓰는 예제에서 계속 에러가 나서 (devC++)
    여기저기 찾아보다가,
    #include <cstring> 을 앞에 넣어 주어야 되네요.
    visual studio 로 할 경우에는 안넣어 주어도 되구요
    왜 이런 차이가 나나요?
    완전초보라서 질문드립니다.

    Reply
    • BlogIcon EXYNOA at 2013.11.05 18:42 신고 [edit/del]

      포함하는 헤더파일의 내용이 저와 달라서 그런것 같습니다. 저 같은 경우에는 iostream 부터 시작하여 xlocale에서 cstring을 include 시키기 때문에 따로 cstring 헤더를 포함하지 않아도 오류가 발생하지 않았던 것입니다. 오류가 만약 나시면 지금 하고 계시는것처럼 cstring을 포함해주세요.

  7. 꼬꼬마중학생 at 2014.06.03 22:12 신고 [edit/del]

    굿굿

    Reply
  8. 빨간산 at 2014.11.04 23:44 신고 [edit/del]

    안녕하세요, 강좌를 따라 하나씩 감사하게 배우고 있습니다. 다만 strcpy(str, aStr);의 strcopy 에서 에러가 나네요. 아무리 봐도 잘 따라 타이핑을 한것 같은데 빌드가 되지 않습니다.
    c:\program files (x86)\microsoft visual studio 12.0\vc\include\string.h(112) : 'strcpy' 선언을 참조하십시오.
    라고 메세지가 출려되는데 혹 이유를 아시는지... 비주얼 스튜디오 2013 익스프레스입니다.

    Reply
    • 냐앙 at 2016.11.07 21:35 신고 [edit/del]

      본문에는 없는데 strcpy를 쓸려면 #include<cstring>이 맨 위에 있어야 합니다. c에서는 string.h에 해당하는거죠. 확인해보세요.

  9. 루이수지 at 2017.03.31 17:49 신고 [edit/del]

    안녕하세요 학교수업이 어려워서 따로 인터넷찾아보면서 C++ 복습하는 사람입니다! 소멸자 궁금한게 있습니다.
    ~ExConstructor()
    {
    cout << "~ExConstructor() called!" << endl;
    }
    이렇게 소멸자를 쓰셨는데, { } 안에 있는 설명을 다 지우고, ~ExConstructor() {} 이렇게만 남기면 소멸자는 ctrl+f5 눌렀을때 나타나진 않는데 제 기능을 할 수 있나요?

    Reply
  10. 4심 at 2017.06.08 13:53 신고 [edit/del]

    컨스트럭터가 게임같은거 만들때 하나 copy constructor나 general constructor 등등 컨스트럭터가 많은데 왜 그런거죠?

    Reply
  11. 질문있습니다! at 2017.06.12 23:05 신고 [edit/del]

    shallow copy의 문제점을 설명하실 때에 예로든 코드에서 위의 코드 결과가 ~MyClass() called!도 두개가 뜹니다. 코드블록이라서 그런걸까요?

    Reply
  12. qwd at 2017.08.25 14:03 신고 [edit/del]

    좋은 설명 감사합니다~

    Reply
  13. 질문있습니다~! at 2018.02.09 10:43 신고 [edit/del]

    첫번째 예제에서
    // while(true) {... 에서 true 대신에 age > 7 이라는 조건을 걸고 싶으면 무슨 변수를 넣어야 하나요?
    while( age>7), while( stu.age>7), while( student::age>7)
    while( _age>7),while( stu._age>7), while( student::_age>7) 다 해봤는데 안 되서 답을 못찾겠네요 ㅜㅜ

    Reply
    • znskjdawer at 2018.02.09 20:25 신고 [edit/del]

      age는 private로 선언된 클래스 내부에서만 사용 가능한거라 main에서는 사용이 불가능해서 오류가 나는거 같습니다~

  14. ask at 2018.08.31 20:45 신고 [edit/del]

    이전까지의 강의에서는 소멸자를 따로 지정하지 않았는데, 소멸자를 따로 지정하지 않으면 어떻게 이루어지는건가요? 소멸자가 자동으로 생성되어 만들어지는 건가요, 아니면 소멸자가 없는 상태로 그냥 함수가 끝나 메모리가 낭비되게 되는 것인가요?

    그리고 생성자와 소멸자를 각각 malloc, free와 비슷하게 생각해도 되는건가요?

    Reply

submit

티스토리 툴바