ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 복사 생성자
    플밍/C++ (overview) 2012. 1. 3. 23:00
    2006/08/01 21:28



    시작에 앞서..

     

    기본자료형 변수의 초기화 방법은 다음과 같다.

     

    [C스타일]

     

    int a = 20;

     

    [C++스타일]

     

    int a(20);

     

     

     

    그렇다면 객체도?  No prob!

     

    [C스타일]

     

    Person p = 20;

     

    [C++스타일]

     

    Person p(20);

     

     

    * C++에서 코딩할때, C스타일로 초기화가 되어 있다면, 컴파일 과정에서 자동으로

       C++스타일로 "묵시적인 변환"을 시켜준다.

     

     

    ===========================================================

     

    복사 생성자의 형태

     

    class AAA{

           int a;

           int b;

    public:

           AAA(){

                  ....

           }

           AAA(const AAA& a){

                  ....

           }
    };

     

    int main(){

           AAA a1();

           AAA a2(a1);

           //같은 표현 : AAA a2 = a1;

    }

     

    코드 중 굵게 표현된 부분이 복사 생성자의 형태이다.

    즉, 자신과 같은 형태(자료형)의 객체를 인자로 받을수 있는 생성자를 의미한다.

     

    (const는 붙여도 되고, 안 붙여도 되지만 붙이는게 좋다.

     그렇지만 &(레퍼런스) 선언은 꼭 해주어야 한다. 없다면 무한루프에 빠져버린다.

     다행히 대부분의 컴파일러는 &선언을 해주지 않으면 컴파일 오류를 발생시켜 준다.)

     

    -> 이 부분에 대해 좀더 자세히 알아보자... ▽

     

    객체가 인수로 전달될 때

    다음과 같이 함수의 인수로 객체를 넘기는 경우에도 복사 생성자가 호출된다. 

     

    void PrintAbout(Person AnyBody)

    {

         AnyBody.OutPerson();

    }

     

    void main()

    {

         Person Boy("강감찬",22);

         PrintAbout(Boy);

    }

     

    ★중요★

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

    복사 생성자가 정의되어 있으면 AnyBody가 Boy를 깊은 복사하므로 아무런 문제가 없다. 객체가 인수로 전달될 때 뿐만 아니라 리턴값으로 돌려질 때도 복사 생성자가 호출된다. 함수의 인수로 사용되거나 리턴값으로 사용되는 객체는 반드시 복사 생성자를 제대로 정의해야 한다.

     

    * call-by-value 과정

        첫째. 매개 변수를 위한 메모리 공간 할당

        둘째. 전달 인자 값의 복사

     

     

    복사 생성자의 인수

    복사 생성자의 인수는 반드시 객체의 레퍼런스여야 하며 객체를 인수로 취할 수는 없다. 만약 다음과 같이 Person형의 객체를 인수로 받아들인다고 해 보자.

     

    Person(const Person Other)

    {

         Name=new char[strlen(Other.Name)+1];

         strcpy(Name,Other.Name);

         Age=Other.Age;

    }

     

    복사 생성자 자신도 함수이므로 실인수를 전달할 때 값의 복사가 발생할 것이다.

    (함수 내에서 생성된 형식 인자 Other에 실제 인자를 대입하는 과정.)

    객체 자체를 인수로 전달하면 복사 생성자로 인수를 넘기는 과정에서 다시 복사 생성자가 호출될 것이고 이 복사 생성자는 인수를 받기 위해 또 다시 복사 생성자를 호출한다. 결국 자기가 자신을 종료조건없이 호출해대는 무한 재귀 호출이 발생할 것이며 컴파일러는 이런 상황을 방관하지 않고 에러로 처리한다.

     

    * 걍 읽어보기 *

     

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

     

    Person(const Person *Other) {

         Name=new char[strlen(Other->Name)+1];

         strcpy(Name,Other->Name);

         Age=Other->Age;

    }

     

    Other의 타입이 Person *로 바뀌었고 본체에서 Other의 멤버를 참조할 때 . 연산자 대신 -> 연산자를 사용하면 된다. 그러나 이렇게 하면 Person Young=Boy; 선언문이 암시적으로 호출하는 생성자인 Person(Boy)와 원형이 맞지 않다. 사실 포인터를 취하는 생성자는 복사 생성자로 인정되지도 않는다. 꼭 포인터로 객체를 복사하려면 main의 객체 선언문이 Person Young=&Boy;가 되어야 하는데 그래야 Person 복사 생성자로 Boy의 번지가 전달된다. main 함수까지 같이 수정하면 정상적으로 잘 동작한다.

    그러나 이는 일반적인 변수 선언문과 형식이 일치하지 않는다. 기본 타입의 복사 생성문을 보면 int i=j; 라고 하지 int i=&j;라고 선언하지는 않는다. 즉 포인터를 통한 객체 복사 구문은 C 프로그래머가 알고 있는 상식적인 변수 선언문과는 틀리다. 클래스가 기본형과 완전히 같은 자격의 타입이 되려면 int i=j; 식으로 선언할 수 있어야 한다.

    그래서 객체 이름에 대해 자동으로 &를 붙이고 함수 내부에서는 전달받은 포인터에 암시적으로 *연산자를 적용하는 레퍼런스라는 것이 필요해졌다. 복사 생성자가 객체의 레퍼런스를 받으면 Young=Boy라고 써도 실제로는 포인터인 &Boy가 전달되어 속도 저하나 무한 호출없이 기본 타입과 똑같은 형식의 선언이 가능하다. 이후 공부하게 될 연산자 오버로딩에도 똑같은 이유로 레퍼런스가 활용된다. C에서는 꼭 필요치 않았던 레퍼런스라는 개념이 C++에서는 필요해진 이유가 객체의 선언문, 연산문을 기본 타입과 완전히 일치시키기 위해서이다.

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

     

    ===========================================================

     

    디폴트 복사 생성자

     

    생성자, 소멸자와 마찬가지로 복사생성자도 사용자가 따로 정의해주지 않으면 자동으로

    컴파일시에 삽입된다.

     

    다만, 디폴트 생성자와 소멸자는 그냥 껍데기만 있고 하는일은 없지만, 디폴트 복사생성자는

    하는일이 있다. 멤버 변수 대 멤버 변수의 복사를 한다.

     

    그런데, 여기서! 주의할점이 있다.

     

    class Person{

           char* name;

    public :

           Person(char* _name){

                  name = new char[strlen(_name)+1];

                  strcpy(name, _name);

           }

           ~Person(){

                  delete[] name;

           }

    };

     

    int main(){

           Person a("CHA");

           Person b(a);

    }

     

    이 코드는 런타임 오류를 발생시킨다. 왜?

     

    지금은 복사생성자를 따로 안 만들어줬기 때문에 디폴트 복사생성자가 들어갔을것이다.

    디폴트 복사생성자 내에서는 b의 name에다가 a의 name을 그대로 복사했을것이다.

    그런데, name은 포인터이고 그렇다면, a와 b의 name은 같은 주소값을 가지고 있을것이다.

    복사 생성자로서의 역할은 훌륭히 수행해 낸것이다.

    그렇다면 왜 에러가?

    에러는 객체가 소멸할때 발생한다. 객체는 b부터 먼저 소멸된다. b의 소멸자에서 name이

    가리키는 공간을 먼저 해제한다. 그리고 객체 a가 소멸될 차례인데, a의 소멸자에서 name이

    가리키는 공간을 해제하려고 봤더니, 이미 b가 해제해버린것이 아닌가!!

    이렇게 이미 해제된 공간을 다시 해제하려고 하기 때문에 에러가 생기는 것이다.

     

    이렇게 멤버변수가 어떤 형태이든 간에(int든 pointer든 객체든)

    무조건 "=" 연산을 해버리는 방식을 두고 얕은 복사(shallow copy) 라고 한다.

     

    그리고 위에서 생긴 에러는 얕은 복사의 문제점을 잘 나타내 준다.

     

    그렇다면, 이 에러를 어떻게 처리할것인가?

     

    답은 깊은 복사(deep copy)이다.

     

    class Person{

           char* name;

    public :

           Person(char* _name){

                  name = new char[strlen(_name)+1];

                  strcpy(name, _name);

           }

           Person(const Person& p){

                  name = new char[strlen(p.name)+1];

                  strcpy(name, p.name);

           }

           ~Person(){

                  delete[] name;

           }

    };

     

    int main(){

           Person a("CHA");

           Person b(a);

    }

     

    최종적으로 정리를 하자면, 생성자 내에서 동적할당을 했을때는 반드시 소멸자에서 해제를

     

    해줘야 하고, 복사 생성자(deep copy를 하는..) 역시 정의해줘야 한다.

     

    ==========================================================

     

    복사 생성자가 호출 되는 때

     

    첫째. 다른 객체로 어떤 객체를 초기화 할때.

     

    Point a1;

    Point a2 = a1;

     

    a2 = a1; (X)    이 경우는 복사생성자가 아닌, 대입연산자가 호출된다.

     

    둘째. 객체를 함수의 인자로 받는 경우.

     

    셋째. 함수 내에서 객체를 값에 의해 리턴하는 경우.

     

    셋째경우를 살펴보자.

     

    class AAA{ 
           int val; 
    public :
           AAA(int i){
                  val = i;
           }
           AAA(const AAA& a){
                  cout << "AAA(const AAA& a) called!" <<endl;
                  val = a.val;
           }
    };

     

    AAA function(void){
           AAA a(10);
           return a;
    }

     

    int main(){
           function(); 
           return 0;
    }

     

    --------------------------------------

    [결과]

    AAA(const AAA& a) called!

    --------------------------------------

     

    main함수 내에서는 function()함수의 리턴값을 받아주는 변수가 없다.

    하지만, 결과 값은 제대로 출력이 된다. 즉, 함수의 리턴값은 그 함수를 호출한 영역으로

    자동으로 복사되어 넘겨진다.

     

    여기서는 객체를 리턴값으로 취하고 있는데, 메인으로 값이 넘겨지는 과정을 보자면,

    우선 function()함수 내에서 AAA타입의 a라는 객체가 만들어지고, 바로 a를 리턴한다.

    주의할것은, a 자체가 리턴되는것이 아니다!

    바로 a의 복사본이 넘겨지는 것이다. (a는 함수가 끝남과 동시에 사라진다.)

    객체의 본사본이므로 객체의 생성과정과 마찬가지로, 객체를 위한 메모리 공간이 할당되고,

    복사생성자에 a를 parameter로 받아서 a의 내용을 복사한 a의 복사본이 만들어진다.

Designed by Tistory.