본문 바로가기
프로그래밍/C++

복사 생성자, 디폴트 생성자 그리고 디폴트 복사 생성자.

by 리뷰하는 (게임)프로그래머_리프TV 2010. 3. 21.




복사 생성자,
디폴트 생성자,
디폴트 복사 생성자,
그리고 디폴트 소멸자까지

쭈욱~ 둘러 보자.

사실 다 따로따로 포스팅 할까 하다가

아무리 봐도 한번에 설명하는게 좀더 나을것 같다는 생각이 든다.

자 일단 쉬운것 부터

디폴트 생성자, 디폴트 소멸자

class를 생성할 때에 우리는 소멸자를 만들 수도
만들지 않을 수도
생성자를 만들 수도
만들지 않을 수도 있다.

이것은 당연한 것이다.

하지만 컴파일러는 만들지 않으면 기본적으로 만들어 준다.

그게 중요하다는 거다.


#include <iostream>

class AAA
{
	// 생성자 소멸자 선언을 해주든 안해주든
	// 컴파일을 하면 다음과 같은 디폴트 생성자, 소멸자가 존재 한다고 보면 된다.
	// 물론 하는 일은 아무것도 없다.
public:
	AAA(){ }
	~AAA(){ }
};

void main()
{
	AAA a;
}




하지만 한가지 주의할 점은 프로그래머가 생성자를 하나라도 생성하게 된다면,
디폴트 생성자는 생성 되지 않는 다는 것이다.

#include <iostream>

class AAA
{
public:
	AAA(int n){ }
	~AAA(){ }
};

void main()
{
	// int 형을 받는 생성자는 문제 없지만
	AAA a(10);

	// 다음 선언은 컴파일 오류를 일으킨다.
	AAA b;

	// 하나라도 생성자 선언을 해주었기 때문에 디폴트 생성자가 생성 되지 않기 때문이다.
}

자, 여기 까지 봤으면 일단 복사 생성자에 대해서 알아 보자.

#include <iostream>

class AAA
{
public:
	int n;
};

void main()
{
	AAA a;
	// a.n에 10을 넣어 준건 알겠는데
	a.n = 10;

	// 이것이 (디폴트)복사 생성자
	AAA b(a);

	// 출력하면 b.n에도 10이 들어가 있다?
	std::cout << a.n << std::endl;
	std::cout << b.n << std::endl;
}


그렇다면 컴파일 하는 동안 무슨 일이 벌어 진 것인가.
그것은 이것과 같다.

// 실제로 클래스 내부에 생성된 디폴트 복사 생성자
	AAA( AAA& a )
	{
		n = a.n;
	}


그렇다 복사 생성자라는 것이 자동으로 생성되어
b.n에 a.n을 넣어 준 것이다.
여기 까지 이해 하고 나면 이제 디폴트 복사 생성자에 대한 문제를 알아 보아야 한다.
디폴트 복사 생성자. 무엇이 문제인가.
일단 가장 첫번째,
디폴트 생성자는 생성자가 클래스 내부에 1개라도 선언되어 있으면 생성되지 않는다고 하였지만,
디폴트 복사 생성자는 그렇지 않다는 것이다.
그 어떤 상황에서도
AAA b(a);
같은 상황으로 선언하면 그 클래스에 생성자가 존재 하던 그렇지 않던(같은 복사 생성자는 제외)
디폴트 복사 생성자를 만든다는 것이다.
그렇다면 이게 어떤 문제를 일으 킬 수 있는가.

#include <iostream>
#include <string.h>

class AAA
{
	char* ch;
public:
	AAA(char* ch)
	{
		// 생성자에서 동적 할당으로 문자열을 삽입
		this->ch = new char[strlen(ch)+1];
		strcpy( this->ch, ch );
	}
	~AAA()
	{
		// 소멸자에서 메모리 해제
		delete [] ch;
	}
	void Showstr()
	{
		std::cout << ch << std::endl;
	}
};

void main()
{
	// a 객체를 생성
	AAA a("test");
	// 문제 없다.
	a.Showstr();

	// 문제는 바로 이것이다.
	AAA b(a);
	b.Showstr();
	// 프로그램을 실행하면 런타임 에러가 등장.
}

 


왜 이런 문제가 발생 하는가,
그것에 대해선,
디폴트 복사 생성자때문이다. 라고 말할 수 있는데,
실제로 컴파일러가 자동으로 만들어 주는 디폴트 복사 생성자에 대해서
그대로 타이핑 해보면 다음과 같다.

	AAA( AAA& a )
	{
		// 단순히 this->ch에 a.ch를 삽입한것 뿐이다.
		// 소멸자에서 ch를 delete할때 무슨 문제가 발생할지 감이 오는가?
		this->ch = a.ch;
	}



흔히 얕은 복사라 하여,
같은 메모리를 2곳에서 쓰다 보니
소멸자에서 첫번째 메모리 삭제 이후
다시 똑같은 곳을 삭제해 주고 있다.
당연히 문제가 생길 수 밖에 없는 것!
그래서 복사 생성자에서 깊은 복사를 할 수 있도록 꼭 추가해 주어야 한다.
(사실 동적 할당이 아니라면 디폴트 복사 생성자를 써도 큰 문제가 되질 않는다.)

	AAA( AAA& a )
	{
		// 이렇게 해주면 된다. 참 간단.
		this->ch = new char[strlen(a.ch)+1];
		strcpy( this->ch, a.ch );
	}



사실 별것 아닌데 마치 거대한 위기가 온듯 설명 했지만,
알고 있는것과 모르는 것의 차이는 크기에,
그렇다면
AAA b(a);
선언만 하지 않으면 되는것 아닌가 라고 생각 할 수도 있지만,
그건 또 아니다.
디폴트 복사 생성자가 생성되는 경우는 3가지가 있는데,
지금 처럼 기존의 생성된 객체로 새로운 객체를 초기화 하는 경우가 첫번째,

두번째로는,
함수 호출 시에 객체를 값에 전달 하는 경우( call by value )이다.

// call by value
void fun( AAA a )
{
	a.Showstr();
}

void main()
{
	// a 객체를 생성
	AAA a("test");
	// 문제 없다.
	a.Showstr();

	// fun(a)를 호출 할때 복사 생성자가 발동된다.
	// 마찬가지로 깊은 복사를 하지 않는다면, 
	// 런타임 에러가 발생한다.
	fun(a);

	// 문제는 바로 이것이다.
	AAA b(a);
	b.Showstr();
	// 프로그램을 실행하면 런타임 에러가 등장.
}



그리고 마지막 세번째는 함수의 리턴에 의한 복사 생성자의 호출 이다.
소스를 보자.

 

// AAA를 리턴 하는 fun
AAA fun()
{
	AAA a("test12");
	return a;
}

void main()
{
	// a 객체를 생성
	AAA a("test");
	// 문제 없다.
	a.Showstr();

	// 마찬가지다. return을 하기 위해선
	// 메모리에 return할 값을 생성하고,
	// 지역 변수인 AAA a("test12")를 지우게 된다.
	// 하지만 얕은 복사이기 때문에
	// return으로 돌아온 a에는 이미 값은 존재 하지 않고
	// return이 종료 되고 돌아온 a를 지우면
	// 역시 런타임 에러가 난다.
	fun();
}
 
    ​



참 어렵다. 하지만 메모리에 구조를 좀 파악했다면,
그렇게 까지 힘든 내용은 아니였던 것 같다.

'프로그래밍 > C++' 카테고리의 다른 글

static  (0) 2010.03.24
멤버 이니셜 라이저(member initializer)  (0) 2010.03.23
friend  (0) 2010.03.21
this 포인터  (0) 2010.03.21
객체 포인터 배열  (0) 2010.03.21