반응형

기본 개념

복사 생성자( Copy Constructor )

  • 객체가 자신과 동일한 타입으로 초기화 될때 사용

  • 사용자가 생성하지 않으면 컴파일러가 자동 생성(Default Copy Constructor)

  • 컴파일러가 제공하는 복사 생성자는 모든 멤버를 복사(Bitwise copy)

  • 필요한 경우 사용자가 직접 정의 가능

복사 생성자의 형태

  • Point( const Point& p)

class Point
{
public:
    int x;
    int y;
public:
    Point() : x(0), y(0) {} // 생성자 #1
    Point(int a, int b) : x(a), y(b) {} // 생성자 #2

    // 내부적으로 컴파일러에 의해 생성된 복사 생성자 형태
    // Point(const Point& p): x(p.x), y(p.y) {}
};

int main()
{
    Point p1; // 생성자 #1 호출
    Point p2(1, 2); // 생성자 #2 호출
    //Point p3(1); // 에러
    Point p4(p2); // 컴파일러가 생성한 복사 생성자 호출

    std::cout << p4.x << std::endl;
    std::cout << p4.y << std::endl;
}

복사 생성자가 호출되는 경우

  • 객체가 자신의 타입으로 초기화 될때

    • Point p2(p1);

    • Point p2{p1};

    • Point p2 = p1;

//객체가 자신의 타입으로 초기화 될때

class Point
{
    int x, y;
public:
    Point(int a = 0, int b = 0) : x(a), y(b) 
    {
        std::cout << "Point()" << std::endl;
    }

    Point(const Point& p): x(p.x), y(p.y)
    {
        std::cout << "Point(Point&)" << std::endl;
    }
};

int main()
{
    Point p1(1, 2); // 생성자
    Point p2(p1); // 복사 생성자
    Point p3{ p1 }; // 복사 생성자
    Point p4 = p1; // Point p4(p1) 복사 생성자
}
  • 함수 인자로 객체를 값으로 받을 때( call by value )

    • const &를 사용하면 복사 생성자/소멸자의 호출을 막을 수 있음

    • void foo( const Point& p);

// 함수 인자로 객체를 값으로 받을 때( call by value )

void foo(Point pt) // 이 순간 복사 생성자 호출
{

}

int main()
{
    Point p; // 생성자 호출
    foo(p); 
}
  • 함수가 객체를 값으로 리턴할 때( return by value )

    • RVO / NRVO

    • g++ 사용시 -fno-elide-constructors 옵션 이용하면 확인 가능

// 함수가 객체를 값으로 리턴할 때( return by value )

Point foo()
{
    Point pt(1, 2); // 생성자

    return pt; // 리턴용 임시객체 생성
}

int main()
{
    foo(); // 해당 라인동안만 임시객체는 유효
}

RVO(Return Value Optimization) / NRVO(Named RVO)

  • 임시 객체(Temporary Object)
    • "클래스이름()"으로 만드는 객체
    • 사용자가 직접 만들거나 컴파일러에 의해 임시 객체를 만드는 코드가 생성되기도 함
    • 단일 문장에서만 사용되며, 문장의 끝에서 파괴됨
    • 함수가 참조로 리턴하면 리턴용 임시객체가 생성되지 않음
    • 지역변수는 참조로 리턴할 수 없음
// RVO 적용 코드

Point foo()
{
    return Point(1, 1); // 객체 생성자체를 임시 객체로 생성하여 복사 생성을 방지
}

int main()
{
    foo();
}


// RVO 미적용 코드

// 내부 객체와 리턴용 임시 객체 총 2개가 생성된 코드
Point foo()
{
    Point pt(1, 1); // pt 생성 
    return pt; // pt를 복사한 리턴용 임시 객체생성
}

int main()
{
    foo();
}

Shallow Copy & Deep Copy

  • 얕은 복사( Shallow Copy )

    • 동적 할당된 메모리가 있을때 메모리 자체를 복사하지 않고 메모리 주소만 복사하는 현상

    • 소멸자에서 메모리를 삭제 하는 경우 문제 발생
    • 클래스 안에 포인터 멤버가 있고 동적 메모리 할당된 코드를 사용한다면 반드시 사용자가 복사 생성자를 만들어서 얕은 복사 문제를 해결 해야함

class Person
{
    char* name;
    int age;

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);
    }
    ~Person() { delete[] name; }
};

int main()
{
    Person p1("KIM", 2); // 단독 실행은 괜찮지만
    Person p2 = p1; // 이렇게 복사할 경우 runtime error(name을 같은 주소값을 가르키고, 소멸자에서 이중 해지 발생)
}
  • 깊은 복사( Deep Copy )

    • 주소가 아닌 메모리 내용 자체를 복사하는 것

class Person
{
    char* name;
    int age;

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);
    }
    ~Person() { delete[] name; }

    // 복사 생성자 직접 구현(Deep Copy)
    // 1. 포인터가 아닌 멤버는 그냥 복사(초기화 리스트)
    Person(const Person& p): age(p.age)
    {
        // 2. 메모리 할당
        name = new char[strlen(p.name) + 1];

        // 3. 메모리 내용을 복사
        strcpy_s(name, sizeof(name), p.name);
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // runtime error
}
  • 참조 계수(Reference Counting)

    • 얕은 복사를 기본으로 참조 카운트를 공유하여 최종 소멸시에만 메모리는 해지 하는 기법

// 참조 계수(Reference Counting)

class Person
{
    char* name;
    int age;
    int* ref; // 참조계수 메모리의 주소

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);

        ref = new int;
        *ref = 1;
    }

    // 소멸자에서 참조카운트 비교 후 최종 소멸시에만 동적 메모리 해지
    ~Person() 
    { 
        if (--(*ref) == 0)
        {
            delete[] name;
            delete ref;
        }
    }

    // 모든 멤버를 얕은 복사 후 참조 계수만 증가
    Person(const Person& p): age(p.age), name(p.name), ref(p.ref)
    {
        ++(*ref); // 참조 계수 증가
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // 복사 생성자.
}
  • 복사 금지(Delete Copy Constructor)

    • 복사 생성자를 삭제(delete)
      • 싱글턴 객체 생성시
      • C++ 표준의 unique_ptr
      • mutex 등의 동기화 객체
// 복사 금지

class Person
{
    char* name;
    int age;
    int* ref; // 참조계수 메모리의 주소

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);

        ref = new int;
        *ref = 1;
    }

    // 소멸자 얕은 복사
    ~Person() 
    { 
        if (--(*ref) == 0)
        {
            delete[] name;
            delete ref;
        }
    }

    // 복사 생성자 삭제
    Person(const Person& p) = delete;
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // 복사 생성자.
}
  • 소유권 이전(Move)
    • 객체가 사용하던 자원을 전달 하는 개념
    • 버퍼 복사 등의 일부 알고리즘에서 사용시 성능향상을 볼수 있음
    • C++11의 move 생성자 개념
    • 복사 후 원본 객체는 사용하지 못한다는 가정(swap에서 유용)
class Person
{
    char* name;
    int age;

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);
    }

    ~Person() { }

    // 소유권 이전의 복사 생성자
    Person(Person& p): age(p.age), name(p.name)
    {
        // 원래 객체가 가진 name을 0으로 
        p.name = 0;
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // 복사 생성자.
}

 

  • C++ 표준라이브러리(STL) std::string을 활용
    • 자체적으로 내부 버퍼관리가 되므로 얕은 복사시 문제 없음
class Person
{
    std::string name;
    int age;

public:
    Person(std::string n, int a) : name(n), age(a)
    {
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1;
}
반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 연산자 재정의 - 기본 개념  (0) 2019.05.08
C++ static, const, this  (0) 2019.05.07
C++ 접근 지정자, 생성자, 소멸자  (0) 2019.05.06
C++ OOP(Object Oriented Programming)  (0) 2019.05.06
C++ Explicit Casting  (0) 2019.05.06

+ Recent posts