C++ Chapter 9.12 : 대입 연산자 오버로딩, 깊은 복사, 얕은 복사
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 9. 연산자 오버로딩 : 대입 연산자 오버로딩, 깊은 복사, 얕은 복사
얕은 복사
- 포인터 값인 주소만 복사하여 넘겨주는 것
깊은 복사
- 주소를 복사하여 넘겨주지 않고 나만의 새로운 메모리를 할당받아 확보한 뒤 그 공간에 내용물만 복사해 오는 것
🔔 얕은 복사
디폴트 복사 생성자
// MyString 클래스
MyString(const & MyString other) // 디폴트 복사 생성자
{
m_data = other.m_data;
m_length = other.m_length;
}
- 복사 생성자를 프로그래머가 정의해주지 않아도 모든 클래스는 기본적인 복사 생성자를 가지고 있다.
- 프로그래머가 복사 생성자를 정의하지 않으면 디폴트 복사 생성자가 호출된다.
- 디폴트 복사 생성자는 생략되어 프로그래머 눈엔 보이진 않지만 위와 같은 형태의 코드로 구성되어 있다.
- 인수로 들어온 같은 타입의 다른 객체의 모든 멤버 값들을 복사하여 자신의 멤버 값으로 초기화 한다.
- MyString(const & MyString other)
- 인수로 들어온 같은 타입의 다른 객체의 모든 멤버 값들을 복사하여 자신의 멤버 값으로 초기화 한다.
#include <cassert>
#include <iostream>
using namespace std;
class MyString
{
public:
char *m_data = nullptr;
int m_length = 0;
MyString(const char *source = "")
{
assert(source); // 문자열 인수로 꼭 받아야 함!
m_length = std::strlen(source) + 1;
m_data = new char[m_length];
for (int i = 0; i < m_length; ++i)
m_data[i] = source[i];
m_data[m_length - 1] = '\0';
}
~MyString()
{
delete [] m_data;
}
};
MyClass 클래스
- 멤버 변수
- char * m_data
- 문자열을 동적 할당받아 그 주소를 저장할 포인터
이 멤버 포인터를 얕은 복사 방식으로 복사하는 과정에서 문제가 생긴다.
- 문자열을 동적 할당받아 그 주소를 저장할 포인터
- int m_length
- 문자열 길이를 저장할 것
- char * m_data
- 생성자
- MyString(const char *source = “”)
- 이 생성자의 역할은 문자열 1개를 인수로 받아 두 멤버 m_data, m_length를 초기화 한다.
- cf)
char *
타입의 포인터를 문자열 리터럴로 초기화 할 순 없지만const char *
타입의 포인터를 문자열 리터럴로 초기화 하는 것은 가능하다! 참고 포스트
- cf)
- m_data 초기화
- m_length 만큼의 길이를 가진 동적 배열을 할당 받아 주소를 저장한다.
- for문 돌려서 인수로 받은 문자열의 한 글자, 한 글자를 복사해준다.
- 마지막 원소는
\0
- m_length 초기화
\0
도 끝에 붙는 것을 생각해야 하므로 인수로 들어온 문자열 길이의 + 1
- 소멸자
- 멤버 포인터 m_data가 가리키는 동적 메모리를 해제시켜준다.
- 멤버 변수
얕은 복사 사용시 문제점
int main()
{
MyString hello("Hello");
cout << (int*)hello.m_data << endl;
cout << hello.m_data << endl;
{
MyString copy = hello;
cout << (int*)copy.m_data << endl;
cout << copy.m_data << endl;
}
cout << hello.m_data << endl;
return 0;
}
💎출력💎
014CFB00
Hello
014CFB00
Hello
硼硼硼硼硼硼硼硼핥퀪?
- MyString
hello
(“Hello”);hello
객체의 m_data 는 힙메모리 주소값으로 초기화 되고hello
객체의 m_length는 6의 값으로 초기화 된다.
- cout « (int*)hello.m_data « endl;
hello
객체의 m_data 값(주소값) 출력- (int*)를 붙이는 이유
- std::cout은 문자열 포인터 (char *)타입이 들어올 경우 포인터가 아닌 문자열 내용을 출력하게끔 ostream 클래스 안에 오버로딩 되어 있기 때문에 주소값을 출력해주기 위해 int* 로 형변환
- cout « hello.m_data « endl;
hello
객체의 *m_data 문자열로 출력
{}
지역 범위- MyString copy = hello
copy
객체가 생성될 때hello
객체를 복사하므로 디폴트 복사 생성자가 호출된다.- 현재
Mystring 클래스
안에서 복사생성자를 정의하지 않았기 때문에 디폴트 복사 생성자가 호출 됨// 디폴트 복사 생성자는 다음과 같이 동작함 MyString(const & MyString other) { m_data = other.m_data; // ⭐얕은 복사⭐ m_length = other.m_length; }
디폴트 복사 생성자에서 m_data = other.m_data; 즉, 주소가 복사 되면서
copy
객체의 m_data 포인터와hello
객체의 m_data 포인터는 동일한 메모리를 가리키게 된다. 이러한 과정을얕은 복사
라고 한다. 두 주소값 출력 결과도 둘 다 “014CFB00”로 같은 것을 볼 수 있다.
- 현재
- cout « (int*)copy.m_data « endl;
copy
객체의 m_data 값(주소값) 출력
- cout « copy.m_data « endl;
copy
객체의 m_data 문자열로 출력
- 스코프가 끝나면서
copy
객체가 소멸된다copy
의 소멸자가 호출 된다.copy
의 m_data 가 가리키는 동적 메모리가 해제된다.
- MyString copy = hello
- cout « hello.m_data « endl;
- 이상한 값이 출력된다!
얕은 복사
의 과정으로 인해copy
객체의 m_data 포인터와hello
객체의 m_data 포인터가 동일한 동적 메모리 공간을 가리키고 있던 상태에서,copy
객체가 소멸되며 호출한 소멸자에서 *m_data* 도 해제시켰기 때문! 없어져 텅 빈 공간에 접근해 출력하려고 하니 이상한 값들이 출력되는 것이다.
- 이상한 값이 출력된다!
🔔 깊은 복사 구현하기
깊은 복사와 얕은 복사의 차이
- 다른 객체의 멤버 값들을 내 멤버로 복사해올 때
- 동적 메모리를 가리키는 포인터 멤버를 가지고 있는 클래스의 경우
얕은 복사
를 사용하여 주소를 복사하여 넘기게 되면- 복사 후 두 객체의 포인터 멤버가 동일한 공간을 가리키게 된다.
- 따라서 한 객체의 포인터 멤버가
delete
로 해제해도 다른 객체의 포인터 멤버는 아직 그 공간을 가리키고 있는게 되기 때문에 문제가 생긴다.
- 주소는 복사하지 않고 새로운 공간을 할당하여 포인터 멤버가 가리키는 공간의 내용물만 복사해온다면
- 두 객체의 포인터 멤버가 다른 공간을 가리키는 것이 되니 한 객체의 포인터 멤버가 해제되어도 다른 객체에 전혀 문제가 가지 않는다.
- 이러한 복사 방법을
깊은 복사
라고 한다.
복사 생성자와 대입 연산자 오버로딩의 차이
복사 생성자, =
대입 연산자 둘 다 복사할 수 있다는 점에서 기능은 비슷하다.
- 복사 생성자
- 생성자이므로 객체가 생성되는 과정에서 대입이 있는 경우 호출 된다.
MyString str1("Hello"); MyString str2(str1); // ⭐복사 생성자 호출 Mystring str3 = str1; // ⭐복사 생성자 호출
- 오버로딩 된 대입 연산자
=
- 단순히 이미 존재하는 객체끼리의 대입시 호출된다.
MyString str1("Hello"); MyString str2; str2 = str1; // ⭐ 대입 연산자 오버로딩 호출
- 자기 자신을 대입하는 것도 가능하므로 이 경우에 대한 처리가 필요하다.
- 자기 자신의 기존 동적 메모리를 비워주는 과정이 필요하다.
- 자기 자신에게 다른 객체를 복사하여 덮어 씌우는것이니까.
깊은 복사 구현하기
복사 생성자
MyString(const MyString &source)
{
m_length = source.m_length;
if (source.m_data != nullptr)
{
m_data = new char[m_length];
for (int i = 0; i < m_length; ++i)
m_data[i] = source.m_data[i];
}
else
m_data = nullptr;
}
- if (source.m_data != nullptr)
- 복사 대상이 되는 객체의 m_data가 nullptr이 아닌 경우에만
깊은 복사
진행.- 포인터가 가리키는 곳을 참조해야 하므로 nullptr이 아닌지 꼭 체크해 주어야 한다.
- nullptr이라면 똑같이 나의 m_data 값도 nullptr로!
깊은 복사
- 새로운 공간 할당
- m_data = new char[m_length];
- 복사 대상이 되는 객체의 m_data가 가리키는 내용물들 (동적 배열 원소들) 복사해오기
- for문 돌려서 일일이 복사
- 새로운 공간 할당
- 복사 대상이 되는 객체의 m_data가 nullptr이 아닌 경우에만
int main()
{
MyString hello("Hello");
cout << (int*)hello.m_data << endl;
cout << hello.m_data << endl;
{
MyString copy = hello; // ⭐복사 생성자 호출⭐
cout << (int*)copy.m_data << endl;
cout << copy.m_data << endl;
}
cout << hello.m_data << endl;
return 0;
}
💎출력💎
013DF918
Hello
013DFAD8
Hello
Hello
- 출력 결과
hello
객체의 m_data 값과copy
객체의 m_data 값이 서로 다른 것을 알 수 있다. - 출력 결과
copy
객체가 해제 되고도hello
객체에 영향 없이 m_data 내용물에 잘 접근할 수 있는 것을 볼 수 있다.
copy
객체가hello
객체로부터 멤버 값들을 복사 받을때 새로운 공간을 할당해서 내용물들을 옮긴거라 두 객체의 m_data 은 아예 다른 공간을 가리키고 있기 때문이다. 즉,깊은 복사
를 했기 때문!
대입 연산자 오버로딩
📢 주의사항 :
=
대입 연산자 오버로딩은 멤버 함수로만 구현이 가능하다.이유는 모르겠지만😱 전역 함수로 구현하는 것은 막혀있다.
- 멤버 함수로 구현되기 때문에
=
를 기준으로 왼쪽 피연산자 객체가 멤버 함수를 호출하는 자기 자신이 되며, 오른쪽 피 연산자 객체가 인수가 된다.
MyString& operator = (const MyString & source)
{
if (this == &source) // 자기 자신을 대입하는 경우
return *this;
delete[] m_data; // 자신의 기존 내용물 비워주기
/* 아래 과정은 복사생성자 깊은 복사 구현과 같다 */
m_length = source.m_length;
if (source.m_data != nullptr)
{
m_data = new char[m_length];
for (int i = 0; i < m_length; ++i)
m_data[i] = source.m_data[i];
}
else
m_data = nullptr;
}
- 자기 자신을 대입하는 경우엔 자기 자신을 리턴한다.
- 자신의 기존 내용물을 비워준다.
- 자신의 m_data 가 가리키는 메모리를 해제시켜준다.
- 새로운 공간을 할당 받을거라서!
- 깊은 복사
- 이 부분은 복사생성자와 동일하다.
int main()
{
MyString hello("Hello");
cout << (int*)hello.m_data << endl;
cout << hello.m_data << endl;
{
Mystring copy;
copy = hello; // ⭐오버로딩 한 대입 연산자 호출⭐
cout << (int*)copy.m_data << endl;
cout << copy.m_data << endl;
}
cout << hello.m_data << endl;
return 0;
}
💎출력💎
013DF918
Hello
013DFAD8
Hello
Hello
- 출력 결과
hello
객체의 m_data 값과copy
객체의 m_data 값이 서로 다른 것을 알 수 있다. - 출력 결과
copy
객체가 해제 되고도hello
객체에 영향 없이 m_data 내용물에 잘 접근할 수 있는 것을 볼 수 있다.
copy
객체가hello
객체로부터 멤버 값들을 복사 받을때 새로운 공간을 할당해서 내용물들을 옮긴거라 두 객체의 m_data 은 아예 다른 공간을 가리키고 있기 때문이다. 즉,깊은 복사
를 했기 때문!
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글남기기