이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 111p ~ 121p
요약
항목 12: 객체의 모든 부분을 빠짐없이 복사하자
- 객체의 안쪽 부분을 캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함수가 딱 둘만 있는 것을 알 수 있다.
- 복사 생성자
- 복사 대입 연산자
- 이 둘을 통틀어 객체 복사 함수(copying function)라고 부른다.
- 객체 복사 함수는 컴파일러가 필요에 따라 자동으로 만들기도 한다.
- 객체 복사 함수의 기본적인 요구 사항은
복사되는 객체가 갖고 있는 데이터를 빠짐없이 복사한다.
이다.
- 객체 복사 함수를 직집 선언한다는 것은 컴파일러가 만든 객체 복사 함수의 기본 동작에 뭔가 마음에 안 드는 것이 있다는 이야기이다.
void logCall(const std::string& func);
class Customer {
public:
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
private:
std::string name;
}
// 복사 생성자
Customer::Customer(const Customer& rhs): name(rhs.name) {
logCall("Customer copy constructor");
}
// 복사 대입 연산자
Customer& operator=(const Customer& rhs) {
logCall("Customer copy assignment operator");
name = rhs.name;
return *this;
}
Customer
클래스는 고객을 나타내며, 이 클래스의 복사 함수는 개발자가 직접 구현했고, 복사 함수들을 호출할 때마다 로그를 남기도록 되어있다.- 현재 코드는 문제가 없어 보이지만, 다음과 같이 클래스의 멤버를 추가하면 문제가 발생하게 된다.
class Date {};
class Customer {
public:
private:
std::string name;
Date lastTransaction;
}
Date
라는 새로운 멤버 변수가 추가되면, 복사 함수의 동작은 완전 복사가 아니라 부분 복사가 된다.- 고객의
name
은 복사하지만lastTransaction
은 복사하지 않는다. - 컴파일러는 이런 사항에 대해 알려주는 정보가 없다.
- 따라서 컴파일러가 자동 생성해주는 객체 복사 함수 사용을 포기했다면, 클래스에 새로운 멤버가 추가될 때마다 복사 함수를 다시 작성해주어야 하는 책임을 가져야 한다.
- 복사 생성자와 복사 대입 연산자에
lastTransaction
을 처리해줄 수 있는 코드를 반드시 작성해야 한다.
- 복사 생성자와 복사 대입 연산자에
- 고객의
- 클래스 상속을 사용하는 경우에는 다음과 같은 문제도 발생한다.
class PriorityCustomer: Customer {
public:
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
int priority;
}
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority) {
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& operator(const PriorityCustomer& rhs) {
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
PriorityCustomer
클래스의 복사 함수는Customer
로부터 상속한 데이터 멤버들의 복사는 진행하지 않고 있다.- 위 코드에서
Customer
부분은 인자 없는 생성자가 호출되어 초기화가 진행되며,Customer
클래스의 인자 없는 생성자는name
과lastTransaction
에 대해 기본적인 초기화를 진행하게 된다. - 복사 대입 연산자는 기본 클래스의 데이터 멤버를 건드릴 시도도 하지 않기 때문에, 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.
- 위 코드에서
- 파생 클래스에 대한 객체 복사 함수를 직접 만든다면, 기본 클래스 부분을 복사에서 빠뜨리지 않도록 각별히 주의해야 한다.
- 기본 클래스의
private
멤버는 직접 건드릴 수 없으니 파생 클래스의 복사 함수 안에서 기본 클래스의 복사 함수를 호출하는 방법을 사용한다.
- 기본 클래스의
// 기본 클래스의 복사 생성자 호출
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs),
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
// 기본 클래스의 복사 대입 연산자 호출
PriorityCustomer& operator=(const PriorityCustomer& rhs) {
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
- 모든 부분을 복사하자의 의미는 다음과 같다.
- 해당 클래스의 데이터 멤버를 모두 복사한다.
- 이 클래스가 상속한 기본 클래스의 복사 함수도 꼬박꼬박 호출한다.
- 클래스의 양대 복사 함수(복사 생성자, 복사 대입 연산자)는 본문이 비슷하게 나오는 경우가 자주 있어서, 한쪽에서 다른 쪽을 호출하게 만들어서 코드의 중복을 줄이고 싶을 수 있겠지만, 복사 함수의 경우에는 그렇게 할 수 없다.
- 복사 대입 연산자에서 복사 생성자를 호출하는 경우
- 이미 만들어진 객체를 다시 생성하는 것이다.
- 특정 조건에서 데이터가 훼손되는 경우가 발생할 수 있다.
- 복사 생성자에서 복사 대입 연산자를 호출하는 경우
- 생성자의 역할은 새로 만들어진 객체를 초기화하는 것이다.
- 대입 연산자의 역할은 이미 초기화가 끝난 객체에게 값을 주는 것이다.
- 따라서 생성중인 객체에게 복사 대입 연산자를 사용하는 것은 아직 초기화도 안 된 객체에게 대입 연산을 하는 것과 같다.
- 중복되는 부분이 있다면, 이를 별도의 private 멤버 함수로 만들어서(init 같은 함수) 이 함수를 호출하는 것이 낫다.
- 복사 대입 연산자에서 복사 생성자를 호출하는 경우
3. 자원 관리
- 프로그래밍 분야에서 자원(resource)이란, 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것을 일컫는다.
- C++에서 가장 흔하게 볼 수 있는 자원은 동적 할당한 메모리가 있으며, 파일 디스크립터, 뮤텍스 락, GUI의 폰트, 브러시 등이 있다.
- 3장의 목표는 순도 100% 객체 기반 방식의 자원 관리를 보여주는 것으로 시작한다.
- C++가 지원하는 생성자, 소멸자, 객체 복사 함수를 사용하는 방법이다.
항목 13: 자원 관리에는 객체가 그만!
// 투자를 모델링해 주는 클래스 설계
// 여러 형태의 투자를 모델링한 최상위 클래스
class Investment {};
// 파생 클래스의 객체를 사용자가 얻기 위한 팩토리 함수 사용
// 이 객체의 메모리 할당 해제는 사용자가 해야함
Investment* createInvestment();
void f() {
Investment* pInv = createInvestment();
// ...
delete pInv;
}
createInvenstment
함수로부터 얻은 투자 객체의 삭제에 실패할 수 있는 경우가 한두 가지가 아니다.- 첫 번째는
//...
부분에서 중간에return
하는 코드가 있는 경우이다. - 두 번째는
createInvestment
호출문과delete
가 하나의 루프 안에 들어 있고,continue
혹은goto
에 의해 갑작스럽게 루프에서 빠져나올 가능성이다. - 마지막으로
//...
부분에서 예외가 발생할 수도 있다.
- 첫 번째는
- 이들의 결과는 메모리가 누출이다.
createInvestment
함수로 얻어낸 자원이 항상 해제되도록 만들 방법은, 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가f
를 떠날 때 호출되도록 만드는 것이다.- 소프트웨어 개발에 쓰이는 상당수의 자원이 힙에서 동적으로 할당되고, 하나의 블록 혹은 함수 안에서만 쓰이는 경우가 잦기 때문에 그 블록 혹은 함수로부터 실행 제어가 빠져나올 때 자원이 해제되는게 맞다.
- 표준 라이브러리를 보면
auto_ptr
이라는 것이 있는데, 이런 용도에 쓰라고 마련된 클래스이다.
- 표준 라이브러리를 보면
#include <memory>
// auto_ptr 사용
void f() {
std::auto_ptr<Investment> pInv(createInvestment());
}
auto_ptr
을 사용하면auto_ptr
의 소멸자를 통해pInv
를 삭제할 수 있게 된다.- 자원 관리에 객체를 사용하는 방법의 중요한 특징이 바로 여기에 있다.
첫째. 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
createInvenstment
함수가 만들어 준 자원은 그 자원을 관리할auto_ptr
객체를 초기화 하는데 쓰이고 있다.- 자원 획득 즉 초기화(Resource Acquisition Is Initialization: RAII)라고 불리는 기법이다.
- 이런 이름이 나온 이유는 자원 획득과 자원 관리 객체의 초기화가 바로 한 문장에서 이루어지는 것이 너무나도 일상적이기 때문이다.
둘째. 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
- 소멸자는 어떤 객체가 소멸될 때(유효범위를 벗어나는 경우가 한 가지 예) 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 된다.
auto_ptr
은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로delete
를 호출해주기 때문에 어떤 객체를 가리키는auto_ptr
의 개수가 둘 이상이면 절대 안된다.- 이런 경우가 발생하면 자원이 두 번 삭제되고, 미정의 동작의 수렁에 빠지게 된다.
- 이런 불상사를 막기 위해
auto_ptr
은 객체를 복사하면(복사 생성자, 복사 대입 연산자 사용) 원본 객체는 null로 만들어 버린다.(소유권의 개념)
std::auto_ptr<Investment> *pInv1(createInvestement());
std::auto_ptr<Investment> *pInv2(pInv1); // 소유권이 pInv2로 이전됨
// pInv1은 null이 됨
pInv1 = pInv2; // 소유권이 pInv1로 이전됨. pInv2는 null이 됨
메모
항목 12: 객체의 모든 부분을 빠짐없이 복사하자
- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 한다.
- 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 말아야 한다. 그 대신, 공통된 동작을 정의하는 제 3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들자.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(132p ~ 142p) (0) | 2024.01.17 |
---|---|
이펙티브 C++(121p ~ 132p) (0) | 2024.01.16 |
이펙티브 C++(99p ~ 111p) (0) | 2024.01.14 |
이펙티브 C++(89p ~ 99p) (1) | 2024.01.12 |
이펙티브 C++(80p ~ 89p) (1) | 2024.01.11 |