2. resource
사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것
가져와서 다 썻으면 해제해야, 즉 놓아 주어야 한다.
동적 할당 메모리(메모리를 할당하고서 해제하지 않으면 memory leak)
3. 자원을 항상 해제되도록 하는 방법
자원을 객체에 넣고
그 자원 해제를 소멸자가 맡도록 하며,
그 소멸자가 반드시 호출되도록 만든다.
4. auto_ptr
포인터와 비슷하게 동작하는 객체(smart
pointer)
가리키고 있는 대상에 대해 소멸자가 자동으
로 delete를 불러주도록 설계되어 있다.
한 객체를 동시에 둘이 가리킬 수 없다. 복사생
성하거나 대입하면 한 auto_ptr은 null이 된다.
std::auto_ptr<Investment>(createInvestment
());
5. 자원 관리에 객체를 사용하는 방법1
자원을 획득한 후에 자원 관리 객체에 넘긴다.
자원 획득 즉 초기화(Resource Acquisition is Initialization:RAII)
자원 획득과 자원 관리 객체의 초기화가 한 문장에서 이루어 진다.
6. 자원 관리에 객체를 사용하는 방법2
자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되므로 실행 제어가 어떤 경위로 블록을 떠나는가
에 상관없이 자원 해제가 제대로 이루어지게 된다.
8. auto_ptr가 답인가?
복사 동작이 이루어지면 이전의 것은 null이 된다.
auto_ptr가 관리하는 객체는 두개 이상의 auto_ptr가 물고 있으면 안 된다.
이 두 가지 특성때문에 동적으로 할당되는 모든 자원에 대한 관리 객체로서 auto_ptr를 쓰는 것은
최선이 아닌 듯하다.
9. 참조 카운팅 방식 스마트 포인터
reference-counting smart pointer(RCSP)
특정한 어떤 자원을 가리키는 외부 객체의 개
수를 유지하고 있다가 그 개수가 0이 되면 해
당 자원으로 삭제하는 스마트 포인터
garbage collection 방식과 유사
std::tr1::shared_ptr<Investment>pInv(crea
teInvestment());
10. reference-counting smart pointer의 특성
std::tr1::shared_ptr<Investment>pInv1(createInvestment());
std::tr1::shared_ptr<Investment>pInv1(pInv2); // auto_ptr과 달리 pInv1, pInv2가
// 동시에 객체를 가리킬 수 있다.
pInv1 = pInv2; // 변화없음
11. auto_ptr, tr1::shared_ptr의 공통 특성
소멸자 내부에서 delete 연산자를 사용한다.
delete [] 연산자가 아니다.
즉, 동적 할당한 배열에 auto_ptr, tr1::shared_ptr을 사용하면 안 된다.
std::auto_ptr<std::string>aps(new std::string[10]); // error
std::tr1::shared_ptr<int> spi(new int[1024]);; // error
12. RAII 객체가 복사될 때 어떻게 해야하나?
복사를 금지한다.
class Lock : private Uncopyable {
public :
…
};
13. RAII 객체가 복사될 때 어떻게 해야하나?
관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 해제하면 안 되는 경우
tr1::shared_ptr를 이용?
tr1::shared_ptr를 참조 카운트가 0이 될 때 자신이 가리키고 있던 대상을 삭제하도록 되어있기 때문
에 안 된다.
tr1:shared_ptr는 „삭제자(delete)‟ 지정을 허용한다.
14. shared_ptr 삭제자 활용
class Lock {
public :
explicit Lock(Mutex *pm) // shared_ptr를 초기화하는데, 가리킬 포인터로
: mutexPtr(pm, unlock // Mutex 객체의 포인터를 사용하고 삭제자로 unlock 함수를 사용
{
lock(mutexPtr.get());
)
private :
std::tr1::shared_ptr<Mutex>mutexPtr;
}
15. RAII 객체가 복사될 때 어떻게 해야하나?
관리하고 있는 자원을 진짜로 복사한다.
깊은 복사(deep copy)
16. RAII 객체가 복사될 때 어떻게 해야하나?
관리하고 있는 자원의 소유권을 옮긴다.
17. 명시적 변환(explicit conversion)
tr1::shared_ptr 및 auto_ptr은 명시적 변환을 수행하는 get이라는 멤버 함수를 제공한다.
get 함수를 사용하면 각 타입으로 만든 스마트 포인터 객체에 들어있는 실제 포인터를 얻어낸다.
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment *pi);
// int days = daysHeld(pInv); // error
int days = daysHeld(pInv.get());
18. 자원 접근과 명시적, 암시적 변환
안전성만 따지만 명시적 변환이 대체적으로 낫다.
하지만 사용자 편의성을 놓고 보면 암시적 변환이 괜찮다.
“맞게 쓰기에는 쉽게, 틀리게 쓰기에는 어렵게“
22. new로 생성한 객체는 스마트 포인터로…
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
// processWidget(new Widget, priority()); error!
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
std::tr1::shared_ptr의 생성자는 explicit로 선언되어 있기 때문에 „new Widget‟표현식에 의해 만들어
진 포인터가 tr1::shared_ptr 타입의 객체로 바꾸는 암시적인 변환이 없다.
23. 매개변수 호출 순서에 따른 위험성
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
연산이 실행되는 순서
priority -> “new Widget” -> tr1::shared_ptr?
반드시 위의 순서대로 되지는 않는다.
priority가 몇 번째로 호출될지는 모른다.
문제는 priority 호출부분에서 error가 발생하면 “new Widget”으로 만들었던 포인터가 유실될 수 있다.
24. 해결책
std::tr1::shared_ptr<Widget> pw(new widget); // new로 생성한 객체를 스마트 포인터에
processWidget(pw, priority()); // prority 호출에 문제가 발생해도 포인터 유실 없음
// memory leak 방지
그래서 new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장을 만들어야 한다.
26. 제대로 쓰기 쉽게, 엉터리로 쓰기 어렵게
class Date {
public :
Date(int month, int day, int year);
…
};
Date d(30, 3, 1995); // 월, 일이 바뀜
Date d(3, 40, 1955); // 일이 이상한 값
struct Day {
explicit Day(int d) : val (d) {}
int val;
};
27. class Date {
public:
Date(const Month& m, const Day& d,
const Year& y);
…
};
Date d(30, 3, 1955) // 자료형이 틀렸다.
Date d(Day(30), Month(3), Year(1995));
// 자료형이 틀렸다.
Date d(Month(3), Day(30), Year(1995));
29. tr1::shared_ptr의 특성 이용1
생성 시점에 자원 해제 함수(„삭제자„)를 엮을 수 있다.
std::tr1::shared_ptr<Investment>pInv(0, getRidOfInvestment); // error
std::tr1::shared_ptr<Investment*>pInv(static_cast<Investment*>)(0), getRidOfInvestmentl
// 0은 int, tr1::share_ptr가 요구하는 것은 Investmet* 타입의 실제 포인터이기 때문에 static_cast
30. tr1::shared_ptr의 특성 이용2
교차 DLL 문제 해결
객체 생성 시에 어떤 동적 링크 라이브러리의 new를 썼는데 그 객체를 삭제할 때는 이전의 DLL과
다른 DLL에 있는 delete를 섰을 경우
tr1::shared_ptr의 기본 삭제자는 tr1::shared_ptr이 생성된 DLL과 동일한 DLL에서 delete를 사용하
도록 만들어져 있다.
31. 좋은 클래스 설계
새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
객체 초기화는 객체 대입과 어떻게 달라야 하는가?
새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
기존의 클래스 상속 계통망에 맞출 것인가?
어떤 종류의 타입 변환을 허용할 것인가? (implicity, explicit)
어떤 연산자와 함수를 두어야 의미가 있을까?
32. 좋은 클래스 설계
표준 함수들 중 어떤 것을 허용하지 말 것인가?
새로운 타입의 멤버에 대한 접근권한은 어느 쪽에 줄 것인가?
„선언되지 않은 인터페이스‟로 무엇을 둘 것인가?
새로 만드는 타입이 얼마나 일반적인가?
정말로 필요한 타입인가?
33. pass_by_value?
class Person {
public :
Person();
virtual ~ Person();
…
private :
std::string name;
std::string address;
};
class Student : public Person {
public :
Student();
~Student();
…
private:
std::string schoolName;
std::string schoolAddress;
};
34. pass-by-value? 비용이 너무 크다.
bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
plato로부터 매개변수 s 초기화시키기 위해
Sudent 복사 생성자 호출
s는 validateStudent가 복귀할 때 소멸
결국, 복사 생성자 1번, 소멸자 1번
Student객체가 생성될 때마다 string 객체 2개
생성
Student객체는 Person객체 파생이므로
Person 생성, string 객체 2개 생성
소멸도 생성과 마찬가지…
35. reference-to-const
호출에 따르는 비용을 줄일 수 있다.
bool validateStudent(const Student& s); // 새로 만들어지는 객체 같은 것이 없기 때문에
// 생성자와 소멸자가 전혀 호출되지 않는다.
// const로 전달된 student 객체를 보호
37. 복사손실 문제
매개변수 w 생성된다.
하지만 매개변수가 Windows 객체이므로, 이 객체를 상속받은 WindowWithScrollBars 객체의 고유
정보들은 손실된다.
결국, 호출하는 display()함수도 Windows 객체의 것이다.
38. 복사손실 문제 해결
void printNameAndDisplay(const Windows& w)
{
std::coutn << w.name();
w.display();
}
w를 참조자로 전달하면 w는 어떤 종류의 윈도우가 넘겨지더라고 그 윈도우의 성질을 갖게 된다.
39. 함수에서 객체 반환할때 참조자 반환하지말자
class Rational {
public :
Rational(int numerator = 0, int denominator = 1);
…
private :
int n, d;
friend
const Rational
operator*(const Rational& lhs, const Rational rhs);
};
43. static을 이용한 객체 반환
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result; // 반환할 참조자가 가리킬 정적객체
result = …;
return result;
}
정적객체는 공유하는 값이므로 활용에 제한이 있을 뿐더러, 스레디 안전성 문제도 있다.
44. 객체를 반환하는 올바른 방법
inline const Rational operator*(const Rational& 1hs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs,d * rsh.d);
}
새로운 객체를 반환하게 만드는 것이 정도이다.
45. 데이터 멤버가 선언될 곳은 private
private으로 선언하면 데이터 멤버에 접근하기 위해서 멤버 함수를 써야 한다.
어떤 데이터 멤버는 public이고, 어떤 데이터 멤버는 private이라면 일관성이 사라진다.
함수를 통해서만 데이터 멤버에 접근할 수 있도록 구현해 두면, 데이터 멤버를 나중에 계산식으로
대체할 수 있을 것이다.(캡슐화, encapsulation)
public 데이터 멤버가 있고, 이것을 제거한다고 가정해보자. 캡슐화가 얼마나 중요한지 알 수 있다.
protected도 마찬가지로 안전하지 않다.
결국은 캡슐화를 고려하였을 때, 데이터 멤버는 private에 선언하는 것이 답이다.
47. 비멤버 버전
void clearBrowser (WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
멤버 버전이 좋을까? 비멤버 버전이 좋을까?
48. 비멤버 버전의 장점
캡슐화
피키징 유연성(packaging flexibility)가 높아진다.
컴파일 의존도도 낮춘다.
확장성이 높아진다.
결국, 비멤버 방법이 멤버 함수보다 여러모로 낫다.
49. namespace를 활용한 비멤버 함수
namespace WebBrowserStuff {
class WebBrowser {…};
void clearBrowser {WebBrowser& wb};
…
}
50. namesapce를 활용한 비멤버 함수의 장점
namespace는 class와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있다.
다시 말해, 필요할 때 기능을 불러다 쓰면 된다. WebBrowser객체가 없어도 쓸 수 있다.
응용도가 높은 편의 함수들과 응용도가 낮은 편의 함수를 구분하여 관리하면 컴파일 의존성에서 비
교적 자유로울 수 있다.
클래스 멤버 함수로 사용하게 되면 기능을 쪼개는 것이 불가능하다.
편의 함수 전체를 여러 개의 헤더 파일에(그러나 하나의 namespace) 나누어 놓으면 편의 함수 집
합의 확장도 쉬워진다. 해당 네임스페이스에 비멤버 비프렌드 함수를 추가하면 끝.
51. 타입변환이 모든 매개변수에 적용된다면 비멤버 함수로
// 유리수를 나타내는 클래스
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
const Rational operator*(const Rational& rhs) const;
private:
…
};
52. Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // OK
result = result * oneEighth; // OK
result = oneHalf * 2; // OK
result = 2 * oneHalf; // error
result = oneHalf.operator*(2); // OK
result = 2.operator*(oneHalf); // error
암시적 타입 변환에 대해 매개변수가 먹혀들려면
매개변수 리스트에 들어있어야 한다.
53. 암시적 변환을 가능하게 하기위해서
class Rational {
…
};
const Rational operator*(const Rational& lhs, const Rational& rhs) // 비멤버 함수로 만들었다.
{ // 모든 인자에 대해 암시적 타입 변환
return Raional(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;
54. 예외를 던지지 않는 swap
// 표준 라이브러리에서 제공하는 swap
namespce std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
55. pimpl 관용구
class Widget Impl {
public :
…
private :
int a, b, c;
std::vector<double> V;
…
};
class Widget {
public :
Widget (const Widget& rhs);
Widget& operator = (const Widget& rhs)
{
…
*pImpl = *(rhs.pImpl);
…
}
…
private :
WidgetImpl *pImpl;
};
56. 표준 swap 알고리즘의 빈틈
위의 Widget 객체를 직접 맞바꾼다면? pImpl포인터만 바꾸는 것이 아니다.
Widget 객체 세 개를 복사하고, WidgetImpl 객체 세 개도 복사한다.
비효율적이다.
57. std::swap 특수화
namespace std {
template<> // 완전 템플릿 특수화를 컴파일러에게 알려 준다.
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl);
}
}
58. class Widget {
public :
…
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
…
};
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
59. 함수는 부분 특수화가 되지 않는다.
// Widget이 클래스가 아닌 클래스 템플릿
template<typename T>
class WidgetImpl {…};
template<typename T>
class Widget {…};
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a,
Widget<T>& b)
{ a.swap(b); }
}
위 코드는 적법하지 않다.
C++은 클래스 템플릿의 부분 특수화는 인정
함수 템플릿에 대해서는 허용하지 않는다.
60. 함수 템플릿의 부분적 특수화는 오버로드
namespace std {
template<typename T>
void swap(Widget<T>& a,
Widget<T>& b)
{ a.swap(b) };
}
위의 코드는 유효하지 않다.
std 내의 템플릿에 대한 완전 특수화는 OK
std에 새로운 템플릿을 추가하는 것은 OK 아
님
61. 템플릿 전용 버전으로 만들자
namespace WidgetStuff {
…
template<typename T>
class Widget {…};
…
template<typename T> // 비멤버 swap 함수
void swap(Widget<T> a, Widget<T> b) // 이번엔 std 네임스페이스의 일부가 아님
{
a.swap(b);
}
}
인자 기반 탐색(argument-dependent lookup) 혹은 쾨니그 탐색(Koenig lookup)
63. T 타입 전용버전이 우선 호출, 없으면 일반형
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // std::swap을 이 함수 안으로 끌어온다.
…
swap(obj1, obj2); // T 타입 전용의 swap을 호출한다.
}
64. 정리하면
std::swap이 특정 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자.
멤버 swap을 제공했으면, 이멤버를 호출하는 비멤도 swap도 제공하자.
클래스에 대해서는, std::swap도 특수화 하자.
사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후 네임스페이스 한
정 없이 swap을 호출하자.
사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하지만 std에 어떤 것이라도 새로
추가할 수는 없다.