프로그램/C,C++

[C++ 기본 3] 1. 객체복사

naudhizb 2022. 4. 4. 11:21
반응형

C에서 구조체를 복사하듯이 C++도 클래스의 복사가 가능하다.
하지만, C에서 포인터의 내용을 직접 복사 불가능하듯이 C++에서도 동적 메모리 할당된 영역은 자동으로 복사되지 않는다(shallow copy)

// 1_객체복사.cpp   102 page ~
#include <iostream>
class People
{
    char* name;
    int   age;
public:
    People(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~People() { delete[] name; }
};
int main()
{
    People p1("kim", 20);
    People p2 = p1;      // runtime error
}

C++ 컴파일러가 자동으로 복사생성자를 만들어 컴파일이 되며
People p2 = p1; 구문이 동작하지만, 멤버인 char* name; 에 대하여 복사할 때 주소만을 복사하여 소멸자 호출 시 비정상 동작으로 판단하여 런타임 오류가 발생하게 된다.

컴파일러가 자동으로 생성하는 복사생성자에 대한 문제를 해결하기 위해 사용자가 깊은 복사(deep copy)를 수행하는 코드를 만들어 주어야 한다.

// 1_객체복사.cpp   102 page ~
#include <iostream>
class People
{
    char* name;
    int   age;
public:
    People(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~People() { delete[] name; }

    // 깊은 복사(Deep Copy) 를 사용한 복사 생성자 
    // 104 page 그림
    People(const People& p) : age(p.age)
    {
        // 1. 포인터가 아닌 멤버는 그냥 복사
        //age = p.age;  // 이 코드 보다 초기화 리스트가 좋다.

        // 2. 포인터 멤버는 주소가 아니라 메모리 자체를 복사
        name = new char[strlen(p.name) + 1];
        strcpy(name, p.name);
    }
};


int main()
{
    People p1("kim", 20);
    People p2 = p1;   

    // 깊은 복사 의 단점(특징..)
    // 아래 코드의 메모리 그림을 생각해 보세요 - 105p 그림
    // => 동일한 자원이 메모리에 여러번 놓이게 됩니다.
    People p3 = p1;
    People p4 = p1;
    People p5 = p1;
}

위 코드와 같이 깊은 복사를 수행하는 생성자 코드를 직접 만들어 클래스를 생성하고 소멸하는데 있어 오류가 없도록 만들 수 있다.
하지만, 깊은 복사의 경우 같은 데이터가 여러번 복사되기 때문에 메모리의 낭비가 심하고 동작을 많이하기 때문에 성능이 떨어지는 단점이 있다.
이를 해결하기 위해서는 데이터를 복사하지 않고, 데이터가 참조되지 않는 경우에만 데이터를 free할 수 있도록 reference counter를 마련하면 된다.

#include <iostream>

class People
{
    char* name;
    int   age;
    int*  ref;   // 참조계수 메모리를 가리킬 포인터
public:
    People(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);

        ref = new int(1); // 한개를 1로 초기화
    }

    // 참조계수를 사용한 복사 생성자
    People(const People& p)
        : name(p.name), age(p.age), ref(p.ref) // 모두 얕은복사후에
    {
        ++(*ref); // 참조계수 증가
    }
            
    ~People() 
    {
        if (--(*ref) == 0)
        {
            delete[] name;
            delete ref;
        }
    }
};
// 106 page 그림 참고
int main()
{
    People p1("kim", 20);
    People p2 = p1;      

    // 참조계수 특징
    // 장점 : 동일 자원을 메모리에 한번만 놓게 되므로 효율적이다

    // 단점 : 1. 하나의 객체가 자원을 수정하려면 복사본을 만든후 
    //             수정해야 한다. - COW(Copy On Write)
    //        2. 멀티 스레드 환경에서 동기화가 복잡해진다.
}

reference counter를 이용하여 같은 데이터를 공유 할 수 있게 되었지만, 데이터를 변경하는 경우, 멀티 스레드에서 동시에 접근하는 경우와 같은 복잡한 경우에 대하여 대응하여야 하기 때문에 프로그램이 복잡해진다는 단점이 있다.

프로그램의 오버헤드를 증가시키거나, 프로그램을 복잡하게 만들 수 밖에 없는 경우에는 또 다른 정책을 사용할 수도 있다. 바로 동작 자체를 금지시키는 것이다. C++에서는 클래스의 연산자나, 생성자에 대하여 사용하지 못하도록 지정할 수 있으며 그 기능을 이용하여 컴파일 시간에 해당 연산을 사용하지 못하도록 막을 수 있다.

// 1_객체복사.cpp   102 page ~
#include <iostream>
class People
{
    char* name;
    int   age;
public:
    People(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~People() { delete[] name; }

    // 복사 정책 3. 복사 금지
    // 복사를 금지할때는 대입연산자도 같이 삭제 하는 것이 원칙
    People(const People& p) = delete;
    void operator=(const People& p) = delete;
};
void foo(People p) {}
int main()
{
    People p1("kim", 20);
    People p2 = p1;      // error

    People p3("lee", 30);

    p3 = p1; // 대입 연산자 호출. 삭제했으므로 error

    //foo(p1);
}

C++의 기본 라이브러리에서도 종류에 따라 다른 정책을 사용하여 객체를 관리하고 있음을 알 수 있다.

#include <iostream>
#include <vector>
#include <mutex>
#include <string>
#include <complex>

// 객체의 복사 방법   
// 1. 얕은 복사 - 컴파일러 제공 버전 사용
// 2. 깊은 복사
// 3. 참조 계수
// 4. 복사 금지

int main()
{
    complex<double> c1(1, 2); // 1 + 2i
    complex<double> c2 = c1;  // 얕은 복사

    vector<int> v1(10);  // 10개 크기의 동적 배열(크기가 변하는 배열)
    vector<int> v2 = v1; // 깊은 복사

    mutex m1;
    mutex m2 = m1; // error. 복사 생성자 삭제.
}
// 구글 에서 "webkit github" 검색후
// source/wtf/wtf/scope.h 열어 보세요. 
// ScopedExit 클래스 복사 생성자 찾아 보세요

사실 char*는 C언어 기반의 데이터 타입이다. 이는 C++의 가장 기본적인 특징 중 하나인 생성자와 소멸자를 지원하지 않기 때문에 가능하다면 생성자와 소멸자를 지원하는 문자열 클래스인 string 객체를 사용하는 것을 추천한다.

// 1_객체복사6.. 
// 객체복사 1번 복사

#include <iostream>
#include <string>

// 되도록이면 char* 를 사용하지 말고 string을 사용하자.!
class People
{
    //int* buff;
    //std::vector<int> buff;
    std::string name;
    int   age;
public:
    People(std::string n, int a) : name(n), age(a)
    {
    }
};
int main()
{
    People p1("kim", 20);
    People p2 = p1;      // runtime error
}
반응형