본문 바로가기
C++/기초(두들낙서)

[C++기초] 깊은 복사, 얕은 복사 - 이동시맨틱,이동생성자,이동 대입연산자

by Meaning_ 2022. 1. 16.
728x90
반응형

지금까지는 깊은복사를 하기 위해 노력했는데 이번에는 깊은복사의 단점을 보완하기 위해  얕은복사를 해줄것이다.

 

깊은복사의 단점은 객체의 크기가 클 수록 많은 양의 복사를 수행해야한다. 

 

그래서 이번에는 깊은복사가 쓸모가 없어지는 경우에 대해서 알아보자.

 

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
#include<iostream>
 
using namespace std;
 
class String {
public:
    String() {
        cout << "String()생성자 호출" << endl;
        strData = NULL;
        len = 0;
    }
    String(const char* str) { //복사 생성자
        
        cout << "String(const*char)생성자 호출" << endl;
        
        len = strlen(str);
        strData = new char[len + 1];
        cout << "strData 할당" << (void*)strData << endl;
 
        #pragma warning(disable:4996)
        strcpy(strData, str);
    }
 
    String(const String &rhs) {//복사 대입 연산자 
        
        cout << "String(String &rhs)생성자 호출" << endl;
        strData = new char[rhs.len + 1];
 
        #pragma warning(disable:4996)
        strcpy(strData,rhs.strData);
        len = rhs.len;
    }
    ~String() {
        cout << "~String() 소멸자 호출" << endl;
        delete[] strData;
        cout << "strData 해제됨: " << (void*)strData << endl;
        
 
        
        strData = NULL;//NuLL로 초기화 안해주면 해제된 메모리에 접근이 안돼서 
    }
 
    String &operator=(const String &rhs) {
         
        if (this != &rhs) {
            delete[] strData;  
            strData = new char[rhs.len + 1];
 
            #pragma warning(disable:4996)
            strcpy(strData, rhs.strData);
            len = rhs.len;
            
        }
 
        
 
        return *this;
    }
 
    char* GetStrData() const{
        
        return strData;
    }
    int GetLen()const {
        return len;
    }
 
private:
    char* strData;//동적할당된 문자열 가리킴
    int len;
 
};
 
String getName() {
    cout << "=== 2 ===" << endl;
    String res("Doodle");
    cout << "=== 3 ===" << endl;
    return res;
}
int main() {
 
    String a;
    cout << "=== 1 ===" << endl;
    a = getName();
    cout << "=== 4 ===" << endl;
    
}
cs

 

약간의 TMI인데 계속 strcpy에러가 떴는데 모든 strcpy코드 위에 #pragma warning(disable:4996) 를 붙여주니 빌드가 잘됐다! 그래서 그냥 기분이 좋아서 TMI를 써봤다..ㅎ

 

이제 출력값을 보자.

우선 main함수에서 String a를 통해 객체가 생성되니까 디폴트 생성자 호출

그리고 === 1 ===  출력

getName()함수 호출 

=== 2 === 가 출력

res객체 만들어지며 String 생성자 호출 

== 3 == 가 출력

그다음에 복사 생성자 -> 복사 대입 연산자가 호출되는데 여기가 이상하다.

 

사실상 getName부터 main함수까지 깊은 복사가 일어날 부분은 a= getName() 밖에 없다. 

--> String a(getName()); 과 a=getName();은 같은 표현이기 때문!

a가 대입연산자를 통해 어떤 함수의 리턴값을 복사를 받는데 어떻게 복사 생성자가 호출될까?

 

임시객체 

 

main함수의 a는 getName의 리턴값을 받는다.

근데 문제는 res자체는 getName()이라는 함수 자체에 포함되어 있는 애고, getName()이 리턴되면 res가 소멸된다.

res가 리턴값인데, res를 리턴하면 res는 소멸된다.(??)

 

그럼 도대체 a는 어디를 받아야 getName()이 리턴하는 값을 받을 수 있을까?

 

우선 res는 아니다. getName이 리턴되는 순간에 그 리턴값을 받는 이름이 없는 객체인 임시객체가 필요하다.

임시객체는 getName이 리턴되며 res는 사라지지만 임시객체는 살아남는다.

이 임시객체가 a로 들어간다. 그러고 임시객체는 사라진다.

 

그래서 res -> 임시객체 임시객체 ->a로 가면서 복사가 2번 일어난다.

이 과정에서 깊은복사가 2번 일어난다.

 

하지만 2번의 복사가 불필요하다. 

 

res가 가지고 있는 strData는  문자열을 가리키고 있음. 임시객체 (얘도 타입은 string)도 strData를 가지고 있고, res

로부터 깊은 복사 받음 

a도 strData로 부터 깊은 복사를 받는다.

res,임시객체,a모두 다른 메모리 주소를 가진다. 

 

 

 

여기서 필요없는 부분이 res에서 임시객체로 깊은복사 되는 것이니까, 복사생성자 말고 얕은 복사 되게 할 것이다.

또한 임시객체에서 a로 깊은 복사되지 않고 얕은 복사가 되도록 할 것이다.

얕은복사의 문제점은 메모리 해제가 두번되는 것이다.

 

그래서 얕은복사는 하되, 소멸자는 한번만 호출되도록 할 것이다. 

 

이동시맨틱

"이동"이라는 컨셉을 가지고 얕은복사를 편하게 하는 것이다. 

 

res가 가진 strData가 문자열("Doodle")을 가리키지 못하게 하고 임시객체가 가진 strData가 그 문자열을 가리키도록 한다.

res에 있던 strData가 가리키는 문자열("Doodle")이  임시객체로 이동한다. 그리고 a까지 이동한다.

 이런과정을 이동 시맨틱이라 한다. 

이동시맨틱을 구현해주기 위해 r-value가 필요하다. 

 

 

r-value

 

우변에만 올 수 있는 애를 의미한다.

예를들어 

int a=5;

일때,

a=a;

는 가능하다. 이렇게 양변에 올 수 있는 애를 l-value라고 한다. 

이처럼 메모리 상에 저장되어 있으면 대입이 가능하다. 

반대로  

5=a;는 되지 않는다.

이때 우변에만 올 수 있는 5는 r-value다.

 

int f() {return 5;} 라는 함수가 있다.

이러고 메인함수에서 f()=1; 이라고 선언해줄 수 있을까?

당연히 절대 안된다. 리턴값에 1을 넣는다는게 말이 안된다..

 

여기서 f()가 반환하는 리턴값은 임시객체이다. 얘도 객체니까 메모리 상에 저장되어있다. 

근데 왜 1을 대입 못할까? f()는 임시객체이기 때문이다.

 

l-value는 메모리 상에 저장되어있는 값

임시객체는 예외적으로 함수가 반환해서 메모리 상에 저장되어 있긴 하지만 그렇다하더라고

값 자체를 수정할 수 없기에 r-value이다.

 

결론적으로 임시객체는 r-value이다. 

 

 

다시 돌아와서 

main함수의 a=getName();을 보자. 여기선 복사 대입연산자가 호출되고,

getName()은 r-value이다.

 

그래서 이 r-value를 표현하는 방법은

 

 

&&r 즉, r-value참조가 getName이 반환하는 임시객체를 참조해준다는 것이다.

그냥 r-value 참조자가 getName을 참조한다고 봐주면된다.

이 r-value참조자를 함수에 넣어주면  r-value를 매개변수로 받는 함수가 될거다.

 

 

이동 생성자

 

이제 이동생성자를 만들어 볼 것이다.

 

1
2
3
    String(String&& rhs) { 
 
    }
cs

 

매개변수를 r-value로 참조 받는 형태이다. 임시객체가 res로부터 복사를 받아야한다. 

(복사순서가 res->임시객체->a , 이해안되면 위에 사진 참고)

 

res가 반환될 때 l-value로 간주된다.&amp;amp;nbsp;

res는 수정이 가능한 l-value인데 어떻게 r-value참조가 될까?

res는 반환되는 동안에는 r-value로 간주가 된다. 그래야 r-value 매개변수로 들어갈 수 있기 때문이다.

결론적으로 res가 임시객체로 복사되는 과정에서 r-value로 간주된다.  

 

이제 얕은 복사를 하기 위한 코드를 작성해보자.

 

 

 

위 사진 처럼 res가 가진 strData가 더이상 문자열을 가리키지 않고 임시객체가 가진 strData만 문자열을 가리키게 해야

소멸자가 한번만 호출될 수 있다.

 

그래서 코드로 작성하면 

1
2
3
4
5
6
7
    String(String&& rhs) { 
        cout<<"String(String&&rhs): "<<this<<endl;
        len=rhs.len;
        strData=rhs.strData //얕은복사
        rhs.strData=NULL;
 
    }
cs

 

strData=rhs.strData는 얕은복사를 해준것이고

rhs.strData=NULL; --> 여기를 주목해서 보면 rhs.strData를 NULL로 지정해주었다.

res가 반환될때, 즉 소멸할때 strData가 NULL이 되면서 아무것도 가리키지 않게 된다. 

그러면 delete []strData를 해줘도(소멸자가 호출되도) 문자열이 소멸하지 않고 잘 살아남을 수 있다. 

(원래는 res가 반환될때 (소멸할때) 소멸자가 호출되면서 delete [] strData가 되니까 자기가 가리킨 문자열도 

같이 소멸됨. 근데 소멸자가 호출되기 전에 res가 가리키는 걸 NULL로 바꾸면서 얘가 문자열을 가리키지 못하게 함

-> 그니까 문자열이 살아남을 수 있음)

 

출력값을 보면 const char 생성자는 res이고 이 주소값은 00FCFC38이다 얘가 소멸되기 전에 String&& 인 이동생성자가 호출된것을 볼 수 있는데 신기하게도 메모리할당이 하나도 안된걸 볼 수 있다. (이동생성자 호출되고 나서) 

이걸 통해 얕은복사가 이뤄진것을 알 수 있다. 

 

이동 대입연산자

이동대입연산자는 &operator에 return *this만 해주면 된다. 

출력값을 보면 이전에 비해 엄청 길이가 줄어들었는데

operator도 이동대입연산자가 실행되면서 이제 메모리 할당이 한번(res객체 만들때)만 된 것을 할 수 있다. (모두 얕은복사로 해결됨!)

 

지금까지 res-> 임시객체만 알아봤는데 임시객체에서 a까지의 과정도 살펴보자.

임시객체가 r-value이고

getName호출되면서 res에서 임시객체로 복사 그리고 res 사라지면서 a=임시객체가 됨 

a=임시객체이기때문에 &operator(String &&rhs)가 호출된다

 

결론적으로 res-> 임시객체(이동생성자)

임시객체 -> a(이동대입연산자)

 

728x90
반응형

댓글