3. Item 5: C++이 몰래 만든 함수들
• 직접 선언하지 않으면 저절로 선언하는 함수들
• 기본 생성자
• 복사 생성자
• 복사 대입 연산자
• 소멸자
• 위의 함수가 필요한 경우 컴파일러가 선별하여 생성
4. Item 5: C++이 몰래 만든 함수들
• 기본 생성자와 소멸자
• 컴파일러의 뒷 작업(배후의 코드)을 처리하는 공간
• 기본 클래스, 비정적 데이터 멤버의 생성자 / 소멸자 호출
• 부모 클래스의 소멸자가 가상 소멸자가 아니라면
• 자식 클래스의 소멸자도 비 가상 소멸자로 만들어 진다
• 유저가 만든 생성자가 하나라도 있다면
• 컴파일러는 기본 생성자를 만들지 않는다.
5. Item 5: C++이 몰래 만든 함수들
• 복사 생성자 / 복사 대입 연산자
• 원본 객체의 비정적 데이터를 사본 객체에 복사하는 기능
• 자동으로 만들어지는 복사기능
• 멤버 변수가 사용자 정의 타입이면 복사 생성자/대입 연산자 호출
• 멤버 변수가 Built-in 타입이면 비트를 그대로 복사
• 상수/참조 타입 멤버변수가 있으면 대입 연산자 안 만들어줌
• 부모의 대입 연산자가 private면 대입 연산자 안 만들어 줌
7. Item 6: 자동 생성 함수 봉인 방법
• 복사되지 않는 객체 만드는 방법
• 복사 생성자, 대입 연산자의 자동 생성을 봉인한다.
• private로 선언하고, 정의하지 않는다.
• private이므로 외부에서 접근 불가능! (friend는?)
• 정의하지 않았으므로 복사 사용하면, 링킹 에러
• 컴파일 시점에 원천 봉쇄하고 싶다면?
• 복사 생성자/ 대입 연산자를 private로 하는 복사 방지 클래스를 만든다.
• 복사 방지 클래스를 상속받는다.
• 최적화/다중상속 이슈는 뒤로 미룸 ^^
9. Item 7: 부모클래스에 가상 소멸자 놓기
• C++ 규정 saying
• “부모 클래스 포인터를 통해서 자식 클래스가 삭제될 때,
부모 클래스에 가상 소멸자가 없다면 Undefined Behavior”
• 일반적인 상황
• 부모 클래스의 소멸자가 호출
• 자식 클래스의 소멸자 호출되지 않음
• 자식 클래스의 종료 처리 및 해제 동작 안함 ERROR & LEAK
10. Item 7: 부모클래스에 가상 소멸자 놓기
• 부모님께 가상 소멸자를 넣어드립시다
• 가상함수는 동적 타입을 추적하여 올바른 함수를 호출하게 한다.
• 가상 소멸자도 동적 타입에 올바른 동작을 수행한다.
• 자식 소멸자 부터 상속 순으로 최상위 부모 소멸자 까지 호출됨
11. Item 7: 부모클래스에 가상 소멸자 놓기
• 그렇다고 아무 때나 가상함수를 남발하면 안 된다.
• 부모가 아니거나, 부모 역할(virtual 함수)이 필요 없는 경우
• virtual을 쓰는 순간 클래스에는 가상함수 테이블,
인스턴스에는 가상함수 포인터가 생긴다.
• 불필요한 메모리 사용
• 부모 클래스에 가상함수가 있을 때, 가상 소멸자를 사용하자.
12. Item 7: 부모클래스에 가상 소멸자 놓기
• 가상 소멸자가 없는 클래스를 상속받지 말자.
• STL이나 std::string등에는 가상 소멸자가 없으므로 주의
• 추상 클래스를 만들 때 이렇다할 가상함수가 없다면?
• 순수 가상 소멸자만 만들어 두면 된다.
• 가상 소멸자는 최상위까지 반드시 호출되므로 정의도 구현해야한다.
• 모든 부모클래스가 다형성을 지원하는 것은 아니다.
• 앞에서도 말했지만, 가상 소멸자를 선택할 때 주의를 기울여라
14. Item 8: 소멸자에서 try catch
• 만약 여러 개의 소멸자에서 동시에 예외를 던진다면?
• 클래스 컨테이너에서 반복자를 통해 소멸자를 호출하는 경우
• Catch는 저 멀리, 활성화된 예외가 축적된다.
• C++에서 감당하기 어려운 상태가 되어버림.
• 불완전 종료 or Undefine Behavior…
15. Item 8: 소멸자에서 try catch
• 소멸자 내부에서 try하려면 catch도 안에서 해라.
• 오류나면 그대로 종료하는 방법 std::abort()
• 문제는 없지만 try catch를 쓰는 의미가?
• 오류나면 로그만 찍고 계속 진행하는 방법
• 무엇이 잘못되었는지 확실히 알기 어렵다.
• 오류 이후에도 프로그램이 올바르게 실행 되는 것이 보장돼야 한다.
• Catch는 오류에 대해서 적절한 대처를 하기 위함인데
어느 쪽도 적절해보이지 않는다.
16. Item 8: 소멸자에서 try catch
• 예외를 처리하는 부분을 소멸자에 넣지 마라
• 다른 함수로 빼서 사용자에게 맡긴다.
• Ex) Delete하기 전에 Release를 반드시 하도록 요구.
• 에러나면 사용자 탓^^
18. Item 9: 생성/소멸자에서 가상함수
• 생성자에서 가상함수를 사용하면 안 된다!
• 상속받은 클래스의 생성자/소멸자를 호출하면
• 최상위 부모클래스부터 해당 클래스까지 정해진 순서대로 생성/소멸자가 호출
• 상위 클래스의 생성자/소멸자 호출시점에서
• 해당 클래스는 현재 진행중인 호출 스택의 부모 타입 처럼 인식된다.
• 런타임 타입 추론을 이용하는 함수들에서도 동일하게 처리됨
• dynamic_cast , typeid 등
• 때문에 이 시점에 가상함수를 쓰면 해당 부모 타입의 가상함수가 호출된다.
19. Item 9: 생성/소멸자에서 가상함수
• 생성/소멸자 호출 시점에서 객체의 타입을 변경시키는 이유
• 생성자의 경우
• 자식 클래스의 멤버는 아직 초기화되지 않은 상태.
• 소멸자의 경우
• 자식 클래스의 멤버 변수는 이미 해제된 상태
• 여기서 멤버 함수를 호출하는 것은 위험하다.
20. Item 9: 생성/소멸자에서 가상함수
• 대처 방법
• 생성자에서는 비 가상 함수만 사용한다.
• 필요한 초기화 정보를 자식클래스에서 부모클래스로 올려준다.
class ChildClass : public ParentClass
{
public:
ChildClass(int arg)
: ParentClass(createLogString(arg))//초기화 리스트를 통해서 부모 생성자에 인자 전달
{...}
protected:
static std::string createLogString(int arg);
/* 정적 함수를 사용하면 멤버변수와 독립적으로 사용가능,
초기화 문제 해결. 생성자 호출 전에 호출가능 */
};
22. Item 10: 대입 연산자의 리턴 형식
• 대입 연산은 chaining 이 가능 (일종의 convention)
• int x, y, z;
• x = y = z = 15; //x = (y = (z = 15));
• 대입 연산의 결과(좌변)가 반환 되어야 가능
• T& operator=(const T& rhs){ … return *this }
• 모든 형태의 대입 연산자에서 동일하게 적용
• +=, -=, *= …
24. Item 11: 자기 대입 처리
• 자기 대입
• 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것
• a[i] = a[j]; *px = *py; //충분히 자기대입 가능한 상황
• 중복참조에서 발생하는 자기대입의 가능성을 고려하자
• 자원 관리 객체를 사용할 때 특히 주의
Widget& operator=( const Widget& rhs ) //위험한 대입연산자의 예
{
delete m_Data;
m_Data = new Data(*rhs.m_Data);
...
return *this;
}
25. Item 11: 자기 대입 처리
• 대입연산의 예외에 대한 안정성을 구축하자.
• 상세한 내용은 item 29로 가시오
• 올바른 순서를 갖추는 것 만으로 자기대입 처리를 가능하게!
Widget& operator=( const Widget& rhs )
{
Data* tempData = m_Data; //원래 데이터를 임시 저장
m_Data = new Data(*rhs.m_Data); //사본으로 멤버변수 대체
delete tempData; //임시 데이터 삭제
...
return *this;
}
26. Item 11: 자기 대입 처리
• 같은 객체인지 체크하고 싶다고?
• 쓸데없는 복사가 일어나는 건 맞다.
• 하지만 자기 대입이 일어날 확률은 vㅔ리 낮다.
• 동일성 체크는 대입 연산 할 때마다 해야 한다.
• 코드 비대, 연산 처리 분기발생, CPU 선행처리, 파이프라이닝 효율 저하…
• 체크의 비용 >>>> 가끔 복사 비용
27. Item 11: 자기 대입 처리
• 비기 copy & swap
• void Swap(Widget& rhs) // *this와 rhs의 데이터 스왑
• Swap에서 큰 비용 없이 데이터 전환이 가능하다면
• 문제 없고 깔끔한 자기대입 처리가능
Widget& operator=( const Widget& rhs )
{
Widget temp(rhs); //rhs의 사본을 생성하고
Swap(temp); //사본과 this의 데이터를 스왑
...
return *this;
}
29. Item 12: 복사는 확실하게
• 복사 생성자/대입 연산자를 직접 만들어 쓰는 경우
• 복사하지 않은 멤버변수가 있어도 컴파일러가 말해주지 않는다.
• 아래 항목을 충족시켰는지 체크할 것
• 해당 클래스의 데이터 맴버를 모두 복사했는가?
• 상속한 모든 부모클래스의 복사 생성자/ 대입 연산자를 호출했는가?
30. Item 12: 복사는 확실하게
• 상속받은 부모 클래스의 복사 생성자/대입연산자 호출 방법
• 초기화 리스트로 부모 객체의 복사 생성자 호출
• 부모 객체의 대입 연산자 직접 호출
ChildClass(const ChildClass& rhs)
: ParentClass(rhs)
{
//Do something
}
ChildClass& operator=( const ChildClass& rhs )
{
ParentClass::operator=( rhs );
//Do Something
return *this;
}
31. Item 12: 복사는 확실하게
• 복사 생성자 대입 연산자 서로 사용할 수 있을까?
• 비슷한 코드니까 둘 중 하나만 만들고 가져다 쓰면 안될까?
• 대입 연산자에서 복사 생성자를 호출하는 경우
• 이미 만들어진 객체를 다시 생성한다?
• 복사 생성자에서 대입 연산자를 호출하는 경우
• 대입 연산은 이미 초기화된 객체에만 사용가능
• 아직 초기화 되지 않은 객체를 대입 연산한다?
• 겹치는 부분을 별도의 멤버함수로 처리할 수는 있다.