이전까지는 얕은 복사의 문제점을 가지고 깊은 복사를 진행했다면 이번에는 의도적으로 얕은복사를 해볼 것이다.
깊은 복사또한 단점이 있다.
strData를 전부 새로 할당한다음에 원본 객체에 있는 문자열을 하나하나 복사한다.
근데 만약에 길이가 길어지면 / 또는 객체의 크기가 커지면 많은 양의 연산을 수행해야한다.
임시객체와 깊은 복사
이 예시를 보면 분명 s5=getName();을 할때만 복사 대입연산자가 실행되면서 깊은복사가 일어날 것이라 추론할 수 있지만
실제로는 복사생성자도 호출되면서 깊은복사가 2번 일어나게 된다. (벌써 비효율이 느껴진다..🚨🚨)
복사생성자가 왜 호출되는지 알기 위해서는 임시객체를 알아야 한다.
우선 getName()이 리턴되면서 소멸자가 호출된다. 문제는 a=getName();을 할 때이다.
이미 소멸됐는데 어떻게 값을 받을까..?
getName이 소멸될때 리턴되는 값을 받는 새로운 변수가 필요한데 얘는 이름이 없는 임시객체이다.
임시객체에 res가 들어간다. res는 사라지지만 임시객체는 살아남고
a에 임시객체가 들어간다.
res가 임시객체로 복사되면서 (1번 복사) --> 복사생성자를 통해서 깊은복사
a에 임시객체가 복사되면서 (2번 복사) --> 복사대입연산자를 통해서 깊은복사
총 2번이 일어난다. 근데 굳이 2번의 깊은 복사가 필요할까?
어차피 res와 임시객체는 사라질 애들인데 굳이 할 필요가 없다.
얕은복사로 해결해보자.
2번 복사의 과정을 모두 얕은 복사로 해결해볼 것이다.
그런데 얕은 복사의 가장 큰 문제점은 메모리 해제가 2번 일어나는 것이다. 이미 해제한 곳의 메모리에 접근해서 해제하려는 문제점을 해결해야한다.
그래서 얕은 복사를 하되, 소멸자를 한번만 호출할 수 있게끔 해볼 것이다.
여기서 사용해볼수 있는게 C++의 이동시맨틱이다!
res가 가리키는 strData를 임시객체에 복사생성자를 통해 깊은 복사가 되는것이 지금까지 한 방법이였다면
깊은 복사를 하지말고 res의 strData가 가리키는 것을 끊고 임시객체의 strData가 res의 strData가 가리킨 곳을 가리키면 된다(주소값만 복사)
이렇게 임시객체의 strData가 이동을 해서 a의 strData까지 이동하게 된다. (이 과정은 복사가 아니고 "이동"이다)
이런 과정을 이동시맨틱이라고 한다.
이런 이동시맨틱을 구현하기 위해서는 r-value가 필요하다.
r-value
int a=5;
a=a;
이렇게 좌변과 우변에 모두 올 수 있는게 l-value이다. 여기서 a가 l-value가 된다.
5=a;는 되지 못한다. 여기서 5는 r-value가 된다.(우변에만 올 수 있고, 수정할 수 없는 값을 의미한다.)
l-value는 메모리 상의 공간에 저장되어 있다.
근데 임시객체는 메모리 상의 공간에 저장되어 있지만 값자체를 수정할 수 없기에
r-value가 된다. (함수도 r-value이다.함수가 리턴하는게 임시객체이기 때문!)
이동 생성자
매개변수로 r-value를 가져와야하는데 r-value는 &&rhs로 표현한다. (r-value참조)
res의 strData가 가리키는 문자열을 임시객체의 strData가 가리키고
res의 strData가 가리키는 것은 끊어야한다. 이러면 얕은복사만 일어나게 된다.
근데 문제는 임시객체가 res로 부터 복사를 받아야 하는데 res는 l-value이다.
근데 res가 반환될 동안에는 r-value로 간주된다. (r-value여야지만 이동생성자로 들어간다. )
이제 여기서 소멸자(메모리 해제)가 한번만 일어나기 위한 작업을 해줄 것이다.
rhs.strData가 NULL로 초기화 됐을 때 어떤일이 일어나는지 알아보자.
rhs라는 r-value 참조자가 res를 참조하고 있는 상황이다.
strData=rhs.strData; 코드를 보면
res의 strData가 어떤 문자열을 가리키고 있다.(여기서 res는 rhs로 봐줘도 된다.
getName함수에서 반환하는 것이 res이고 이것이 rhs로 들어오는 것이기 때문에)
그리고 임시객체의 strData도 그 문자열을 가리키고 있다.
근데 rhs.strData를 NULL로 초기화 시키면 res의 strData가 더이상 문자열을 가리키지
않을 것이다. 임시객체의 strData만 문자열을 가리킨다.
이렇게 되면 res가 소멸하는 시점보다 이전에 strData를 NULL로 만들어서 strData는 아무것도 가리키지 않기에
delete[] strData를 해줘도 문자열의 메모리는 소멸되지 않을 수 있다.
이게 무슨 말이냐면 소멸자가 호출되는 시점보다 이전에 res의 strData가 가리키는 것을 NULL로 바꾸면서 res가 문자열을
가리키지 못한다. 원래 소멸자가 호출되면 delete[] strData를 하면서 자기가 가리키는 문자열을 삭제하면서 소멸하는데 NULL로 바꿔주면 어차피 가리키는게 NULL이니까 res가 사라져도 (함수가 끝날때 res는 사라짐. 왜냐면 rhs라는 r-value참조자가 참조하는게 getName함수로부터 반환된 res이기 때문)문자열 데이터는 살아남게 되면서 아무런 영향을 미치지 않을 수 있다!
++) String(&&rhs) 소멸자가 호출될때 당연히 rhs 참조자가 가리키는 res도 같이 소멸한다. 그러면 res가 가리키는 문자열도 같이 소멸하게 된다. 그러면 임시객체의 strData는 가리키던 문자열이 사라지니까 가리키는 대상이 사라지는 어이없는 상황이 발생한다. 그렇기에 소멸자 호출 전에 res의 strData가 아무것도 가리키지 않게 설정해놓으면 소멸자가 호출해도 res랑 NULL만 소멸하기 때문에 임시객체의 strData는 멀쩡하게 문자열을 잘 가리킬 수 있다.
그러면 strData는 문자열을 계속해서 가리키게 되고 문자열은 살아남게 된다.
s5객체가 생성되면서 String() 생성자 호출
getName함수를 호출하면서 res에 Doodle이라는 문자열 동적할당 -> 깊은복사 1번
s5=getName() 대입연산시 얕은복사를 하는데 이때 String(String &&)함수 즉, 이동생성자 호출
그리고 소멸자가 호출되면서 얕은복사를 통해서 복사가 되는 것을 확인할 수 있다.
s5.getStrData()를 출력하면 Doodle이 잘 출력되는것을 확인할 수 있다.
이동대입연산자
여길 보면 이동 생성자 한번 호출되고 이동 대입연산자도 호출되면서 얕은복사만 이뤄지는 것을 확인
할 수 있다.
++) const 레퍼런스는 r-value도 담을 수 있다. 애초에 const를 쓰면 값을 수정하지 못하는데
r-value도 수정할 수 없는애를 담고 있기 때문!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | #include <iostream> #define _CRT_SECURE_NO_WARNINGS #pragma warning(disable:4996) using namespace std; class String { public: //생성자 String() { cout << "String() 생성자 호출" << endl; strData = NULL; len = 0; }; String(const char* str) { //문자열 동적할당 (생성자) cout << "String(const char* str) 생성자 호출" << endl; len = strlen(str); strData = new char[len+1]; cout << "strData 할당" << (void*)strData << endl; strcpy(strData, str);//strData에 str의 내용을 넣어준다.(깊은복사) } ~String() { cout << "~String() 소멸자 호출" << endl; delete[] strData; cout << "strData 해제됨: " << (void*)strData << endl; strData = NULL; }; String(const String &rhs) { //String s2=s1 복사생성자 호출할 때 cout << "String(String& rhs) 생성자 호출" << endl; strData = new char[rhs.len + 1]; strcpy(strData, rhs.strData); //새로 할당된 strData에 매개변수로 받아온 rhs.strData를 넣어준다. len = rhs.len; } String(String &&rhs) { cout << "String(String&&):" << this << endl; len = rhs.len; strData = rhs.strData; rhs.strData = NULL; } String &operator=(const String &rhs) { //s3=s1 대입연산자 수행할 때 //객체가 같은 값을 가지고 있는지가 아닌 같은 주소값을 가리키고 //있는지 판별해야함 if (this != &rhs) { cout << "opertaor=(const String &rhs) 연산자 실행" << endl; delete[] strData; strData = new char[rhs.len + 1]; strcpy(strData, rhs.strData);//깊은복사 len = rhs.len; //깊은 복사 (일반값) } return *this; } String &operator=(String&&rhs) { cout << "String &operator(String&&)" << this << endl; len = rhs.len; strData = rhs.strData; rhs.strData = NULL; return *this; } char* GetStrData()const { return strData; } int GetLen()const { return len; } private: char* strData; int len; }; String getName() { String res = "Doodle"; return res; } int main() { int val = 4; int& ref = val; cout << ref << endl; cout << &ref << endl; String s1="안녕!";//문자열 동적할당 String s2 =s1;//복사생성자 String s3="Hello"; //먼저 객체 할당 부터 들어감 "String(const char* str) 생성자 호출" s3=s1; //그다음에 operator= 연산자 호출됨 String s4 = "Hello"; s4 = s4; String s5; //String()생성자 호출 s5=getName();//얕은 복사로 수행할거기 때문에 이동생성자 호출 //깊은 복사가 이때만 일어나야함 (대입연산자 오버로딩) //근데 왜 복사생성자도? cout << s1.GetStrData() << endl; cout << s2.GetStrData() << endl; cout << s3.GetStrData() << endl; cout << s4.GetStrData() << endl; cout << s5.GetStrData() << endl; //등호의 우변에만 올 수 있는 값 5=a;는 안됨 //a는 좌변과 우변에 올 수 있기에 l-value } | cs |
'C++ > 기초(두들낙서)' 카테고리의 다른 글
Polygon 연습문제 (0) | 2022.07.10 |
---|---|
[C++] 묵시적 형변환 (0) | 2022.07.10 |
[C++] 분할 컴파일/ (0) | 2022.07.05 |
[C++] 함수와 구조체 / 함수 포인터 / 참조변수 (0) | 2022.07.05 |
[C++] 멤버 메서드 활용하기 (0) | 2022.07.02 |
댓글