부끄러움 중에서 가장 나쁜것은 검약과 빈곤을 부끄러워하는 것이다. -리바우스
* 우선 C++ : The Core Language (C 프로그래머를 위한 C++, 한빛미디어)라는 서적을 아주 많이 참고했음을 밝힌다. 9장을 참조해라. 정말 잘 설명해놓았다. ^_^a
- 위 책의 링크 :
여기를 클릭하세요.
- 참고 문서 링크 :
14CopyConstructors.pdf (14.44 KB)
- 원본과는 다르게 제 주관적으로 내용을 많이 고쳤음을 알립니다.
1 시작하며 #
우선 클래스나 생성자/소멸자같은 기본개념과 연산자 오버로딩등을 알고있다고 간주하겠다. 이런 원론적인 내용을 설명하는 것은 무지무지 어려운 까닭에 서적이나 다른 사이트의 내용을 많이 참조했음을 밝힌다. STL 및 C++의 사용에 조금이나마 도움되었으면 좋겠다.
2 개요 #
컴파일러가 자동적으로 생성할 수 있는 생성자는 두가지의 형태가 있다. 만약 프로그래머가 기본 생성자를 정의하지 않았다면 컴파일러는 기본 생성자를 자동으로 제공한다는 것을 상기하자. 이 기본 생성자는 객체를 초기화할 목적으로 각각의 데이타 맴버들의 기본 생성자들을 호출하게 된다. 만일 이 기본생성자를 덮어쓰고자(override) 한다면, 간단하게 하나를 작성하여 제공해야 할 것이다. 컴파일러가 자동으로 만들어주는 생성자의 또다른 타입은 복사 생성자이다. 이것은 이미 존재하는 인스턴스로 부터 객체를 복사하면서 생성하고자 할때 호출된다. 문자열을 구현한 클래스를 하나 가정하고, 이것을 MyString이라고 부르기로 가정하자. 데이타 맴버는 문자열의 길이와 문자열안의 문자들을 담을 포인터가 있다고 정해보도록 한다.
class MyString
{
public:
MyString(const char* s = "");
~MyString(void);
...
private:
int length;
char* str;
};
이 예제에서, 우리는 MyString 생성자가 문자들의 메모리 공간을 할당할 것이고, 소멸자는 메모리를 해제한다고 가정할 것이다. 복사생성자는 MyString 객체의 간단한 초기화를 실행할때 호출될 것이다:
MyString me("Jerry");
MyString clone = me; // 복사생성자가 실행된다.
이 예제를 보면, MyString을 마치 문자열 클래스처럼 사용하는 것을 볼 수 있다. STL의 string 클래스와는 혼동을 피하기위해서 별도의 클래스명을 사용하였음을 밝힌다. 더 중요한 점이라면, 복사생성자는 값으로 객체를 인자로 넘길 때와 값으로 객체를 반환할 때 호출된다. 예를 들자면 화일을 여는 함수를 다음과 같이 표현할 수 있다:
void OpenFile(MyString filename)
{
// 객체를 C 문자열로 변환하고, 화일 핸들을 여는등의 작업을 한다.
}
문자열을 정의하고 OpenFile 함수를 다음과 같이 호출할 수 있을 것이다:
MyString name("flights.txt");
OpenFile(name);
name 객체를 인자로 넘길때, 복사생성자가 호출된 함수로부터 OpenFile 함수안의 지역 인자변수로 MyString 객체를 복사하기위해 호출된다. 여기서는 복사생성자를 선언하지 않았기 때문에 기본 복사생성자가 호출된다.
3 기본 복사 생성자 #
기본 복사생성자는 객체의 전체 맴버에 걸친 복사를 수행한다. 여기서 예를 든 MyString 클래스에서의 복사생성자는 문자열 길이를 나타내는 length와 C 문자열 포인터 변수인 str을 복사할 것이다. (문자열의 내용이 아닌 문자열의 포인터가 복사됨에 주의!) 어쨌거나, 각각의 문자들은 복사되지 않는다. 이런 것을 가리켜서 shallow copy라고 부른다. (왜냐하면, 클래스상에서 한단계 레벨까지 밖에 복사가 이루어지지 않기 때문에 붙여진 이름이다.) 메모리 내부를 그림으로 그려본다면 다음과 같이 나타낼 수 있을 것이다:

이것은 문제를 일으킬 수 있다. 왜냐하면 지금 문자들이 두개의 인스턴스에 의해 공유되고 있기 때문이다. 만약 OpenFile함수인자인 filename 객체내부에서 문자열의 내용을 변경한다면, 인자로 넘기기 이전의 변수인 name의 내용도 변경될 것이다. 좀더 안좋은 상황이라면, OpenFile 함수가 종료하고 filename 객체가 소멸할때 내부 맴버인 str의 메모리를 해제한다면, 함수가 반환된 후인 name에서는 유효하지 않은 메모리를 참조하는 결과가 발생하게 된다! (정말로 겁나는 일이다. Access Violation 에러가 바로 뜨게 된다.)
4 우리가 원하는 것 : Deep Copy #
이런 형태의 잠재적인 재앙을 피하는 방법은 MyString 객체를 deep copy하도록 만드는 것이다. 우리는 length 변수의 복사와 더불어, str 변수의 전체 문자열을 복사해야한다. (단지 포인터만 복사하는 것이 아니어야 한다.) 이런 복사를 deep copy라고 하며, 이것을 그림으로 그려보면 다음과 같다:

자, 이렇게 하면 원본인 name 변수에 영향을 주지 않으면서 filename 객체의 내용을 관리할 수 있음을 알 수 있다. 게다가 이렇게 하면 OpenFile함수가 반환되어서 filename 객체내의 문자열 배열이 메모리 해제되더라도 원본인 name내부의 str 맴버 문자열은 굳건하게 남아있게 된다.
5 복사 생성자를 선언해보자! #
MyString객체에 deep copy를 구현하려면, 별도의 복사생성자를 선언할 필요가 있다. 복사생성자는 다음과 같은 특징이 있다.
- 반환값이 없다. (생성자니깐..^^)
- 자기자신을 타입으로 가지는 상수 레퍼런스를 인자로 갖는다.
class MyString
{
public:
MyString(const char* s = ““);
MyString(const MyString& s);
...
};
복사생성자 내부코드도 내친김에 보도록 하자:
MyString::MyString(const MyString& s)
{
length = s.length;
str = new char[length + 1];
strcpy(str, s.str);
}
어떤 클래스라도 deep copy에 대한 기능을 제공하려면 그에 맞는 복사생성자를 제공해야한다. 만일 제작하려는 클래스의 모든 맴버 변수들이 내장형 기본 타입으로만 이루어져있다면 (다시말하면, 포인터 변수나 별도 정의된 클래스 인스턴스가 아닐 경우), 복사생성자의 작성을 생략하고, 기본 복사생성자를 사용하게 해도 좋다.
6 한계 #
복사생성자는 객체간 대입이 실행될 때에는 호출되지 않는다. 예를 들어 다음과 같은 코드를 작성했다면, 아직까지도 shallow copy가 실행됨을 알 수 있을 것이다:
MyString betty("Betty Rubble"); // 문자열을 "Betty Rubble"로 초기화 한다.
MyString bettyClone; // 빈 문자열로 초기화한다.
bettyClone = betty;
이것은 앞서 제작된 복사생성자 대신에 대입 연산자가 호출되기 때문이다. 기본적으로 대입연산자는 객체의 맴버전반에 걸친 shallow copy를 수행한다. 어쨌거나 C++은 이 복사연산자를 덮어쓸 수 있다. (다시말하면, 중복정의할 수 있다. overriding.) 이것도 역시 곧 설명할 것이다.
7 값에 의한 인자전달을 방지해야 할 경우 #
복사생성자를 통한 클래스 객체의 복사가 일어나는 것을 막아야 하는 경우도 일어날 수 있을 것이다. 클래스내에 private 생성자로써 복사생성자를 선언하거나 그것을 아예 기술하지 않으면, 컴파일러로 하여금 작성된 클래스에 대한 객체가 값으로 전달되는 것을 방지하는 효과가 있다. (다시 말하면, deep copy가 일어나지 않는다.) 이것은 특정 내부 맴버를 공유하고 싶을 때가 보통인데, 게임 프로그래밍을 할 때, 거대한 비트맵이 하나 존재하고, 각각의 시작 포인터 좌표만을 가지고 있는 ShadowSprite라는 클래스가 있다고 가정하자. 선언하자면 다음과 같은 형태일 것이다.
class ShadowSprite {
public:
int x, y; // 스프라이트의 좌표
char *data; // 스프라이트에 해당하는 비트맵상에서의 시작 포인터
long height; // 스프라이트의 높이
};
위 클래스의 인스턴스는 아무리 여러개가 생긴다해도 data변수의 내용을 각 부분별로 나누어가지는 구조라고 가정해보자. 이런 경우라면, 소멸자에서 data를 free()만 하지 않는다면 복사생성자를 선언할 필요가 없다.
8 대입 #
클래스를 위한 대입연산자(=)를 재정의하는 것이 가능하다고 위에서 언급했었다. 대입연산자는 왼쪽 인자가 클래스 객체임을 나타내는 맴버 함수의 형태로써 정의되어야한다. 기본적으로, 대입연산자는 모든 클래스마다 정의되어있고, 맴버전반에 걸친 shallow copy만을 수행하도록 지정되어있다. (즉, 포인터 맴버 변수를 포함하는 클래스는 재정의할 필요가 있다.) 정반대로 객체간의 deep copy를 방지하려면 앞서 설명한 복사생성자와 마찬가지로 private으로 정의하거나 아예 정의하지 않으면 된다.
대입과 복사생성간의 핵심적인 차이점이라면 대입은 내장된 리소스의 소멸을 일으킬 가능성이 있다는 것이다. 메모리나 다른 리소스가 고립되지 않는다는 것을 보증해야할 경우라면 대입연산자를 오버로딩해야만한다. (다시 말하면, malloc()한 포인터 변수를 포함한 객체를 백업하지 않고 그냥 소멸시킬 경우가 대표적인 예이다.) 대입연산자 메소드의 문법은 다소 지저분하므로, 이것을 기본 형태로 놓고 고쳐서 제작하는 것이 좋다. (약간이라도 잘못 기입하면 원하지 않는 역할로써 동작하기 쉽기 때문이다.)
const MyString& operator=(const MyString& rhs)
{
if (this != &rhs) { // 오른쪽의 인자가 자기자신이 아닐 경우만 진행한다.
delete[] this->str; // 필요없는 메모리를 반납한다.
this->str = new char[strlen(rhs.str) + 1]; // 새로운 메모리를 할당한다.
strcpy(this->str, rhs.str); // 문자들을 복사한다.
this->length = rhs.length; // 길이도 복사한다.
}
return *this; // 자기자신의 레퍼런스를 반환한다. 이제 따로 떨어진 대입결과가 동작할 것이다.
}
대입 연산자를 바라보는 좋은 방법은 다음과 같다 : 대입은 금방 재생성한 것에 뒤따라 발생하는 소멸(destruction)이다. 즉, 다시 말하면, 맴버변수에 대한 일단 소멸자를 동작시킨 후, 다시 생성자를 동작한 후 진짜로 데이타를 복사해넣는다. 위 소스에서는 delete[] 메서드를 str 맴버변수에 사용함으로써 복사되는 MyString 객체의 str 변수를 초기화하는 것을 볼 수 있다. 그 다음 3 라인은 str의 내용을 생성하고 채우는 처리를 수행하고 있는 것을 볼 수 있다.
- 주의점 #1 : this != &rhs 조건문은 자기자신을 자기자신에게 대입하고 있는지 여부를 검사하고, 자기자신일 경우면 소멸->재생성 처리를 하지 않는다. name = name이라고 기술하는 것은 전적으로 적법한 문장이다. 그러나 이럴때에도 내부 맴버를 소멸시키고 다시 재생성하는 것은 전혀 이득없는 일이다.
- 주의점 #2 : 반환값의 타입이 const MyString&이고 반환값은 *this으로 지정해야한다. 이것은 오른쪽에서 왼쪽으로 대입이 수행될 때 int나 double과 같은 값이 올 때에는 에러가 발생해야 하기 때문이다. (대입연산자는 여러 타입에 대해서 정의할 수 있다는 것을 알아두자.)
MyString heroine("ginger"); MyString cluck("babs"); MyString quack("rhodes"); MyString meanie("mrs. tweety"); meanie = quack = cluck = heroine; cout << heroine << cluck << quack << meanie << endl; // 아마도 이렇게 출력될 것이다 : gingergingergingerginger
9 끝내며 #
복사생성자는 확실히 쉬운 부분은 아니다. C++을 많이 사용하는 프로그래머도 자주 그 특성을 잊기 쉬운 문법이라고 생각한다. 요약해본다면 복사생성자는 완전히 메모리가 분리된 새로운 객체를 생성해낸다. 대입연산자는 객체내에 존재하는 데이타를 다루어야만 한다. 간단한 클래스에서는 동일하게 동작하는 것처럼 보일 수도 있다. 그러나 좀더 복잡한 클래스(예를 들어 내부에 다른 객체의 포인터를 가지고 있는 경우)에서는 좀더 많은 작업을 해주어야 한다는 것을 알아두자. 약간은 허접하게 번역한 것도 있고 토단것도 많지만 C++을 공부하는 사람들에게 도움되길 빈다. 특히 이글을 읽는 노동부분들은 조금이나마 개념을 잡는데 도움이 되기를 바란다.








