객체 복사
C#, Java, Python 같은 다른 많은 언어에서는 객체에 대해 = 연산을 수행하였을 경우 객체 복사를 수행하지 않는다.
하지만 C++에서는 class가 = 연산을 수행하였을 경우 복사가 일어난다.
#include <iostream>
#include <string>
using namespace std;
class Data
{
public:
int data1;
int data2;
string data3;
};
void Print(const Data& data)
{
cout << data.data1 << " " << data.data2 << " " << data.data3 << endl;
}
int main()
{
Data a;
a.data1 = 15;
a.data2 = 11;
a.data3 = "333";
// 복사
Data b = a;
Print(b);
return 0;
}
그렇다면 그 복사 과정은 구체적으로 어떤 형태로 일어날까?
C++에서는 생성과 동시에 복사를 하는 상황과 (복사생성자),
생성 이후 복사를 하는 상황(대입연산자)으로 나누어 복사 과정을 세분화시켜두었다.
다음 코드는 따로 정의하지 않더라도 C++ 컴파일러가 기본적으로 생성해주는 함수들이다.
기본적으로 이런 형태로 복사가 이루어지고 있기 때문에,
C++ 객체를 사용하는 경우에는 항상 복사비용에 대해 고려하며 사용해야 한다.
예를 들어 Data의 data3은 string 타입인데 이 문자열이 엄청 길어지는 경우를 가정해보자.
그런 경우 복사 생성자나 대입 연산자에서는 data3 = other.data3 을 통해 다시 string class의 대입 연산자를 호출하게 될 것이고, string class의 대입 연산자는 문자열을 빠짐없이 복사할 것이다. 그렇다면 이 data 객체를 복사하는 비용은 생각보다 훨씬 클 수 있다.
class Data
{
public:
int data1;
int data2;
string data3;
// 기본 생성자
Data() :data1(0), data2(0) {}
// 복사 생성자 (따로 정의하지 않는 경우 이 형태로 컴파일러가 생성해줌)
Data(const Data& other)
{
data1 = other.data1;
data2 = other.data2;
data3 = other.data3;
}
// 대입 연산자 (따로 정의하지 않는 경우 이 형태로 컴파일러가 생성해줌)
Data& operator=(const Data& other)
{
data1 = other.data1;
data2 = other.data2;
data3 = other.data3;
return *this;
}
};
이제 이 함수가 어느 타이밍에 호출되는지 소스코드와 출력 결과물 보고 확인해보자.
#include <iostream>
#include <string>
using namespace std;
class Data
{
public:
int data1;
int data2;
string data3;
// 기본 생성자
Data() :data1(0), data2(0) {}
// 사용자 정의 생성자
Data(int data1, int data2, string data3) : data1(data1), data2(data2), data3(data3) {}
// 복사 생성자 (따로 정의하지 않는 경우 이 형태로 컴파일러가 생성해줌)
Data(const Data& other)
{
data1 = other.data1;
data2 = other.data2;
data3 = other.data3;
cout << "copy constructor" << endl;
}
// 대입 연산자 (따로 정의하지 않는 경우 이 형태로 컴파일러가 생성해줌)
Data& operator=(const Data& other)
{
data1 = other.data1;
data2 = other.data2;
data3 = other.data3;
cout << "copy assignment operator" << endl;
return *this;
}
};
void Print(const Data& data)
{
cout << data.data1 << " " << data.data2 << " " << data.data3 << endl;
}
int main()
{
Data a;
a.data1 = 15;
a.data2 = 11;
a.data3 = "333";
cout << "----------------------------------------" << endl;
// 복사 생성자
Data b = a;
cout << "----------------------------------------" << endl;
// 대입 연산자
b = a;
cout << "----------------------------------------" << endl;
return 0;
}
----------------------------------------
copy constructor
----------------------------------------
copy assignment operator
----------------------------------------
컨테이너에서의 객체 복사
객체 복사의 비용은 컨테이너와 같이 사용될 때 더 극대화된다.
다음 코드와 결과물을 확인해보자.
// ...
int main()
{
Data a(15, 11, "333");
vector<Data> vec;
vec.push_back(a);
vec.push_back(a);
vec.push_back(a);
vec.push_back(a);
vec.push_back(a);
vec.push_back(a);
cout << "----------------------------------" << endl;
vec.erase(begin(vec));
vec.erase(begin(vec));
return 0;
}
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
copy constructor
----------------------------------
copy assignment operator
copy assignment operator
copy assignment operator
copy assignment operator
copy assignment operator
copy assignment operator
copy assignment operator
copy assignment operator
copy assignment operator
코드와 결과물을 보면, 생성자를 호출한 횟수보다도 훨씬 많은 복사 생성자를 호출하고 있음을 확인할 수 있다.
왜 이런 현상이 일어나는 것일까?
이것은 vector에 추가된 원소의 수가 내부적으로 잡아둔 배열 용량보다 커지는 경우, 배열을 좀 더 큰 사이즈로 재할당하고 내부 변수들을 모두 복사하기 때문이다. 이와 비슷한 현상은 push_back 뿐만 아니라 erase 시에도 발생한다. 배열 내부 원소를 제거하는 경우 중간 메모리만 비워둘 수 없기 때문에 뒤의 모든 원소들을 모두 한칸씩 옮기고 이 과정에서 복사가 여러번 발생하게 된다.
그러므로 컨테이너에서 구조체를 사용하는 경우, 구조체의 크기가 꽤나 큰 상황이라면 이런 복사비용을 생각하며 개발해야한다. 그렇다면 이 문제를 완전히 피할 수 있는 방법은 없을까??
가장 쉬운 방법은 다른 언어들처럼 객체를 레퍼런스 타입으로 관리하는 것이다.
// ...
int main()
{
vector<Data*> vec;
vec.push_back(new Data(15, 11, "333"));
vec.push_back(new Data(15, 11, "333"));
vec.push_back(new Data(15, 11, "333"));
vec.push_back(new Data(15, 11, "333"));
vec.push_back(new Data(15, 11, "333"));
vec.push_back(new Data(15, 11, "333"));
cout << "----------------------------------" << endl;
vec.erase(begin(vec));
vec.erase(begin(vec));
return 0;
}
----------------------------------
보다시피 복사 생성자나 대입 연산자가 단 한번도 호출되지 않았다.
하지만 이 코드는 복사 비용문제는 해결했지만 다른 문제가 남아있다.
바로 vector가 파괴될 때 내부 원소들에 대한 메모리 정리를 해주지 않는다는 것이다.
물론 파괴되기 직전에 for loop을 돌리며 모든 원소를 delete 해주면 해결될 것이다.
하지만 개발자가 메모리 해제를 잊어버린다면 어떨까??
물론 그러지 않도록 주의하여야겠지만, 인간은 언제나 실수를 하기 때문에, 그런 상황 자체를 시스템적으로 막는 방법을 생각해보는건 현명한 일이다.
C++에서는 메모리 관리를 위한 포인터로 몇가지 스마트 포인터들을 지원하는데, 위와 같은 상황에 가장 적합한 스마트 포인터는 unique_ptr이다. unique_ptr은 특정 객체에 대한 유일한 소유권을 갖는 포인터로서 포인터가 파괴될 때 담당하는 객체도 파괴시켜준다. 그러므로 unique_ptr로 감싼 후 컨테이너에 넣어준다면 메모리 문제와 복사비용 문제를 모두 해결할 수 있다.
소멸자를 정의하여 잘 파괴되었는지 확인해보자.
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Data
{
public:
int data1;
int data2;
string data3;
// 기본 생성자
Data() :data1(0), data2(0) {}
// 소멸자
~Data()
{
cout << "destructor" << endl;
}
// 사용자 정의 생성자
Data(int data1, int data2, string data3) : data1(data1), data2(data2), data3(data3) {}
// 복사 생성자 (따로 정의하지 않는 경우 이 형태로 컴파일러가 생성해줌)
Data(const Data& other)
{
data1 = other.data1;
data2 = other.data2;
data3 = other.data3;
cout << "copy constructor" << endl;
}
// 대입 연산자 (따러 정의하지 않는 경우 이 형태로 컴파일러가 생성해줌)
Data& operator=(const Data& other)
{
data1 = other.data1;
data2 = other.data2;
data3 = other.data3;
cout << "copy assignment operator" << endl;
return *this;
}
};
void Print(const Data& data)
{
cout << data.data1 << " " << data.data2 << " " << data.data3 << endl;
}
int main()
{
vector<unique_ptr<Data>> vec;
vec.push_back(make_unique<Data>(15, 11, "333"));
vec.push_back(make_unique<Data>(15, 11, "333"));
vec.push_back(make_unique<Data>(15, 11, "333"));
vec.push_back(make_unique<Data>(15, 11, "333"));
vec.push_back(make_unique<Data>(15, 11, "333"));
vec.push_back(make_unique<Data>(15, 11, "333"));
cout << "----------------------------------" << endl;
vec.erase(begin(vec));
vec.erase(begin(vec));
return 0;
}
----------------------------------
destructor
destructor
destructor
destructor
destructor
destructor
'프로그래밍 > C++' 카테고리의 다른 글
[C++] C++에서 Partial Class 구현 방법 (0) | 2023.09.23 |
---|---|
[C++] C++에서 Interface 구현 방법 (0) | 2023.09.23 |
[C++] Casting 총정리 (0) | 2023.09.23 |
[C++] C++의 데이터 타입 종류 (0) | 2021.04.17 |
[C++] friend (2) | 2021.04.06 |