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

[C++기초] 깊은복사와 얕은 복사 ,복사생성자

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

 

얕은 복사(참조만 복사)

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
 
using namespace std;
 
int main() {
    int* a = new int(5);
    int* b = new int(3);
 
    a = b;
 
    cout << "a=" << a << endl;
    cout << "b=" << b << endl;
 
    delete a;
    delete b;
}
cs

a에는 100이 들어갈거고 b에는 200이 들어갈거다.

a=b; 를 통해 200번지에 있는 값(3)을 100번지(5)에 넣으려 했는데

사실상 a=b의 의미는 b의 주소값(200)을 a의 주소값에 넣는 것이다.
a의 주소값이 200이기 때문에 a가 3을 가리키게 된다.
이러면 잘 된거 아니야? 라는 생각이 들 수 있지만 문제점이 2가지가 있다.


1) a의 주소값이 200이 되면서 원래 a가 100번지를 가리켰는데 200번지를 가리키면서
100번지의 값은 접근을 아예 못하게 된다.

2) delete a를 했을 때 a,b모두 같은 값을 가리키니까 200번지가 해제되고
delete b를 했을 때 b도 200번지를 가리키니까 200번지를 해제하려하는데 이미 해제가 되어있다.
이미 해제가 된 애를 해제하면 런타임에러가 뜬다.

결론적으로 a=b 의 형식은 참조만 복사되는 얕은 복사이다.
포인터가 가리키는 주소값을 바꾼다고 보면 된다.

깊은 복사 

 

가리키는 값 자체가 복사된 것이다. (값을 복사하는 것이다)
실제로 가리키는 공간자체에 들어있는 값을 바꾸는 것이다.

깊은 복사는 *a=*b 형식을 취한다.

 

얕은 복사 깊은 복사 
겍체 복사할 때 해당 객체만 복사하여 새 객체 셍성
복사된 객체는 원본객체와 같은 메모리 주소 참조
데이터 자체를 통째로 복사해서 
복사된 두 객체는 서로 각각 다른 독립적인 메모리를 차지함. 

동적할당과 깊은 복사

 


문자열의 길이를 저장해주는 String클래스를 만들어줄 것이다.

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
#include <iostream>
 
using namespace std;
 
class String {
public:
    String() {
 
    }
    String(const char* str) {
 
    }
 
    ~String() {
 
    }
 
private:
    char* strData;
    int len;
 
    
};
 
int main() {
 
}
cs

 

*strData는 문자열 동적할당을 해주는 것으로, 동적할당된 주소를 저장한다.
len은 문자열의 길이를 저장해준다.

10번째 줄에 String(const char* str) <-- str이 const char형을 가리키는 포인터라는 뜻이다.

여기서 더 수정을 해서 문자열을 동적할당 할 수 있는 코드를 만들어볼 것이다.

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
#include <iostream>
 
using namespace std;
 
class String {
public:
    String() {
        strData = NULL;
        len = 0;
 
    }
    String(const char* str) {
 
        //문자열 동적할당
 
        len = strlen(str);
        strData = new char[len+1];
        strcpy(strData, str);
    }
 
    ~String() {
        delete[] strData;
    }
 
    char* GetStrData() const {
        return strData;
    }
 
    int GetLen()const {
        return len;
    }
 
private:
    char* strData;
    int len;
 
    
};
 
int main() {
 
    String s;
 
 
}
cs

 

코드를 하나하나씩 뜯어보자.

 

 

첫번째 디폴트 생성자에서는 초기화를 해준다. strData는 포인터이기 때문에 0으로 초기화해주기 보단 NULL이라고 초기화해준다. len은 int형이기 때문에 0으로 초기화해줘도 된다.

특히 main함수를 보면 String s; 로 선언만 해줬을 경우에는 차라리 동적할당을 해주지 않는게 좋기 때문에 NULL이라고 strData를 초기화해주는 것이 좋다. 

두번째 생성자에서는 문자열을 동적할당해준다.
문자열 동적할당을 하려면 문자열의 길이를 알아야하기 때문에 매개변수로 받아온 문자열의 길이를 strlen을 통해 알아내고, 그 길이만큼 strData에 동적할당해준다.
len+1을 해주는 것은 문자열의 마지막에 null문자가 있기 때문에 len만 해주면 마지막 문자가 잘리기 때문에 len+1을 해준 것이다.

문자열을 복사해주면 strData=str; 이라고 생각해줄 수 있는데 이렇게하면 에러가 뜰뿐만 아니라 얕은복사가 되기 때문에 strcpy를 통해 깊은 복사를 실행해준다. 

(strData에 str의 주소가 들어가면서 어떤것도 strData를 가리키는것이 없게됨!!)


char* GetStrData() cosnt{} <-- 이런형태는 처음 봤을 것 같은데 strData가 char*형이기 때문에 그렇게 써준거다.

복사생성자/복사생성자 오버로딩

 

위의 코드를 조금 수정해보았다.

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
 
#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() {
        cout << "~String() 소멸자 호출" << endl;
        delete[] strData;
        cout << "strData 해제됨: " << (void*)strData << endl;
        strData = NULL;//NuLL로 초기화 안해주면 해제된 메모리에 접근이 안돼서 
    }
 
    char* GetStrData() const{
        
        return strData;
    }
    int GetLen()const {
        return len;
    }
 
private:
    char* strData;
    int len;
 
 
 
};
int main() {
    String s1("안녕");
 
    String s2(s1);//복사 생성자 사실상 String s2=String(s1); 사실상 객체 전달
 
    cout << s1.GetStrData() << endl;
    cout << s2.GetStrData() << endl;
 
     
}
cs


우선 코드를 보면서 의문이 생기는게
cout << "strData 할당" << (void*)strData << endl;
void포인터이다! void 포인터는 자료형이 정해지지 않은 특성때문에 어떤 자료형으로된 포인터든 모두 저장할 수 있다.
즉 void 포인터는 주소값만 가지고 있지 자료형은 가지고 있지 않는다.

여기서 void 포인터를 써준 이유는 strData가 문자열로 인식될 수도 있고, char를 나타내는 포인터로 인식될 수도 있기에이걸 문자열 취급(strData ==&strData[0]) 되지 않도록 void 포인터로 형변환해준 것이다. (즉, 문자열 취급 받지 않고 주소값을 내보낼 수 있게 void포인터로 형변환 해준 것이다. 

String s2(s1);//복사 생성자
이제 복사 생성자 부분을 살펴보자.
출력값을 보면 생성자는 한번 호출되지만 소멸자는 2번 호출된다. 뿐만 아니라 "안녕"은 두번 출력되는데
이는 s2객체에도 strData가 가리키는 공간이 할당되어야 하는데 할당되지 않았다(생성자가 호출되지 않았음)
근데 안녕이 두번 출력되는 것으로 보아 s1.strData의 주소값이 똑같이 복사된 것을 알 수 있다. (주소값이 같으니까 같은 대상 "안녕"을 가리킬 것)
결론적으로 얕은 복사가 된 것을 알 수 있다.

이때 String s2(s1)을 복사 생성자가 쓰인거라 볼 수 있는데
복사생성자란 클래스의 객체를 생성할 때 이미 존재하는 동일 클래스의 다른 객체(여기선 s1)를 그대로 복사하기 위해 필요하다.

복사생성자를 만들어보면 (복사생성자 오버로딩하는 것으로 보는게 더 정확하다)

1
2
3
4
5
6
7
8
String(const String &rhs) {
       
        cout << "String(String &rhs)생성자 호출" << endl;
        strData = new char[rhs.len + 1];
        strcpy(strData,rhs.strData);//깊은복사
       
        len = rhs.len;//깊은 복사
}
cs

복사생성자의 매개변수에 왜 굳이 레퍼런스 변수를 넘길까?


레퍼런스 변수 &rhs가 눈에 띄는데 String rhs를 하면 String의 정의에 이미 String이 포함되어 있어서 에러가 뜬다.

매개변수로 클래스형 객체를 받아오기 때문에 레퍼런스 변수를 써줘야 하는 것이다. (그리고 애초에 레퍼런스 변수 자체가 함수의 매개변수로 가장 많이 사용된다. 그중에서도 크기가 큰 객체를 함수에 인수로 전달할 때 사용한다.)

https://min-zero.tistory.com/entry/C-%EA%B8%B0%EB%B3%B8-%EA%B3%B5%EB%B6%80%EC%A0%95%EB%A6%AC-12-%EC%B0%B8%EC%A1%B0-%EB%B3%80%EC%88%98reference-variable

 

[C++ 기본 공부정리] 12. 참조 변수(reference variable)

공부 내용을 정리하는 목적 이므로 참고용으로만 읽어 주시기 바랍니다. 틀린 부분에 대한 지적은 감사합니다. 일반 변수 : 값을 저장하기 위해 메모리에 공간을 할당받아 직접 저장하는 변수

min-zero.tistory.com


https://thrillfighter.tistory.com/337

 

복사 생성자의 인수는 왜 레퍼런스를 사용하나?

복사 생성자는 클래스의 객체를 생성할 때 이미 존재하는 동일 클래스의 다른 객체를 그대로 복사하기 위해 필요하다. 클래스는 일종의 데이터 집합이다. 이 데이터를 다루는 도구인 멤버함수

thrillfighter.tistory.com

https://velog.io/@sjongyuuu/C-%EB%B3%B5%EC%82%AC-%EC%83%9D%EC%84%B1%EC%9E%90Copy-Constructor

 

C++ 복사 생성자(Copy Constructor)

지난 생성자와 초기화 리스트편 포스팅에 이어 이번 포스팅에서는 복사 생성자(Copy Constructor)에 대해 다루어 보려고 한다.

velog.io

정리하자면 레퍼런스를 붙이지 않아야 객체를 생성하지 않고 call-by-reference로 주소만 넘기면서 성능이 아주 좋아진다!

객체의 경우 메모리가 크기 때문에 레퍼런스 변수를 넣어주지 않으면 매개변수에 전부 복사하고

다시 매개변수를 새로운 객체에 복사하는 작업이 수행돼서 매우 비효율적이다.

근데 레퍼런스를 쓰면 객체를 복사하는 과정없이 새로운 객체에 복사하게 된다.

복사 생성자를 통해 우리는 매개변수로 받아온 rhs, 사실상 s1객체의 strData와 len을 복사해올건데,

strData=rhs.strData;
len=rhs.len; 을 해줄 수 있다.

사실 len과 rhs.len은 포인터가 아니라 기본자료형인 int이기 때문에 서로 다른 객체상에 있는 서로 다른 메모리에 있기에 아무런 문제가 되지 않는다. 즉, 얘네는 깊은 복사가 된다.

문제는 rhs.strData이다. 얘네는 얕은 복사가 일어난다. 왜냐면 strData가 포인터 이기 때문이다. 저렇게 등호만 써주면 참조만 복사되는 얕은복사가 일어난다.


s1과 s2에는 strData가 있는데 s1의 strData에는 100번지가 저장되어있다. 근데 strData=rhs.strData를 만나면서 s1의 주소값이 s2의 주소값으로 들어가게 된다.

이러면 메모리 해제가 2번되는 오류가 발생할 수 있다. 그렇기에 깊은 복사를 하기 위해서는
strData를 새로 할당해줘야 한다.

그래서 String(const char* str)의 아래에 있는 동적할당 코드를 가져다 쓰면되는데 (얘도 깊은복사 한거니까!)

strData = new char[rhs.len + 1];
strcpy(strData,rhs.strData);
 
중요한건 그냥 len이 아니라 rhs.len을 해줘야한다.
그냥 len은 private필드의 len이기에 매개변수로 받아온 s1객체의 len이 아니게 되므로, rhs.len을 함으로써 s1의 len의
길이를 가져다 써야한다.

strData = new char[rhs.len + 1];
strcpy(strData,rhs.strData); 이 두줄의 코드를 더 자세히 살펴보자.
 
첫번째 줄 코드는
strData는 포인터이기 때문에 얘가 가리키고 있는 문자열이 있을 것이다.

두번째 줄 코드는 strcpy를 사용해주는데,

strcpy를 통해 문자열을 복사해준다. 이 과정을 통해 깊은 복사가 일어난다.
s1의 private 필드에 있는 *strData와 len이 정상적으로 복사되게 된다.


출력값을 보면 (strcpy에러로 빌드 에러가 나서 출력 순서만 적는다)
const char *str 생성자 호출 -> strData할당 (s1객체 메모리 할당)-> &rhs 생성자 (복사생성자 호출)
\-> strData할당(s2객체 메모리 할당) -> "안녕"->"안녕" -> 소멸자 2개 호출

동적할당(복사할수 있는 공간을 만들어줌)strcpy(strData와 len을 복사해옴)를 통해 얕은 복사를 했을 때는 생성자가 한번만 호출됐는데 깊은복사를 하면서 생성자가 두번 호출된다.

 

 

그렇다면 복사생성자 왜 쓰는 것일까?

 

자기자신과 같은 타입의 매개변수로 객체를 초기화할 때 사용하는 생성자. 

복사본을 만들때 복사생성자를 사용한다. 

 

<특징>

  • 자신과 같은 타입의 객체를 인자로 받는다.
  • 복사 생성자가 정의되어 있지 않다면, 디폴트 복사 생성자(Default Copy Constructor)가 생성된다.

참고로 ++ 

 

복사생성자는 깊은복사를 하고

디폴트 복사생성자는 얕은복사를 한다. 

 

<복사 생성자가 사용되는 경우>

  • 첫째, 객체가 객체 자신의 Type(형식)으로 초기화되는 경우
  • 둘째, 함수의 매개 변수 Type(형식)으로 객체가 전달되는 경우
  • 셋째, 함수의 리턴 Type(형식)으로 객체가 리턴되는 경우
728x90
반응형

댓글