ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [객체의 복사와 대입 연산자] 의도하는 동작이 있다면 구현하라
    개발/C·C++ 2021. 4. 26. 12:02

    primitive type이 자연스럽게 연산이 가능한 이유는 이미 구현되어 있기 때문입니다. = 연산자를 사용해 값을 초기화할수도 있고 복사도 할 수 있습니다. 우리는 그냥 사용하기만 하면 됩니다. 우리가 정의한 클래스도 초기화 하고 복사도 하며 대입도 됩니다. 복사와 대입 기능을 따로 구현한 적 없는데 되긴 됩니다. 친절은 여기까지입니다.

     

    컴파일러가 자동으로 수행해주는 복사와 대입은 값 복사만 해줍니다. 만약 동적으로 메모리를 할당한 포인터 변수가 있다면 값 복사는 매우 치명적인 오류의 원인이 됩니다. 객체가 여러 개인데 그 객체의 포인터가 가리키는 메모리가 한 곳이라고 생각해보세요. 끔찍합니다. 어느 한 객체의 포인터 변수만 해제가 되어도 나머지 객체의 포인터 변수는 dangling pointer가 됩니다. 다음 코드를 살펴보세요.

    #include <iostream>
    using namespace std;
    
    class Student
    {
    private:
        int _age;
        int _score;
        char* _name;
        
    public:
        Student(){}
        Student(int age, int score, const char* name)
        : _age(age), _score(score), _name(new char[strlen(name) + 1])
        {
        	strcpy(_name, name);
        }
        ~Student()
        {
            delete[] _name;
        }
    }
    
    int main()
    {
        Student s0(15, 77, "John");
        Student s1 = s0; // 복사
        Student s2;
        s2 = s0; // 대입
    }

    딱히 문제가 있어 보이지 않습니다. 적어도 컴파일 타임의 오류는 없습니다. 빨간줄이 안 떴으니까 한 번 실행을 해보겠습니다. 런타임 오류가 발생합니다. 처음에 언급했던 메모리 해제 문제 때문입니다. 값만 복사돼서 하나의 메모리를 두 개 이상의 변수가 가리키고 있기 때문입니다. 이를 얕은복사라고 합니다. 여기에선 char*를 쓰지 않고 string을 이용해서 이름을 받으면 간단하게 해결됩니다. 예제는 동적 메모리를 가리키는 포인터 멤버 변수가 있을 때 복사 생성자를 만들지 않고 대입 연산자를 오버로딩하지 않았을 때의 문제를 보여주기 위해 작성됐습니다.

    복사는 복사 생성자에서 위임 생성자를 이용해 값을 초기화하면 편합니다. 대입 연산자에서는 기존의 _name의 메모리를 해제해준 다음에 제대로 메모리를 할당해줍니다.

    #include <iostream>
    using namespace std;
    
    class Student
    {
    private:
        int _age;
        int _score;
        char* _name;
        
    public:
        Student(){}
        Student(int age, int score, const char* name)
        : _age(age), _score(score), _name(new char[strlen(name) + 1])
        {
        	strcpy(_name, name);
        }
        
        // 복사 생성자
        Student(const Student& s)
        : Student(s._age, s._score, s._name)
        {}
        
        // 대입 연산자 오버로딩
        Student& operator=(const Student& s)
        {
            _age = s._age;
            _score = s._score;
            if(_name)
            {
                delete _name;
                _name = nullptr;
            }
            _name = new char[strlen(s._name) + 1];
            strcpy(_name, s._name);
            return *this;
        }    
        
        ~Student()
        {
            delete[] _name;
        }
    }
    
    int main()
    {
        Student s0(15, 77, "John");
        Student s1 = s0; // 복사
        Student s2;
        s2 = s0; // 대입
    }

    대입 연산자의 반환값이 참조인 이유는 대입 연산자는 s0 = s1 = s2와 같은 연산도 가능해야 하며 이는 원본을 반환하는 것이기 때문입니다. 컴파일러가 암시적으로 만들어주는 디폴트 생성자, 복사 생성자, 대입 연산자는 오류가 발생하지 않는 최소입니다. 의도하는 뭔가가 있다면 반드시 구현해줘야 합니다.

    댓글

Designed by Tistory.