Part2::Ch 02. 연산자 오버로딩- 07. 대입 연산자 오버로딩, 복사 생성자

2025. 5. 3. 15:11Programming Language/C++

반응형

먼저 복사 생성자에 대해서 알아보도록 하자.

class Person {
private:
    string name;
public:
    Person() = default;
    Person(const char* c)
        :name(c)
    {

    }

    // 복사 생성자
    Person(const Person& other) {
        name = other.name;
    }
};

이런 클래스가 존재한다고 할때

int main() {
    Person p1("Alice");
    Person p2 = p1; 
    
    Person p1("Alice");
    Person p2;
    p2 = p1;
}

여기에서 

Person p2 = p1;
와
Person p2;
p2 = p1;

는 다르다.

 

위의 Person p2 = p1; 의 경우는 복사 생성자를 호출해서 사용하는 것이고  Person p2; p2 = p1; 의 경우는 대입 연산을 수행한 결과 이다.

 

기본적으로 복사생성자와 대입연산의 경우는 디폴트로 구현이 되어 있어 위 처럼 궂이 만들어주지 않더라도 

#pragma warning(disable:4996)
#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;
public:
    Person() = default;

    Person(const char* c)
        :name(c)
    {

    }
    void print() {
        std::cout << m_name << std::endl;
    }
};

int main() {
    Person p1("Alice");
    Person p2 = p1;  // 복사 생성자 호출 - 문제 없음

    Person p3("Brown");
    Person p4;
    p4 = p3; // 대입 연산자 - 문제 없음
}

이렇게 정상적으로 컴파일 되는 것을 볼 수 있다.

 

근데 만약에 우리가 name을 string이 아니라 char 배열로 받는다면 크기를 지정해줘야하고 

class Person {
private:
    char m_name[9];
public:
    Person() = default;

    Person(const char* name)
        :m_name()
    {
        strcpy(m_name, name);
    }
    void print() {
        std::cout << m_name << std::endl;
    }
};

이때 호출부에서 이 크기를 벗어난 값을 받는다면

int main() {
    Person p1("Alice Jame Kim");
    Person p2 = p1;  
}

에러를 발생시킨다.

 

그렇기에 이 크기를 동적으로 할당 받기 위해 타입을 char* 타입으로 변경하고 생성자 이니셜라이즈 리스트에서 동적으로 공간을 할당 받은 다음에 데이터를 복사해주도록 해주면

class Person {
private:
    char* m_name; // 동적 할당을 위해 타입변경
public:
    Person() = default;

    Person(const char* name)
        :m_name(new char[strlen(name) + 1]) // 동적 할당받을 크기 지정 및 동적 할당
    {
        strcpy(m_name, name); // 값 복사
    }
    void print() {
        std::cout << m_name << std::endl;
    }
};

int main() {
    Person p1("Alice Jame Kim");
    Person p2 = p1;  
}

문제가 없이 실행되는 것처럼 보인다.

 

그렇지만 저렇게 만들어주면 동적할당된 name의 공간을 해제해줘야 하기 때문에 소멸자를 만들어 해당 공간을 소멸시켜줘야한다.

class Person {
private:
    char* m_name;
public:
    Person() = default;

    Person(const char* name)
        :m_name(new char[strlen(name) + 1])
    {
        strcpy(m_name, name);
    }

    // 동적 할당된 공간을 소멸시킬 소멸자 생성
    ~Person() {
        delete[] m_name;
    }
    
    void print() {
        std::cout << m_name << std::endl;
    }
};

int main() {
    Person p1("Alice Jame Kim");
    Person p2 = p1;  
}

이렇게 실행해보면

또 문제가 있단다;

 

디버깅을 사용해서 이 문제의 시작점을 확인해보면

보면 복사된 m_name의 주소값이 동일한 것을 볼 수 있다.

 

이렇게 복사가 될때 얕은복사, 주소값만 복사해서 넣어줘서 발생하는 문제이다.

 

그래서 우리는 깊은 복사를 해야하고 복사 생성자를 사용해야하는 이유가 바로 이것이다.

 

복사 생성자는 

Person(const Person& person) {

}

이렇게 매개변수로 const Person& person 타입의 매개변수를 받아주고 이제 생성자 이니셜라이저 리스트를 사용해서 동적할당을 해준 후에 내부에서 strcpy로 값을 복사해주면 된다.

Person(const Person& person)
    :m_name(new char[strlen(person.m_name) + 1])
{
    strcpy(m_name, person.m_name);
}

이제 실행해보면

이렇게 주소값이 각각 다른것을 볼 수 있다.

 

추가로 생성자 이니셜라이저 말고 생성자 위임이라는 방식을 사용해서도 사용이 가능하다.

Person(const Person& person)
    :Person(person.m_name) // 생성자 위임, Person(name) 생성자를 부르는것과 동일
{
}

이렇게 되면 내부에 값 복사도 할 필요가 없어진다.

동일하게 작동하는 것을 볼 수 있다.

 

이제 대입 연산자 오버로딩을 해보자.

먼저 대입 연산의 경우는 아래 처럼 연속적으로 대입하는 경우도 있기 때문에 

Person p1{ "Alice Jame Kim" };
Person p2;
Person p3;
p3 = p2 = p1; // 연속적으로 대입하기도함

대입 연산자 오버로딩 함수의 반환값으로 그 객체의 타입을 그대로 반환해줘야 한다.

Person& operator=(const Person& p) {

}

그리고 이제 내부에서 값을 일일이 대입해주면 된다.

Person& operator=(const Person& p) {
    m_name = new char[strlen(p.m_name) + 1]; // 동적으로 공간할당
    strcpy(m_name, p.m_name); // 값 복사
}

그런데 이렇게만 해주면 되는게 아니라 이게 대입이기에 이전에 초기화시점에 만들어 졌던 동적할당된 공간을 지워주면서 새로 공간을 할당해줘야한다.

Person& operator=(const Person& p) {
    delete[] m_name; // 생성자로 만들었던 동적할당 공간 제거 
    m_name = new char[strlen(p.m_name) + 1]; // 새롭게 전달 받은 대입할 문자열만큼의 공간 생성
    strcpy(m_name, p.m_name); // 값 복사
}

그리고 이렇게 한 후에 마지막으로 연속 대입 연산이 가능하게 하기 위해  *this를 반환해준다.

Person& operator=(const Person& p) {
    delete[] m_name;
    m_name = new char[strlen(p.m_name) + 1];
    strcpy(m_name, p.m_name);
    return *this;
}

이러고 대입 연산을 실행해보면

int main() {
    Person p1{ "Alice Jame Kim" };
    Person p2 = p1;  
    p1.print();
    p2.print();

    Person p3, p4, p5;
    p5 = p4 = p3 = p2;
    p3.print();
    p4.print();
    p5.print();
}

주소값이 각자 다 다르게 

결과를 출력하는 것을 볼 수 있다.

 

 

**복사가 일어나는지 확인하는 방법

더보기

이전에 복사가 일어나는지 안일어나는지에 대해서 확인하지 않고 & 참조로 넘어가면 복사가 안되고 그냥 value로 넘어가면 복사가 일어난다고 했던 부분을 복사 생성자에 console을 찍으면서 확인이 가능하다.

복사생성자에 이렇게 로그를 찍어주고

단순하게 Person& 참조값을 매개변수로 받는 함수를 한번 만들고 

이렇게 함수로 던져주면 p1이 &참조로 받아지면서 복사가 일어나지 않는다.

이걸 참조가 아닌 Person 타입으로 받으면

이렇게 복사가 일어나는 것을 확인할 수 있다.

 

근데 또 반환타입이 Person이라면 

전달하는 타입과는 상관없이 return하면서 복사가 일어나게 된다.

이것도 반환을 & 참조로 변경하면

복사가 일어나지 않는다.

 

또한 내부에서 객체를 만들어서 반환하면

복사가 일어나지 않는다.

 

이런 점들에 대해서 알아두고 나중에도 테스트 해보면서 언제 복사가 일어나는지 파악해두면 좋을듯 싶다

 

 

복사가 일어나는 경우

- 함수의 매개변수의 타입이 참조가 아닌 경우 

- 반환형의 타입이 참조가 아닌경우

 

복사가 일어나지 않는 경우

- 함수의 매개변수의 타입이 참조인 경우

- 반환형의 타입이 참조인 경우

- 내부에서 생성한 객체를 참조형이 아닌 타입으로 반환하는 경우

 

 

반응형