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

[C++기초] 깊은복사, 얕은 복사 - 대입연산자

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

대입 연산자

그동안에는 객체가 생성될 때 복사가 같이 이뤄지는 경우 (복사생성자)를 봐왔는데

이번에는 객체가 먼저 생성되고 -> 이후에 대입이되는 코드를 짜보려한다.

이럴때도 연산자 오버로딩이 쓰인다. 

 

연산자 오버로딩을 하기 위한 메서드를 만들면 

'operator=' 메서드를 만들면 된다.

 

 

s3=s1;은 사실상 s3.operator=(s1); 이라 생각해도 된다. 

 

이렇게 s3=s1인 등호 연산을 실시하면 얕은복사가 일어난다.

이를 방지하기 위해 operator= 메서드의 코드를 채워볼 것이다. 

 

우선 operator메서드에서 매개변수를 &rhs인 레퍼런스 변수로 받아올 건데 그냥 rhs만 받아오면

s1이 rhs에 복사된다. 하지만 레퍼런스변수를 사용하면 s1이 rhs에 복사되는 형태가 아닌

rhs가 s1을 가리키는 상태가 된다. 이렇게 되면 객체 복사가 일어나지 않아서 복사의 과정을 한번 줄여서 효율적이다. 

 

반환 타입또한 레퍼런스  (String &)로 설정해볼 수 있다. 

예를 들어 레퍼런스를 반환타입으로 설정해주지 않고 리턴하게 되면 임시객체가 형성된다.

임시객체가 형성되는 순간에 또 객체 복사가 일어나서 객체복사가 두번 이뤄지는 비효율적인 상황이 생길 수

있다. 이때 반환타입을 레퍼런스로 설정해주면 과정을 한번 줄일 수 있다.

 

 

연산자 오버로딩을 활용하여 깊은 복사로 대입연산 실행하기 

본문내용넣기

이제 진짜 깊은 복사로 대입연산을 하기 위해 operator 메서드를 채워보자.

복사 생성자랑 똑같이 메모리공간 동적할당 -> 원본객체 복사의 과정을 거치는 코드를 작성해주었다.

복사생성자는 객체가 막 생성되는 순간이기 때문에 strData안의 공간이 없는 상태에서 할당이 되지만

대입연산은 이미 객체가 생성된 후 대입연산을 진행하는 것이기에 strData안에 값이 존재할 수 있다.

그래서 메모리를 해제해주고 새로 할당을 해줘야 한다. 

 

 

그리고 String 값을 반환하기 위해 코드를 수정해볼 것이다. 

그냥 rhs를 반환해봐야겠다..!로 생각할 수 있는데 const String을 String으로 반환할 수 없다

 

 

그래서 *this를 반환하면 대입된 값을 반환할 수 있다. 

this라는 것은 함수가 실행될 당시 함수에 속해있는 객체의 주소값이다. 여기에 별표를 붙여주면

객체자체가 나온다. 

 

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
 
#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;
 
        strcpy(strData, str);
    }
 
    String(const String& rhs) {
        
        cout << "String(String &rhs)생성자 호출" << endl;
        strData = new char[rhs.len + 1];
        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) {
        
        delete[] strData;
        strData = new char[rhs.len + 1];
        strcpy(strData, rhs.strData);
        len = rhs.len;
        return *this;
    }
 
    char* GetStrData() const {
 
        return strData;
    }
    int GetLen()const {
        return len;
    }
 
private:
    char* strData;//동적할당된 문자열 가리킴
    int len;
 
 
 
};
int main() {
    String s1("안녕");
 
    String s2(s1);//복사 생성자
 
    String s3;//기본 생성자 호출
    //s3 = s1; //기본생성자 호출 후 대입 --> 연산자 오버로딩
 
 
    s3.operator=(s1);//깊은복사 일어나야함 
 
    
 
    cout << s1.GetStrData() << endl;
    cout << s2.GetStrData() << endl;
    cout << s3.GetStrData() << endl;
 
    
 
}
cs

 

s3에 해당되는 출력값을 보면

 

디폴트 생성자 호출 -> operator 생성자의 strData 할당의 순서로 출력되는 것을 확인할 수 있다. 

 

 

자기 자신에 자기 자신을 대입하는 경우?!

 

 

이번에는 s3에 String(String &rhs) 생성자가 호출될 수 있도록 안에 문자열을 넣어볼 것이다. 

 

근데 이때 결함이 하나 있는데 자기 자신에 자기 자신을 대입하는 경우이다.

 

이런경우에는 쓰레기값이 출력이 된다. 

 

operator= 메서드를 자세히 보자.

 

1
2
3
4
5
6
7
8
9
10
11
 
 
    String &operator=(const String& rhs) {
        
        delete[] strData;
        strData = new char[rhs.len + 1];
        strcpy(strData, rhs.strData);
        len = rhs.len;
        return *this;
    }
 
cs

 

우선 main함수에서 

String s3("Hello");

s3.operator=(s3); 를 하면

 

strData가 "Hello"를 가리키는 상황이 된다.

 

대입연산을 하는 operaotr 메서드의 

delete[] strData; 를 하면

 

 

해제되면서 더는 가리키지 않는다. 하지만 rhs.len은 그대로 유지되고 결국 rhs는 s3를 가리키기 때문에 s3에서 매개변수로 받아온 "Hello"의 길이인 5는 여전히 남아있게 된다.

 

strData = new char[rhs.len + 1];

길이가 5+1인 새로운 문자열이 생기게 된다. 

 

그리고 strData가 새로운 문자열을 가리키게 될거다.

 

strcpy(strData, rhs.strData);

진짜 복사를 할때 문제가 생기는데 빈 문자열에는 어차피 쓰레기값만 채워져 있다. strcpy를 통해 빈문자열에 빈문자열을 복사하면 아무일도 일어나지 않는다..ㅋ

 

len = rhs.len;

결국 len도 5는 5로 바뀌게 된다. 

 

이런 상황을 바꾸기 위해 조건문을 써줘 볼 것이다. 

그냥 객체가 같은지만 비교하는게 아닌 객체의 주소값이 같은지를 비교해야한다.

즉, 들어있는 값이 같은지 비교하는게 아닌 두 객체가 메모리 상의 같은 공간에 있는 객체를 가리키고 있는지가

중요하다.  

 

그래서 if(this!=&rhs) 라는 조건문을 생각해 볼 수 있다. 이때 rhs는 레퍼런스로 생각하기 보단 rhs의 주소값

으로 생각해주는게 좋다. 

 

 

결국 자기자신에 자기자신을 대입하면 아무일도 일어나지 않을 것이다.

 

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
 
#include<iostream>
#define _CRT_SECURE_NO_WARNINGS
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;
 
        strcpy(strData, str);
    }
 
    String(const String& rhs) {
        
        cout << "String(String &rhs)생성자 호출" << endl;
        strData = new char[rhs.len + 1];
        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];
            strcpy(strData, rhs.strData);
            len = rhs.len;
 
        }
        
        
        return *this;
    }
 
    char* GetStrData() const {
 
        return strData;
    }
    int GetLen()const {
        return len;
    }
 
private:
    char* strData;//동적할당된 문자열 가리킴
    int len;
 
 
 
};
int main() {
    String s1("안녕");
 
    String s2(s1);//복사 생성자
 
    String s3("Hello");
    s3.operator=(s3);
 
    
 
    cout << s1.GetStrData() << endl;
    cout << s2.GetStrData() << endl;
    cout << s3.GetStrData() << endl;
 
    
 
}
cs

 

cout GetStrData() 부분만 어떻게 출력되는지 살펴보면

 

안녕

안녕

"Hello"가 된다.

 

왜냐면 자기자신에 자기자신을 대입하기에 함수가 실행될때 객체의 주소와 매개변수로 받아온 애의 주소가 같기 때문이다. 그래서 조건문에 의해 걸러져서 대입 연산 자체가 이뤄지지 않게 된다. 

 

지금까지 깊은복사 공부한 내용 정리 

728x90
반응형

댓글