관리 메뉴

Kim's Programming

C++ - 클래스 생성자/파괴자(2/3) 본문

Programming/Cplusplus

C++ - 클래스 생성자/파괴자(2/3)

Programmer. 2015. 8. 31. 02:53

복사 생성자


복사 생성자는 지금까지의 것들 보다 난이도가 있습니다. 변수를 선언할 때 = 구분자 다음에 상수로 초기값을 지정할 수 있으며 이미 생성되어 있는 같은 타입의 다른 변수로도 초기화할 수 있습니다. 다음은 가장 간단한 타입인 정수형의 예입니다.

1
2
int a=4;
int b=a;
cs

정수형 변수 a는 선언됨과 동시에 3으로 초기화되었습니다. 그리고 동일한 타입의 정수형 변수 b는 선언과 동시에 a로 초기화 되었습니다. 결국 두 변수는 모두 3의 값을 가지게 될 것입니다. 이런 초기화는 실수형이나 문자형, 구조체 등에 대해서도 똑같이 허용됩니다. 클래스가 int와 동일한 자격을 가지는 타입이 되기 위해서는 이미 생성되어 있는 같은 타입의 객체로부터 초기화될 수 있어야 합니다. 객체에 대해서도 이런 초기화가 성립하는지 앞에서 쓴 소스에 다음 코드를 작성해보겠습니다.

1
2
3
4
5
6
7
void main()
{
    Position Posit{ 3010'A' };
    Position Posit_1 = Posit;
    Posit_1.OutPosition();
}
 
cs

Posit 객체가 먼져 30,10위치의 문자 'A'를 출력하도록 초기화 되었으며 Posit_1객체는 선언과 동시에 Posit 객체로 초기화되었습니다. 이때 멤버별 복사에 의해 Posit_1은 Posit의 모든 멤버값을 그대로 복사받으며 두 객체는 완전히 동일한 값을 가지게 됩니다. Position 객체가 내부에 모든 정보를 포함하고 있기 떄문에 이런 초기화는 전혀 문제가 있습니다. 그렇다면 모든 객체에 대해 이런 초기화가 가능할 까요? 앞에서 썼던 코드를 조금 수정하여 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string.h>
using namespace std;
class Person
{
private:
    char *Name;
    int Age;
 
public:
    Person(const char *aName, int aAge)
    {
        Name = new char[strlen(aName) + 1];
        strcpy(Name, aName);
        Age = aAge;
        cout << Name << " 객채 생성자 호출! " << endl;
    }
    ~Person()
    {
        cout << Name << " 겍채 파괴! " << endl;
        delete[] Name;
    }
    void OutPerson()
    {
        cout << " 이름 : " << Name << " 나이 : " << Age << endl;
    }
};
void main()
{
    Person per("홍길동"45);
    Person Per_1 = per;
    Per_1.OutPerson();
}
 
cs

이 코드는 정상적으로 컴파일 및 실행은 가능하지만 종료할 때 파괴자에서 실행중 에러가 발생합니다. 왜 그럴까요? Per_1객체가 per객체로 초기화될 때 멤버별 복사가 발생하며 Per_1의 Name 멤버가 per의 Name과 동일한 번지를 가리키고 있습니다. 정수형의 Age끼리 값이 복사되는 것은 아무 문제가 없지만 포인터끼리의 복사는 문제가 됩니다.


이런 상태에서 Per_1.Outperson이나 per.Outperson 함수 호출은 정상적으로 실행됩니다. 그러나 두 객체가 같은 메모리를 공유하고 있기 떄문에 한쪽에서 Name을 변경하면 다른 쪽도 영향을 받게 되어 서로 독립적이지 못합니다. 이 객체들이 파괴될 때 문제가 발생하는데 각 객체의 파괴자가 Name 번지를 따로 해제하기 떄문입니다. new는 per의 생성자에서 한 번만 했고 delete는 각 객체의 파괴자에서 두 번 샐행하였기 떄문에 이미 해제된 메모리를 다시 해제하려고 시도하기에 실행중 에러가 됩니다. 정수형으로도 한번 살펴보겠습니다.

1
2
3
in a=2;
int b=a;
b=5;
cs

b가 생성될 때 a의 값으로 초기화되어 a와 b는 같은 값을 가집니다. 하지만 이는 어디까지나 초기화될 때 잠깐 같을 뿐이지 두 변수는 이후 완전히 독립적으로 동작합니다. 이후에 b에 5를 대입한다고 한들 a는 영향을 받지 않으며 이 뒤에 a에 또 다른 값을 넣게 되더라도 b는 영향을 받지 않습니다. 정수형의 복사 생성이 이처럼 독립적이므로 사용자 정의형도 이와 똑같이 복사 생성을 할 수 있어야합니다.


Person per_1 = Per;선언문에 의해 per_1은 Per의 멤버값을 복사받지만 이 때의 복사는 포인터를 그대로 복사하는 얕은 복사입니다. 따라서 Per은 per_1과 일시적으로 같은 값을 가지지만 Person의 Name을 빌려서 정보를 표현하는 불완전한 객체이며 독립적이지 못합니다. 이 문제를 해결하려면 초기화할 때 얕은 복사를 해서는 안 되며 깊은 복사를 해야 하는데 이때 복사생성자가 필요합니다. 얕은 복사가 문제의 원인이었으므로 깊은 복사를 하는 복사 생성자를 만들어 해결할 수 있습니다. 다음 예제는 위 소스에서 복사 생성자를 추가한 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string.h>
using namespace std;
class Person
{
private:
    char *Name;
    int Age;
 
public:
    Person(const char *aName, int aAge)
    {
        Name = new char[strlen(aName) + 1];
        strcpy(Name, aName);
        Age = aAge;
        cout << Name << " 객채 생성자 호출! " << endl;
    }
    Person(const Person &Other)
    {
        Name = new char[strlen(Other.Name) + 1];
        strcpy(Name, Other.Name);
        Age = Other.Age;
        cout << Name << " 객채 생성자 호출! " << endl;
    }
    ~Person()
    {
        cout << Name << " 겍채 파괴! " << endl;
        delete[] Name;
    }
    void OutPerson()
    {
        cout << " 이름 : " << Name << " 나이 : " << Age << endl;
    }
};
void main()
{
    Person per("홍길동"45);
    Person Per_1 = per;
    Per_1.OutPerson();
}
 
cs

복사 생성자는 자신과 같은 타입의 다른 객체에 대한 레퍼런스를 전달받아 이 레퍼런스로 부터 자신을 초기화합니다. Person 복사 생성자는 동일한 타입의 Other를 인수로 받아서 자신의 Name에 Other.Name의 길이만큼 버퍼를 새로 할당하여 복사합니다. 새로 메모리를 할당해서 내용을 복사하였으므로 이 메모리는 완전한 자신의 것이며 안전하게 따로 관리할 수도 있습니다. Age는 단순 변수이므로 값만 대입받으면 됩니다. 컴파일러는 Person Per_1=per 구문을 Person Per_1=Person(per);으로 해석하는데 이 원형에 맞는 생성자인 복사 생성자를 호출합니다. 실인수 per이 Person객체이므로 Person을 인수로 받아들이는 생성자 함수를 호출할 것입니다. 복사생성자에 의해 Per_1은 깊은 복사를 하여 메모리에 완전한 사본을 복사합니다.


이제 Per_1과 per는 타입만 같을 뿐 완전히 다른 객체이고 메모리도 따로 소유하므로 각자의 Name을 마음대로 바꿀 수 있고 파괴자에서 메모리를 해제해도 문제가 없습니다. 복사 생성자에 의해 두 객체가 완전한 독립성을 얻은 것입니다. 복사 생성자의 임무는 새로 생성되는 객체가 원본과 똑같으면서 완전한 독립성을 가지도록 하는 것입니다. 만약 객체가 데이터베이스를 사용한다면 이 클래스의 복사 생성자는 새 객체를 위한 별도의 데이터베이스 연결을 해야하며 독점적인 자원을 필요로 한다면 마찬가지로 별도의 자원을 할당해야합니다. 그래야 Class A=B; 선언문에 의해 A가 B에 대해 독립적으로 초기화됩니다.


::객체가 인수로 전달될 때


같은 종류의 다른 객체로 새 객체를 선언하는 경우는 그리 흔하지 않습니다. 하지만 다음과 같이 함수의 인수로 객체를 넘기는 경우는 아주 흔한데 이때도 복사 생성자가 호출됩니다.

1
2
3
4
5
6
7
8
9
10
void PrintAbout(Person AnyBoy)
{
    AnyBody.OutPerson();
}
 
void main()
{
    Person Per("홍길동",24);
    PrintAbout(Per);
}
cs

함수 호출 과정에서 형식 인수가 실인수로 전달되는 것은 일종의 복사생성입니다. 함수 내부에서 새로 생성되는 AnyBody가 실인수 Per을 대입받으면서 초기화되는데 이때 복사 생성자가 없다면 AnyBody가 Per을 얕은 복사하면 두 객체가 동적 버퍼를 공유하는 상황이 됩니다. AnyBody는 지역변수이므로 PrintAbout 함수가 리턴될 때 AnyBody의 파괴자가 호출되고 이떄 동적 할당된 메모리가 해제됩니다. 이후 Per이 메모리를 정리할 때는 이미 해제된 메모리를 참조하고 있으므로 반드시 에러가 발생할 것입니다.


복사 생성자가 정의되어 있으면 AnyBody가 Per을 깊은 복사하므로 아무런 문제가 없습니다. 객체가 인수로 전달될 뿐만 아니라 리턴값으로 돌려질 때도 복사 생성자가 호출됩니다. 위 테스트 코드를 위에 있는 코드에 대입하면 정상적으로 실행합니다. 하지만 복사 생성자를 주석으로 묶어버리면 다운됩니다. 함수의 인수로 사용되거나 리턴값으로 사용되는 객체는 반드시 복사 생성자를 제대로 정의해야합니다.


::복사 생성자의 인수


복사 생성자의 인수는 반드시 객체의 레퍼런스여야 하며 객체를 인수로 취할 수는 없습니다. 만약 다음과 같이 Person 형의 객체를 인수로 받아들인다고 해보겠습니다. 객체 자체를 인수로 전달하면 복사 생성자로 인수를 넘기는 과정에서 다시 복사 생성자가 호출될 것이고 이 복사 생성자는 인수를 받기 위해 또 복사 생성자를 호출합니다. 결국 자기가 자신을 종료조건 없이 호출해대는 무한재귀호출이 발생할 것이며 이런 상황을 방관하지 않는 컴파일러는 에러로 처리합니다.


이런 이유로 복사 생성자의 인수로 객체를 전달할 수는 없습니다. 그렇다면 포인터의 경우는 어떨까요? 포인터는 어디까지나 객체를 가르키는 번지값이므로 한 번만 복사되며 무한 호출되지 않습니다. 또한 객체가 아무리 거대해도 단 4바이트만 전달되므로 속도도 빠릅니다. 복사생성자가 객체의 포인터를 전달받도록 다음과 같이 수정해보겠습니다.

1
2
3
4
5
6
7
Person(const Person *Other)
{
    Name = new char[strlen(Other->Name) + 1];
    strcpy(Name, Other->Name);
    Age = Other->Age;
    cout << Name << " 객채 생성자 호출! " << endl;
}
cs

Other의 타입이 Person *로 바뀌었고 본체에서 Other의 멤버를 참조할 떄 (.)연산자 대신에 (->)연산자를 사용하면 됩니다. 그러나 이렇게 하면 person per=Per_1;선언문을 암시적으로 호출하는 생성자인 Person(per)과 원형이 맞지 않습니다. 사실 포인터를 취하는 생성자는 복사 생성자로 인정되지도 않습니다. 꼭 포인터를 이용하여 복사하려면 main의 객체 선어문이 Person per_1=&Per;가 되어야 하는데 그래야 Person 복사생성자로 Per의 번지가 전달됩니다. main 함수까지 같이 수정하면 정상적으로 잘 동작하게됩니다. 하지만 이는 일반적인 변수 선언문과 형식이 일치하지 않습니다. 기본타입의 복사 생성문을 보면 int i=j;라고 하지 int i=&j라고 선언하는 경우는 없습니다. 즉 포인터를 통한 객체 복사 구문은 C프로그래머가 알고 있는 상식적인 변수 선언문과는 다릅니다. 클래스가 기본형과 완전히 같은 자격의 타입이 되려면 int i=j;식으로 선언할 수 있어야합니다.


그래서 객체 이름에 대해 자동으로 &를 붙이고 함수 내부에서는 전달받은 포인터에 암시적으로 *연산자를 적용하는 레퍼런스라는것이 필요해졌습니다. 복사 생성자가 객체의 레퍼런스를 받으면 per_1=Per라고 써도 실제로는 포인터인 &Per이 전달되어 속도 저하나 무한 호출없이 기본 타입과 똑같은 형식의 선언이 가능합니다. C에서는 필요치 않았던 레퍼런스라는 것은 객체의 선언문, 연산문을 기본타입과 일치시키기 위하여 C++에서 필요해졌습니다.


복사생성자로 전달되는 인수는 상수일 수도 있고 아닐 수도 있는데 내부에서만 읽기만 하므로 개념적으로 상수 속성을 주는 것이 옳습니다. int i=j;연산후 j값이 그대로 유지되어야합니다. 결론짓자면 Class 클래스의 복사 생성자 원형은 Class(const Class &)여야 합니다.


::디폴트 복사 생성자


클래스가 복사 생성자를 정의하지 않으면 컴파일러가 디폴트 복사 생성자를 만듭니다. 컴파일러가 만드는 디폴트 복사 생성자는 멤버 끼리 1:1 복사함으로써 원본과 완전히 같은 사본을 만들기만 할 뿐 깊은 복사는 하지 않습니다. 만약 디폴트 복사 생성자만으로 충분하다면 굳이 복사 생성자를 따로 정의할 필요는 없습니다. 앞에서 사용했던 Position 클래스의 디폴트 복사 생성자는 다음과 같이 생성될 것입니다.

1
2
3
4
5
6
Position(const Position &Other)
{
    x=Other.x;
    y=Other.y;
    ch=Other.ch;
}
cs

대응되는 멤버 끼리 그대로 대입하는데 전부 단순 타입이라 대입만 하면 잘 복사가 됩니다. 이런 디폴트 복사 생성자가 있기 떄문에 별도의 조치가 없어도 앞에서 처럼 Position Posit_1 = Posit; 이라는 식의 대입이 잘 동작하는 것입니다.

 또한 Class A=B; 식의 선언을 하지 않거나 객체를 함수의 인수로 사용할 일이 전혀 없다는 것이 확실하다면 이때도 복사 생성자가 필요없습니다. 그러나 이런 가정은 무척위헙합니다. 왜냐하면 클래스의 사용자는 클래스가 일반 타입과 동등하기때문에 int, double에서 가능한 일들은 클래스에 대해서 모두 가능하다고 기대하며 실제로 그런 코드를 작성하기 떄문입니다. 이 기대에 부응하기 위해 클래스는 모든 면에서 기본 타입과 완전히 같아야합니다.


멤버 초기화 리스트


클래스가 타입이라면 int, double 같은 기본타입도 클래스인가 하는 질문을 할 수 있습니다. 이 질문의 답은 그렇다 입니다. C++은 일반적인 타입도 클래스와 동등하게 취급하며 클래스에 적용되는 문법이 일반 타입에 대해서도 적용됩니다. 그 예시로 정수형 변수를 선언하면서 초기화 하는 두 문장을 보겠습니다.

1
2
int a = 3;
int a(3);
cs

첫 번째 줄은 C에서 사용하던 전통적인 문법이고 두 번째 줄은 C++의 객체 초기화 문법입니다. C 컴파일러는 첫 번째 것만 인정하지만 C++ 컴파일러는 일관성을 위해 두 번째 줄 까지 인정합니다. int a(3);이라는 선언문은 int 클래스 타입의 객체 a를 선언하되 생성자 int(int aa)를 호출하는 문장으로 해석할 수 있습니다. 실제로 이 문장이 int 클래스의 생성자를 호출하는가 아니면 C방식대로 변수의 값만 초기화하는가는 컴파일러에 따라 다르지만 적어도 이론상으로 생성자를 호출한다고 해도 전혀 억지가 아닙니다.

객체 초기화의 임무를 띤 생성자가 하는 주된 일은 멤버 변수의 값을 초기화하는 것입니다. 그래서 생성자의 본체는 보통 전달받은 인수를 멤버 변수에 대입하는 대입문으로 구성됩니다. 멤버에 단순히 값을 대입하기만 하는 경우 본체에서 = 연산자를 쓰는 대신 초기화 리스트(Member Initialization List)라는 것을 사용할 수 있습니다. 초기화 리스트는 함수 선두와 본체 사이에 :를 찍고 멤버와 초기 값의 대응관계를 나열 하는 것입니다. Position 생성자를 초기화 리스트로 작성하면 다음과 같습니다.

1
2
3
4
Position(int ax, int ay, char ach) : x(ax),y(ay)ch(ach)
{
    //내용
}
cs

초기화 리스트 항목은 "멤버(인수)"의 형태를 띠며 멤버=인수 대입 동작을 합니다. 단순한 대입만 가능하며 복잡한 계산을 한다거나 함수를 호출하는 것은 불가능합니다. 위의 Position 생성자는 초기화 리스트의 지시대로 x는 ax로 y는 ay로, ch는 ach로 초기화합니다. 초기화 리스트에서 모든 멤버에 값을 대입했으므로 본체는 아무 것도 할 일이 없어졌는데 물론 더 필요한 초기화가 있다면 본체에 추가코드를 작성 할 수 있습니다. 생성자 본체에서 값을 직접 대입하는 것과 초기화 리스트의 효과는 동일하므로 둘 중 편한 방법을 사용하면 됩니다. 하지만 몇가지 경우에는 본체에 대입문을 쓸 수 없으므로 반드시 초기화 리스트로 멤버를 초기화해야합니다. 주로 대입 연산을 쓸 수 없는 특수한 멤버의 경우입니다.


::상수 멤버 초기화


상수를 선언할 때 반드시 초기화 해야합니다. Const Int Year=365;의 형식으로 상수를 선언하려는데 =365를 빼게되면 상수이기 떄문에 다시는 상수값을 정의할 수 없어서 에러로 처리됩니다. 단, 클래스 멤버일 때는 객체가 만들어질 때까지 초기화를 연기할 수 있으며 생성자의 초기화 리스트에서만 초기화가 가능합니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string.h>
using namespace std;
class Some
{
public:
    const int Value;
    Some(int i) :Value(i){}
    void OutValue()
    {
        cout << Value << endl;
    }
};
void main()
{
    Some S(5);
    S.OutValue();
}
cs

Some 클래스는 정수형 상수 Value를 멤버로 가지고 있는데 상수에 대해서는 대입 연산자를 사용할 수 없습니다. 상수의 정의에 의해 다음 코드는 당연히 불법입니다.

1
2
3
4
Som(int i)
{
    Value=i;
}
cs

Value 멤버는 상수이므로 값을 변경할 수 없으며 대입 연산 자체가 인정되지 않습니다. 그래서 초기화 리스트라는 특별한 문법이 필요합니다. 초기화 리스트는 본체 이전의 특별한 영역이며 생성자에서만 이 문법이 적용됩니다. 상수는 원래 선언할 때 초기값을 주어야 하나 클래스 정의문에 다음과 같이 초기값을 주는 것은 불가능합니다.

1
2
3
4
Som(int i)
{
    Value=i;
}
cs

클래스 선언문은 컴파일러에게 클래스가 어떤 모양을 하고 있다는 것을 알릴 뿐이지 실제 메모리를 할당하지는 않습니다. 그러므로 Value 멤버는 아직 메모리에 실존하지 않으며 존재하지도 않는 대상의 값을 초기화할 수는 없습니다. 상수는 객체가 생성될 떄 반드시 초기화되어야 하며 상수 멤버 초기화의 책임은 생성자에게 있습니다. 따라서 상수멤버를 가지는 클래스의 모든 생성자들은 상수 멤버에 대한 초기화 리스트를 가져야 합니다. 만약 이를 위반하면 에러 처리 됩니다.

'Programming > Cplusplus' 카테고리의 다른 글

C++ - 캡슐화(1/3)  (0) 2015.09.19
C++ - 클래스 생성자/파괴자(3/3)  (0) 2015.09.03
C++ - 클래스 생성자/파괴자(1/3)  (1) 2015.08.30
C++ - 파괴자(Destructor)  (0) 2015.08.30
C++ - 생성자(Constructor)  (0) 2015.08.29