얕은 복사
면접에도 자주 등장하는 단골 질문이다.
오늘 사용할 class를 만들어보자.
#include <iostream>
using namespace std;
class Knight
{
public:
public:
int _hp = 100;
};
int main()
{
// 그대로 복사하고 싶은 경우
Knight knight;
knight._hp = 200;
// 여러 방법 존재
Knight knight2 = knight; // 복사 생성자
Knight knight3;
knight3 = knight; // 복사 대입 연산자
return 0;
}
우리가 Knight 클래스에 아무런 코딩을 하지않았는데 빌드가 잘 되는 것을 볼 수 있다.
복사 생성자와 복사 대입 연산자를 직접 만들지 않으면 컴파일러가 암시적으로 만들어준다.
그렇다면 우리는 아무것도 안해도 될까?
그건 아니다. 컴파일러가 해주는 복사만 우리가 사용할 경우는 그럴 수 있지만 그렇지 않는 경우가 있다.
만약 이제 기사가 Pet이라고 하는 class를 가지고 있다고 해보자.
class Pet
{
public:
Pet()
{
cout << "Pet()" << endl;
}
Pet(const Pet& pet)
{
cout << "Pet(const Pet&)" << endl;
}
~Pet()
{
cout << "~Pet()" << endl;
}
public:
};
class Knight
{
public:
public:
int _hp = 100;
Pet _pet;
};
다음과 같은 경우라면 knight가 생성될 때 Pet의 생성자가 알아서 실행 될 것이다. 또한 player가 사라지면 pet의 소멸자가 실행될 것이다. 우리가 만약 pet이 player가 없어져도 돌아다니게 만들고 싶다고 했다면 위의 설계는 잘못된 것이다. 그리고 만약 pet안에 데이터가 4000 바이트가 있다고 하면 knight에도 4000가 포함되어 엄청 커졌을 것이다.
가장 중요한 것은 pet을 상속한 rabbit class가 있다고 했을 때, player는 사용할 수 없을 것이다.
그래서 대부분 *를 사용할 것이다.
#include <iostream>
using namespace std;
class Pet
{
public:
Pet()
{
cout << "Pet()" << endl;
}
Pet(const Pet& pet)
{
cout << "Pet(const Pet&)" << endl;
}
~Pet()
{
cout << "~Pet()" << endl;
}
public:
};
class Knight
{
public:
public:
int _hp = 100;
Pet* _pet;
};
int main()
{
Pet* pet1 = new Pet();
// 그대로 복사하고 싶은 경우
Knight knight;
knight._hp = 200;
knight._pet = pet1;
// 여러 방법 존재
Knight knight2 = knight; // 복사 생성자
Knight knight3;
knight3 = knight; // 복사 대입 연산자
return 0;
}
그래서 이렇게 하게된다면 knight는 정상적으로 pet을 가지고 있게 될것이다. 그런데 knight2에서도, knight3에서도 각자의 pet이 모두 pet1을 가리키고 있는 문제를 볼 수 있다.
이런 경우를 얕은 복사라고 한다.(shallow Copy)
멤버 데이터를 비트열 단위로 완전 똑같이 복사 하는 것이다 (메모리 영역 값을 그대로 복사)
#include <iostream>
using namespace std;
class Pet
{
public:
Pet()
{
cout << "Pet()" << endl;
}
Pet(const Pet& pet)
{
cout << "Pet(const Pet&)" << endl;
}
~Pet()
{
cout << "~Pet()" << endl;
}
public:
};
class Knight
{
public:
~Knight()
{
delete _pet;
}
public:
int _hp = 100;
Pet* _pet;
};
int main()
{
Pet* pet1 = new Pet();
// 그대로 복사하고 싶은 경우
Knight knight;
knight._hp = 200;
knight._pet = pet1;
// 여러 방법 존재
Knight knight2 = knight; // 복사 생성자
Knight knight3;
knight3 = knight; // 복사 대입 연산자
return 0;
}
이렇게 된다면 knight가 죽고 pet1이 delete하면 knight2, knight3 에서도 역시 delete를 요청하게 될 것이다. 이전 포스팅을 봤다면 알겠지만 이미 free한 메모리를 다시 free하는건 아주 위험한 일이기 때문에 이 경우 문제가 난다.
우리가 위에서 하고 싶었던거 바로 깊은 복사이다.
깊은 복사
Deep Copy
멤버 데이터가 참조(주소) 값이라면, 데이터를 새로 만들어준다. (원본 객체가 참조하는 대상까지 새로 만들어서 복사)
즉 pet이라고 하는 데이터를 힙 영역에 하나 더 만들어서 그 값을 가리키고 있도록 해야하는 것이다.
#include <iostream>
using namespace std;
class Pet
{
public:
Pet()
{
cout << "Pet()" << endl;
}
Pet(const Pet& pet)
{
cout << "Pet(const Pet&)" << endl;
}
~Pet()
{
cout << "~Pet()" << endl;
}
public:
};
class Knight
{
public:
Knight()
{
_pet = new Pet();
}
Knight(const Knight& knight) //깊은 복사
{
_hp = knight._hp;
_pet = new Pet(*knight._pet);
}
Knight& operator=(const Knight& knight) // 깊은 복사
{
_hp = knight._hp;
_pet = new Pet(*knight._pet);
return *this;
}
~Knight()
{
delete _pet;
}
public:
int _hp = 100;
Pet* _pet;
};
int main()
{
// 그대로 복사하고 싶은 경우
Knight knight;
knight._hp = 200;
knight._pet;
// 여러 방법 존재
Knight knight2 = knight; // 복사 생성자
Knight knight3;
knight3 = knight; // 복사 대입 연산자
return 0;
}
Steps
암시적 복사 생성자의 step
- 부모 클래스의 복사 생성자 호출
- 멤버 클래스의 복사 생성자 호출(포인터 아님)
- 두 경우가 아니라면 멤버가 기본 타입일 경우 메모리 복사 -> 메모리 복사
명시적 복사 생성자 step
- 부모 클래스의 기본 생성자 호출
- 멤버 클래스의 기본 생성자 호출
명시적 복사 생성자를 프로그래머가 작성하게 되는 순간 복사에 대한 모든 책임을 프로그래머가 가진다.
암시적 복사 대입 연산자 step
- 부모 클래스의 복사 대입 연산자 호출
- 멤버 클래스의 복사 대입 연산자 호출
- 멤버가 기본 타입일 경우 메모리 복사
명시적 복사 대입 연산자 step
- 알아서 해주는거 없음...
명시적 복사 생성자를 프로그래머가 작성하게 되는 순간 복사에 대한 모든 책임을 프로그래머가 가진다
명시적 복사 생성자를 프로그래머가 작성하게 되는 순간 복사에 대한 모든 책임을 프로그래머가 가진다
명시적 복사 생성자를 프로그래머가 작성하게 되는 순간 복사에 대한 모든 책임을 프로그래머가 가진다
Casting
이전 포스팅에서 다룬 타입 변환은 고전 C언에서 사용 하던 타입 변환이다. 이번 시간에 알아볼 것은 이것이다.
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
static cast : 타입 원칙에 비춰볼 때 상식적인 캐스팅만 허용해준다.
- int <-> float
- Plyaer * -> Knight*
int hp = 100;
int maxHp = 200;
float ratio = static_cast<float>(hp) / maxHp;
Player* p = new Knight();
Knight* k1 = static_cast<Knight*>(p); //안정성을 보장하는 것이 아님
dynamic_cast : 상속 관계에서의 안전한 형변환에 사용.
RTTI(RunTime Type Information)
dynamic_cast를 하려면 부모는 virtual 함수를 하나라도 가지고 있어야한다.
Player* p = new Knight();
Knight* k1 = dynamic_cast<Knight*>(p);
virtual 함수를 하나라도 만들면 객체의 메모리에 가상 함수 테이블 주소가 기입된다.
만약 잘못된 타입으로 캐스팅을 했으면 nullptr을 반환한다.
그렇기에 안전하게 null 체크를 통해 사용가능하다.
const_cast : const를 붙이거나 떼거나 할 때 사용. 자주 사용하지 않음.
PrintName(char* n){
cout << n << endl;
}
int main(){
PrintName(const_cast<char*>("Minseok"));
}
reinterpret_cast : 가장 위험하고 강력한 형태의 캐스팅
re - interpret == 다시 간주하다. 포인터랑 전혀 관계없는 다른 타입 변환하는 기능
Player* p;
__int64 address = reinterpret_cast<__int64>(p);
이런 상황이 많이 등장하지 않는다.
'C++ > 기초' 카테고리의 다른 글
[C++] C++의 위험성과 메모리 관리의 위험성 (3) | 2023.11.29 |
---|---|
[C++] 전방 선언 (0) | 2023.11.29 |
[C++] 타입 변환 - 2 (1) | 2023.11.28 |
[C++] 타입 변환 - 1 (0) | 2023.11.28 |
[C++] 동적 할당 (1) | 2023.11.28 |