[C++] 복사생성자

Vaughan·2022년 7월 27일
1

C++

목록 보기
3/6
post-thumbnail

1. 복사생성자란?

01-C++에서 선언 및 초기화 방법

C++은 변수/참조자에 대하여 기본적인 초기화 방법 외에도 객체 생성시 생성자를 이용해 초기화 했던 방식으로 선언 및 초기화를 할 수 있도록 지원한다.

  • 기본적인 방법 : int num = 20;
  • 객체 생성에 사용했던 방법 : int num(20)

02-객체 대입을 이용한 초기화

복사생성자가 어떤일을 하는지 이해하기 위해서 일단 주어진 예시코드와 그 실행결과를 보고 복사생성자의 기능을 예측해보자.

  • 소스코드 및 실행결과
    #include <iostream>
    using namespace std;
    
    class SimpleClass {
        private:
            int num1;
            int num2;
        public:
            SimpleClass(int num1, int num2) {
                this->num1 = num1;
                this->num2 = num2;
                cout<<"생성자 호출"<<endl;   
            }
            void Show() {
                cout<<"num1: "<<num1<<endl;
                cout<<"num2: "<<num2<<endl;
            }
    };
    
    int main(){
        SimpleClass cls1(100, 200);
        SimpleClass cls2 = cls1;  /*복사 생성자*/
        cout<<"=== 클래스 1의 멤버변수 값 ==="<<endl;
        cls1.Show();
        cout<<"=== 클래스 2의 멤버변수 값 ==="<<endl;
        cls2.Show();
        return 0;
    }

  • 소스코드 분석
    • 객체 cls1은 클래스에서 사용자가 정의한 생성자를 이용하여 정상적으로 생성되었다.
    • 그러나 cls2에서는
      • 기본 생성자가 존재하지 않는데 기본생성자를 이용한 생성방식을 사용했다.
        SimpleClass cls2;
      • 초기화를 할 때 각 멤버변수에 대한 초기화를 진행하는게 아니라 다른 객체를 대입하여 초기화했다.
        cls2 = cls1;
  • 결과 분석
    • 객체 cls1은 생성자를 이용하여 멤버변수를 초기화 할 때 사용한 값인 100, 200이 제대로 출력된다.
    • 객체 cls2 또한, 객체 cls1과 동일한 멤버변수 값을 가지는 것을 확인할 수 있다.

03-복사생성자

클래스의 객체를 인자로 받아, 멤버를 복사하는 생성자를 복사 생성자(copy constructor)라고 부른다.

  • 객체를 다른 객체를 대입하여 초기화하면, 각 객체간의 멤버 복사가 일어난 결과를 만든다.
  • 클래스 객체를 인자로 받는 생성자를 호출하는 것과 동일한 기능을 가진다.
    • 인자로 전달된 원본 객체는 변경하지 않으므로 const 키워드를 사용한다.
    • 복사생성자의 매개변수는 반드시 참조형이어야한다. &
    SimpleClass(const SimpleClass &copycls) 
    		: num1(copycls.num1), num2(copycls.num2) { }
  • 만약 복사생성자가 정의되지 않았다면, 멤버 복사 기능을 가지는 디폴트 복사 생성자가 자동으로 삽입된다.

04-대입 연산자를 이용한 객체 생성시 묵시적 변환

  • 01 단원에서 설명한 변수의 선언 및 초기화 방법처럼, 다른 객체의 대입을 이용한 객체의 초기화에도 2가지 방법으로 선언 및 초기화를 할 수 있다.
    • SimpleClass cls2 = cls1; (대입 연산 이용)

    • SimpleClass cls2(cls1); (생성자 호출)

      → 따라서 대입 연산자를 이용한 방식으로 객체를 생성하면, 두 번째 방식으로 묵시적 변환이 일어나서 복사 생성자가 호출된다.

  • 만약 이런 묵시적 변환이 일어나는 것을 막고싶다면, 정의한 복사생성자 앞에 explicit 키워드를 붙여 묵시적 변환이 발생하지 않게함으로서 대입연산자를 이용한 객체 생성 및 초기화를 막을 수 있다.
  • 또한, 인자가 1개인 일반적인 생성자에서도 묵시적 변환이 발생한다.
    • SimpleClass cls = 10;
    • SimpleClass cls(10);

2. 깊은 복사와 얕은 복사

01-얕은 복사와 깊은 복사

  • 얕은 복사 : 단순히 멤버가 가진 값을 그대로 복사한다.
  • 깊은 복사 : 참조변수의 경우 새로운 객체를 만들어 할당하는 방식으로 복사한다.
  • 디폴트 생성자는 멤버 대 멤버를 단순히 복사만 하는 얕은 복사 방법을 사용한다.

02-얕은 복사의 문제점

얕은 복사 방식은 멤버변수가 힙(HEAP)의 메모리 공간을 참조하는 경우에 문제가 된다.

  • HEAP에 할당된 멤버(문자열, 객체…)는 참조를 이용하여 접근하기 때문에, 변수 자체에는 참조하려는 주소값이 저장되어있다.

  • 따라서 참조변수를 얕은 복사 방식으로 복사하게 되면 주소 값이 복사되기 떄문에 동일한 주소를 참조하는 새로운 멤버를 만들게 된다.

  • 이로 인해 발생하는 문제점

    • 같은 주소를 참조하기 때문에 어떤 하나의 객체의 (참조타입)멤버변수 값만 변경해도 복사한 다른 객체의 멤버값이 모두 변경된다.
    • 원본 객체가 소멸할 때 해당 멤버변수를 삭제하기 때문에, 복사 객체가 소멸할 때는 이미 지워진 멤버변수를 대상으로 delete 연산을 하기 때문에 문제가 발생한다.
  • 얕은 복사의 문제점이 발생하는 예제 코드

    • 원본 객체를 이용해 복사 객체를 디폴트 복사 생성자를 이용하여 복사한 뒤, 복사된 객체의 멤버변수 값만 변경했지만 원본 객체의 멤버 참조변수(=name)까지 함께 변경되었다.
    • name 이 원본 객체가 소멸되면서 사라져, 복사본 객체가 소멸자를 호출하면 제대로 동작하지 않아 “소멸자 호출" 문장이 1번만 출력되었다.
    #include <iostream>
    using namespace std;
    
    class Student {
        private:
            int num;
            char* name;
        public:
            Student(int num, char* name) {
                this->num = num;
                int len = strlen(name) + 1;
                this->name = new char[len];
                strcpy(this->name, name);
                cout<<"생성자 호출"<<endl;   
            }
            void SetStudentInfo(int num, char* name) {
                this->num = num;
                int len = strlen(name) + 1;
                strcpy(this->name, name); 
            }
            void GetStudentInfo() {
                cout<<"num: "<<num<<endl;
                cout<<"name: "<<name<<endl;
            }
            //소멸자
            ~Student() {
                delete []name;
                cout<<"소멸자 호출"<<endl;
            }
    };
    
    int main(){
        Student ori(1800, "홍길동");
        Student copy = ori;  /*복사 생성자*/
    
        cout<<"=== 원본 객체의 멤버변수 값 ==="<<endl;
        ori.GetStudentInfo();
    
        cout<<"=== 복사본 객체의 멤버변수 변경 ==="<<endl;
        copy.SetStudentInfo(2000, "전우치");  
    
        cout<<"=== 원본 객체의 멤버변수 값 ==="<<endl;
        ori.GetStudentInfo();
        cout<<"=== 복사본 객체의 멤버변수 값 ==="<<endl;
        copy.GetStudentInfo();
        
        return 0;
    }

03-깊은 복사를 이용하는 복사 생성자의 정의

깊은 복사 방법을 이용하고자 한다면, 단순히 참조변수에 저장된 값인 주소값을 복사하는 것이 아니라 해당 참조변수가 참조하는 값을 복사하여 새로운 변수를 동적할당해주어야 한다.

  • 깊은 복사 생성자가 하는일
    • 참조변수가 아닌 멤버 변수 복사 (=앝은 복사와 동일)
    • 참조변수는 새로운 메모리 공간을 할당한 뒤 기존 참조변수가 참조하는 값을 복사하여 할당하고 메모리 주소값을 멤버에 저장
  • 깊은 복사 생성자 예시
    //(깊은)복사 생성자
    Student(const Student& copy) : num(copy.num) {
        int len = strlen(copy.name) + 1;
        name = new char[len];
        strcpy(name, copy.name);
    }

3. 복사생성자의 호출시점

복사 생성자는 세 가지 시점에서 호출된다.

01-메모리공간의 할당과 초기화가 동시에 일어나는 세가지 경우

  • 새로운 메모리 공간을 할당하여 변수를 생성함과 동시에, 다른 변수에 저장된 값으로 초기화할 때
    int num1 = num2;
  • 함수의 호출과 동시에 인자를 Call-by-value 방식으로 전달할 때
    func(num1)
  • 어떠한 값을 반환할 때
    return num1;

→ 반환된 값을 변수에 저장하지 않더라도, 값을 반환함과 동시에 함수가 종료되기 때문에 반환값을 사용하기 위해서는 사라지는 지역변수를 대신하여 별도로 저장하고 있어야 한다.

따라서 함수가 어떤 값을 반환하면, 별도의 메모리 공간이 할당되면서 반환값으로 초기화된다.

02-복사생성자가 호출되는 세가지 경우

복사생성자는 새로운 객체를 만들면서, 동시에 동일한 클래스의 객체로 초기화해야 할 때 호출된다.

  • 기존에 생성된 객체를 이용하여 새로운 객체를 초기화 할 때
  • Call-by-value 방식으로 함수 호출을 진행할 때, 전달된 인자가 객체인 경우 → 함수 안에서(지역 ) 새로운 객체를 만들어야한다.
  • 객체를 반환할 떄, 참조형으로 반환하지 않는 경우 [임시 객체]

⇒ 이러한 세 가지 상황은 01 단원에서 앞서 설명한 상황을 객체를 대상으로 진행한 것이라고 생각할 수 있다.

03-각 경우별 복사생성자의 호출과정

  • 기존에 생성된 객체를 이용하여 새로운 객체를 초기화 할 때
    (지금까지 복사생성자를 다루며 계속 설명한 경우이므로 여기서는 설명을 생략한다.)
  • 인자 전달에 의한 복사생성자 호출
    • 함수의 매개변수 객체의 복사생성자가 호출된다.
    • 복사생성자의 인자로 함수에 인자로 전달된 객체가 사용된다.
  • 반환에 의한 복사생성자 호출
    • 객체를 반환하면 임시 객체가 생성된다.
    • 임시 객체의 생성을 위하여 임시 객체의 복사생성자가 호출된다.
    • 복사생성자의 인자로 반환된 객체가 사용된다.
    • 객체 반환과 동시에 함수 호출이 종료되기 때문에 지역적으로 선언되었던 객체는 소멸된다.

04-임시객체

  • 임시객체가 생성된 위치에는 임시객체의 참조 값이 반환된다.
  • 따라서 반환된 객체를 따로 저장하지 않더라도 반환 위치에 존재하는 임시객체의 참조 값을 이용하여 객체의 멤버에 대한 접근이 가능하다.
  • 임시객체의 소멸 시점
    • 임시객체는 다음 행으로 넘어가면 바로 소멸된다.
    • 만약 임시객체를 참조자가 참조하게 되면 소멸하지 않는다.
      SimpleClass &ref = func(100);
      → func함수에서 객체를 반환하며 생긴 임시객체의 참조 값이 참조자 ref에 전달된다.
profile
우주의 아름다움도 다양한 지식을 접하며 스스로의 생각이 짜여나갈 때 불현듯 나를 덮쳐오리라.

0개의 댓글