[C++ 기본 3] 1. 객체복사
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
}