일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- thread
- 한국산업기술대학교
- C
- 멀티쓰레드
- 옵저버
- 프레임워크
- Design Pattern
- 스마트포인터
- material
- c++
- 메모리관리
- 멀티코어 프로그래밍
- random access
- stl
- 쓰레드
- 유니크포인터
- Multithread
- sequential
- MultiCore
- EFFECTIVE C++
- 디자인패턴
- observer pattern
- 멀티코어
- vector
- Atomic
- Unreal
- 복사생성자
- 옵저버 패턴
- multi-core
- 게임공학과
- Today
- Total
태크놀로지
[Effective C++] 대입연산자, 제대로 메모리 관리를 하고 있는가? 본문
1. 대입연산자는 *this의 참조자를 반환하게 하자 (convention)
int x,y,z;
x = y = z = 15;
위와 같이 대입연산은 여러개가 사슬처럼 엮일 수 있다.
x = (y = (z = 15));
또 하나의 특성은, 우측 연관 연산이라는 점이다.
class Widget{
public:
Widget& operator= (const Widget& rhs){
//...
return *this;
}
}
이렇게 대입 연산이 사슬처럼 엮이려면 대입연산자는 좌변인자에 대한 참조자를 반환하도록 구현되어야 한다. (일종의 convention)
+=, -=, *= 등 모든 형태의 대입연산자에서도 Convention이 적용된다.
2. operator=에서 자기대입에 대한 처리가 빠지지 않도록 하자
class Widget;
Widget w;
...
w = w;
자기대입이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는것
// ex1)
for(int i= 0; ...) for(int j= 0; ...)
a[i] = a[j];
// ex2)
*px = *py;
이러한 상황이 일어나지 않을것이라고 착각할 수 있는데, 위와 같은 상황으로 인해 자기대입연산이 일어날 수 있다.
자기대입으로 인해 발생하는 상황
여러 곳에서 하나의 객체를 참조하는 상태(중복참조)가 발생한다.
- 중복참조의 상황을 구현해야할경우, 기본클래스의 참조자나 포인터를 사용하자.
자기대입에 대해 안전하게 구현해보자.
class Resource {
};
class Widget {
private:
Resource* pb; // 힙 할당한 리소스
public:
void swap(Widget& rhs){}
public:
// 안전하지 않은 operator=
Widget& operator=(const Widget& rhs) {
// 만약 this = rhs (자기대입)이라면
// pb는 삭제된 자기 리소스를 복사하게 됨
delete pb;
pb = new Resource(*rhs.pb);
return *this;
}
// 대책1 : 일치성검사
Widget& operator=(const Widget& rhs) {
// 객체가 같은지, 자기대입인지를 검사한다.
if (this == &rhs) return *this;
delete pb;
pb = new Resource(*rhs.pb);
return *this;
}
// 대책2 : 코드 문맥순서 변경
Widget& operator=(const Widget& rhs) {
// 원본포인터를 사본 포인터에 받아놓고, 사본포인터를 통해 원본데이터 삭제
Resource* orig = pb;
pb = new Resource(*rhs.pb);
delete orig;
return *this;
}
// 대책3 : 복사 후 맞바꾸기 (copy and swap)
// 자기대입안전성 (+ 예외 안전성)
Widget& operator=(const Widget& rhs) {
Widget temp(rhs); // 객체 소멸자를 이용한 자원관리 방법
swap(temp); // + 예외안전성 (자세한 부분은 자원관리파트에서 진행)
return *this;
}
};
안전하지 않은 operator=
만약 this = rhs (자기대입)이라면, pb는 삭제된 자기 리소스를 복사하게 된다.
대책1 : 일치성검사 operator=
객체가 같은지, 자기대입인지를 검사한다. 같을경우 복사과정없이 자기자신을 리턴한다.
대책2 : 코드 문맥순서 변경 operator=
원본포인터를 사본 포인터에 받아놓고, 사본포인터를 통해 원본데이터를 삭제한다.
대책3 : 복사 후 맞바꾸기 (copy and swap) operator=
자기대입안전성 뿐만아니라 swap함수에서의 예외 안전성까지 더해진다.
+ swap 함수 구현방법 (예외안전성에 대한 자세한 부분은 자원관리파트에서 진행)
3. 객체의 모든 부분을 빠짐없이 복사하자.
복사생성자와 대입연산자 선언을 하지 않을경우, 컴파일러가 자동으로 만들어냄.
-> 복사시 객체의 비트만 복사하기 때문에, 기본타입객체가 아닐경우 제대로 복사가 일어나지 않음.
클래스 상속 복사생성자,대입연산자 구현시 주의해야할 경우
class Customer {
private:
int name;
public:
Customer() : name(-1) {}
virtual ~Customer() = default;
Customer(const Customer& rhs) : name(rhs.name) {}
Customer& operator=(const Customer& rhs) {
name = rhs.name;
return *this;
}
void SetName(const int _name) {
name = _name;
}
const int GetName() const {
return name;
}
};
class PriorityCustomer : public Customer {
private:
int priority;
public:
PriorityCustomer() : priority(-1) {};
virtual ~PriorityCustomer() = default;
// 기본생성자의 데이터복사가 일어나지 않고있음.
PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority){}
PriorityCustomer& operator=(const PriorityCustomer& rhs) {
priority = rhs.priority;
return *this;
}
};
int main()
{
PriorityCustomer a;
a.SetName(5);
PriorityCustomer b(a);
cout << b.GetName() << endl; // 5가 나와야하지만 기본값 -1이 나옴
}
PriorityCustomer 복사생성자,복사대입연산자에서 기본생성자의 데이터(name) 복사가 일어나지 않고 있다.
컴파일시 name은 a와 같은 5가 나와야하지만, 기본값 -1이 나온다.
// 파생클래스 복사뿐만 아니라, 기본클래스까지 복사
PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), priority(rhs.priority) {}
PriorityCustomer& operator=(const PriorityCustomer& rhs) {
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
다음과 같이 파생클래스의 데이터를 복사할 뿐만 아니라, 기본클래스의 복사생성자,대입연산자를 호출하여 기본클래스의 데이터까지 복사해주어야한다.
복사대입연산자와 복사생성자에서의 공통된 부분 분리
복사대입연산자에서 복사생성자를 호출하거나, 복사생성자에서 복사대입연산자를 호출하지마라.
※ 생성자의 역할은 새로 만들어진 객체를 호출하는 것, 대입연산자는 '이미' 초기화가 끝난 객체에게 값을 대입하는것
- 복사대입연산자에서 복사생성자를 호출 : 복사대입연산자에서는 이미 만들어진 객체인데, 복사생성자를 호출한다는것은 이미 존재하는 객체를 '생성'하라는 꼴이다.
- 복사생성자에서 복사대입연산자를 호출 : 복사생성자에서 객체를 생성중에 있는데, 복사대입연산자를 호출한다는것은 아직 생성중인 객체에 값을 대입하는것이다.
해결책 : 제3의 함수에다 분리해놓고 양쪽에서 이것을 호출하여 사용
(1-3) 정리
- 대입연산자는 *this의 참조자를 반환하도록 만들어라
- operator= 구현시, 자기대입에 대해 제대로 처리하자. (1.원본객체와 복사대상객체의 주소비교, 2.문장의 순서를 적절히 조정, 3.copy and swap)
- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스부분을 빠뜨리지 않고 복사해야한다. *상속대입연산자 작성시 주의*
- 클래스의 복사 함수를 구현할때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지마라. (제 3의 함수에다 분리해놓고 양쪽에서 이것을 호출하여 사용)
출처
'C++' 카테고리의 다른 글
[Effective C++] 객체 기반 방식의 자원관리 (0) | 2020.12.02 |
---|---|
[Effective C++] 생성자와 소멸자, 제대로 메모리 관리를 하고 있는가? (0) | 2020.11.19 |
[Effective C++] 객체 생성시 초기화 방법 (0) | 2020.11.18 |
[Effective C++] 왜 const를 사용해야하는가 (0) | 2020.11.18 |
[C++/STL] Ranking System 분석 (0) | 2020.09.25 |