멤버 함수 보다는 비멤버 비프렌드 함수와 더 가까워지자.


웹브라우저를 나타내는 클래스가 있다.

이 클래스에는 캐시를 비우는 함수, URL 기록을

없애는 함수, 쿠키를 제거하는 함수가 있다.


이 세 동작을 한꺼번에 하고 싶은 사용자를 위해

세 함수를 불러주는 함수도 준비해 둘 수 있을 것이다.




class Web{

private:

...

public:

...

void clearEverything();

}


위와 같이 멤버함수로 제공해도 되지만,

비멤버 함수로 제공해도 된다.


void clearWeb(Web& wb){

wb.fct1();

wb.fct2();

...

}


멤버 버전인 경우 Web 클래스의 private 멤버에

접근할 수 있다. 따라서 캡슐화의 정도가 낮아진다.

반면 비멤버 비프렌드 함수일 경우 Web클래스의

어떠한 private 멤버에 접근할 수 없다.

또한 패키징 유연성과 확장성을 높히는 동시에

컴파일 의존도도 낮출 수 있다.




주의해야 할 점은 함수는 어떤 클래스의 

비멤버가 되어야 한다는 말이 그 함수는

다른 클래스의 멤버가 될 수 없다라는 의미가

아니라는 것이다. 위의 clearWeb라는 함수는

다른 클래스의 정적 멤버함수로 만들어도 된다.

(Web클래스의 멤버만 아니면 됨)




더 나아간 방법 --> 같은 네임스페이스 안에 두는 것


namespace WebStuff {

class Web { ... };

void clearWeb(Web& wb);

...

}


네임스페이스는 클래스와 달리 여러 개의

소스 파일에 나뉘어 흩어질 수 있다.

clearWeb같은 함수는 편의상 준비한 함수들

이기 때문에, 이 함수가 없다해도 사용자는

각각의 함수들을 따로 호출하면 된다.




편의 함수가 많이 생길 수 있는 클래스라면

연관이 있는 편의 함수를 하나의 헤더 파일에

몰아서 선언하고, 같은 네임스페이스에 넣어주자.


//"web.h"

namespace WebStuff {

class Web { ... };

...

}


//"bookmark.h"

namespace WebStuff {

...

}


//"cookies.h"

namespacce WebStuff {

...

}


이렇게 하면 필요한 기능에 대해서만

include해서 사용하면 되기 때문에

필요없는 기능들에 대한 컴파일 의존도를

낮출 수 있다. 클래스의 멤버함수일 경우

이런 식으로 기능을 쪼개는 것 자체가 불가능하다.

하나의 클래스는 그 전체가 통으로 정의되야 하고

여러 조각으로 나눌 수가 없기 때문이다.




편의 함수 전체를 여러 개의 헤더 파일에

나누어 놓으면 확장도 손쉬워진다.

해당 네임스페이스에 비멤버 비프렌드 함수를

원하는 만큼 추가해주면 끝난다.




POINT!!

멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록하자.

캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어난다.

데이터 멤버가 선언될 곳은 private 영역임을 명심하자.




1. 문법적 일관성

데이터 멤버가 public이 아니라면 접근은

함수로 해야할 것이고, 공개 인터페이스에 있는 것들이

전부 함수뿐이라면, 그 클래스에 멤버에 접근하고 싶을 때

괄호를 붙여야 하는지 말아야 하는지 고민할 필요가 없다.




2. 데이터 멤버의 접근성에 대해 훨씬 정교한 제어를 할 수 있다.

멤버 함수를 통한 접근은 접근 불가, 읽기 전용, 읽기 쓰기 접근을

내가 직접 구현할 수 있다. 심지어는 쓰기 전용 접근까지도




3. 캡슐화

자동차가 지나가는 속도를 모니터링 하는 클래스가 있다.

class speedData{

...

public:

void Add(int speed);

double average() const;

...

}


평균 값을 구하는 average 함수를 구현하는 방법은 두 가지이다.

첫 번째, 평균값을 넣어두는 데이터 멤버를 반환하는 방법

두 번째, 함수가 호출될 때 마다 평균값을 계산해서 반환하는 방법

이렇듯 평균값 접근에 멤버 함수를 통해 하게되면

(평균값을 캡슐화 하게되면) 내부 구현을 이렇게 혹은 저렇게

바꿀 수 있게 되고, 사용자는 기껏 해봤자 컴파일만 다시하면 된다.




4. 구현상의 융통성을 전부 누릴 수 있다.

데이터 멤버를 읽거나 쓸 때 다른 객체에 알림을

보낸다든지, 클래스의 불변속성 및 사전조건, 사후조건을

검증한다든지, 스레딩 환경에서 동기화를 건다든지 하는 일이다.

(C# 에서는 프로퍼티라 한다.)




5. 불변속성

사용자로부터 데이터 멤버를 숨기면, 클래스의 불변속성을

항상 유지하는 데 절대로 소홀해질 수 없게 된다. 불변속성을

보여줄 수 있는 통로가 멤버 함수밖에 없기 때문이다.


캡슐화는 현재의 구현을 나중에 바꿀 수 있다. --> 3번 참고

데이터 멤버가 public으로 되어있다면 캡슐화되지 않았다는 뜻이고

현재의 구현을 나중에 바꾸게 되면 코드가 깨지게 된다.


public으로 데이터 멤버를 선언할 경우 이 데이터 멤버를

변경하고자 할 때, 이 데이터를 쓰고 있는 모든 부분을

수정해야 할 것이며, protected 데이터 멤버라고 해도

상속받는 모든 클래스의 코드가 깨지게 될 것이다.




POINT!!

1. 데이터 멤버는 private로 선언하자,

2. 접근 수준은 private(캡슐화 제공) 그리고 나머지 (캡슐화 없음)으로 구분하자.

함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자.


유리수를 나타내는 클래스가 있고 이 클래스에는

두 유리수를 곱하는 멤버 함수가 선언되어 있다.


Rational {

private:

int n ,d;

public:

Rational(int _n, int _d);

...

const Rational operator*(const Rational& lhs, const Rational& rhs);

};




Rational operator*(..); 위 함수에서는

참조자가 아닌 객체를 반환하고 있다.

이 함수가 참조자를 반환하도록 만들어졌다면,

이 함수가 반환하는 참조자는 반드시 이미 존재하는

Rational 객체의 참조자여야 한다.


참조자를 반환한다면 객체의 생성과 소멸에 드는

비용을 조금이라도 줄여지지 않을까?

그렇다면 참조자를 반환해보자.


const Rational& operator*(const Rational& lhs, const Rational& rhs){

Rational result (lhs.n * hrs.n, lhs.d * rhs.d );

return result;

}




참조자를 반환하려면 그 객체가 존재해야 한다고 말했다.

따라서 어차피 함수 안에서 객체가 하나 만들어진다.

즉, 객체의 생성과 소멸에 드는 비용을 지불한다는 말이다.


이보다 더 큰 문제는 result는 지역 객체이다.

함수가 끝나면 이 객체는 소멸된다.

따라서 이 함수는 소멸된 객체의 참조자를

리턴하고 있는 것이다. 이 참조자가

가리키고 있는 놈은 존재하지 않는다.




그렇다면 스택이 아닌 힙에 생성하고 참조자를 반환하면 어떤가?

const Rational& operator*(const Rational% lhs, const Rational& rhs){

Rational *result = new Rational (lhs.n * hrs.n, lhs.d * rhs.d);

return *result;

}


여전히 생성자가 호출되어야한다.

그리고 new로 할당한 메모리를 누가 해제를 해주는가?

operator* 로부터 반환되는 참조자 뒤에 숨겨진

포인터에 대해서는 사용자가 어떻게 접근할 방법이 없다.

즉, 자원누출이 심각한 코드가 된다.




스택에 할당하든 힙에 할당하든 생성자를 한번은 호출한다.

그렇다면 정적 객체로 함수 안에 정의해 놓고 참조자를 반환해보자.

const Rational& operator*(const Rational% lhs, const Rational& rhs){

static Rational result;

result = ...;

return result;

}




operator==가 선언되었고 a,b,c,d 모두 Rational 객체

if((a*b) == (c*d)); 

위의 표현식은 항상 값이 true이다.


왜? operator*은 정적 객체의 참조자를 반환한다.

둘의 값이 같지 않을 수가 없다.


이쯤되면 정적 배열을 써보는 것은 어떨까?

함수가 불릴 때마다 다른 객체를 반환해야 하기 때문에

n번의 호출이 있다면 n개의 배열이 있어야 한다.

비록 한번이지만 생성자가 n번 호출되고 소멸자가 n번 호출된다.

그리고 객체에 값을 어떻게 넣을 것인가?




그냥 새로운 객체를 반환하도록 하자!

inline const Rational operator*(const Rational& lhs, const Rational& rhs){

Rational result (lhs.n * hrs.n, lhs.d * rhs.d );

return result;

}




이 코드에도 반환 값을 생성하고 소멸시키는 비용이 들지만

올바른 동작에 지불되는 작은 비용이다. 또한 몇몇 조건하에서는

최적화 메커니즘에 의해 operator*의 반환 값에 대한 생성과

소멸 동작이 안전하게 제거 될 수 있다. (반환 값 최적화 : RVO)


POINT!!

값에 의한 반환이 최선이라면 그냥 그렇게 하자.

값에 의한 전달 보다는 상수객체 참조자에 의한 전달 방식을

택하는 편이 대개 낫다.


기본적으로 C++는 함수로부터 객체를 전달받거나

함수에 객체를 전달할 때 값에 의한 전달 방식을 사용한다.

실제 인자의 사본을 통해 초기화 되며 그 함수가 반환한

값 또한 사본을 돌려받는다. 이는 복사생성자에 의해 수행되며

때에 따라서는 고비용 연산이 되기도 한다.


class AAA{             --> Base 클래스

private:

string name;

string address;

public:

AAA();

virtual ~AAA();

}


class BBB : public AAA {  --> Drived 클래스

private:

stirng bName;

stirng bAddress;

...

}


bool validate(BBB b); --> BBB값으로 전달 받는 함수


BBB bbb;

bool check = validate(bbb);




bbb로부터 매개변수 b를 초기화 시키기 위해

1. BBB는 Drieved 클래스이기 때문에 Base 

   먼저 생성 되어야 한다. AAA의 복사생성자 호출

2. AAA 클래스 안의 stiring 생성자 호출.

3. string이 두개이기 때문에 또 생성자 호출.

4. BBB의 복사생성자 호출

5. BBB객체의 string 생성자 호출

6. string이 두개이기 때문에 또 생성자 호출.


7. BBB의 소멸자 호출

8. string 소멸자 호출

9. stirng 소멸자 호출

10. AAA의 소멸자 호출

11. string 소멸자 호출

12. stirng 소멸자 호출




상당히 고비용이다. 따라서 우리는

상수객체에 대한 참조자로 전달하게 만들자.


bool validate(const BBB& b);


새로 만들어지는 객체는 없기 때문에

생성자와 소멸자가 전혀 호출되지 않는다.

값을 전달 받는 validate함수는 전달되는 BBB에 

어떠한 변화가 생겨도 그 변화로부터 안전하게

보호를 받는다. (사본이 넘겨지기 때문에)

이번에는 참조에 의한 전달 방식을 사용했기 때문에

원본을 그 변화로 부터 지켜줘야 한다. --> const




또한 참조에 의한 전달 방식은 복사손실 문제도 해결해준다. 

복사 손실이란?

Drived 클래스 객체가 Base 클래스 객체로서

전달되는 경우가 있다. (함수의 매개변수로 넘길 때)

이 때 객체가 값으로 전달되면 Base 클래스의

복사 생성자만이 호출되고, Drived 클래스의

특징은 잘려나가게 되는데 이를 복사 손실이라 한다.


class AAA{   --> Base 클래스

...

public:

string GetName() const;

virtual void show() const;

};


class BBB : public BBB{  --> Drived 클래스

...

public:

...

virtual void show() const;

};


void print(AAA a){  --> AAA값을 받는 함수

cout<<a.name;

a.show()

}


BBB b;

print(b);




위의 경우 값으로 전달되어 복사가 된다.

Drived클래스의 정보는 잘려나가게 된다.

따라서 print함수의 show()는 절대 BBB의 것이 될 수 없다.


void print(const AAA& a);

print(b);


상수 객체 참조자에 의한 전달 방식을 사용한다면

이러한 문제를 말끔하게 해결 할 수 있다.

참조자를 전달한다는 것은 결국 포인터를

전달한다는 것과 일맥상통하기 때문이다.




하지만 참조자에 의한 전달이 어떤 상황에서든

빛을 발하는 것은 아니다. 기본제공 타입의 경우에는

값으로 넘기는 편이 더 효율적일 때가 많다.

기본제공 타입 외에도 STL의 반복자, 함수객체도 마찬가지.

따라서 반복자와 함수 객체를 구현할 때는 반드시

1. 복사 효율을 높일 것과

2. 복사손실 문제에 노출되지 않도록

만드는 것이 필수이다.




기본제공 타입은 작다. 따라서 타입 크기만

작으면 모두 값에 의한 전달이 더 효율적인가?

--> 크기가 작아도 비용이 비쌀 수 있다.


만약 복사의 비용이 그렇게 비싸지 않더라 하더라도

컴파일러 중에는 기본제공 타입과 사용자 정의 타입을

아예 다르게 취급하는 것들이 있다. 기본제공 타입은

레지스터에 넣어주지만, 사용자 정의 타입 객체는

아무리 기본제공 타입과 비슷하더라도 넣어주지 않는다.

이러한 개발 환경이라면 참조에 의한 전달을 쓰는 것이 낫다.

포인터만큼은 확실하게 레지스터에 들어간다.




복사 비용이 싸더라도 값에의한 전달을 할 수 없는

또 하나의 이유는 사용자 정의 타입의 크기는 언제든

변화에 노출되어 있다는 것이다. 지금은 작더라도

유지보수를 거듭하게 되면 언제 커질지 모르는 것이다.


따라서 기본제공 타입, STL의 반복자, 함수객체 타입

위 세가지 말고는 그냥 상수객체 참조자에 의한 전달방식을 쓰자.




POINT!!

1. 값에 의한 전달 보다는 상수 객체 참조자에 의한 전달

2. 기본제공 타입, STL의 반복자, 함수객체 타입은 값에의한 전달



인터페이스 설계는 제대로 쓰기엔 쉽게,

엉터리로 쓰기엔 어렵게 하자.


날짜를 나타내는 어떤 클래스가 있다.

class Date{

public:

Date( int month, int day, int year );

...


};


Date d( 1990 , 7 ,13 );

Date d( 20, 12, 2003);

매개변수의 전달 순서가 잘 못 될 수 있다.




해결책

새로운 타입을 들여와 인터페이스를 강화하자.

일,월,연을 구분하는 간단한 랩퍼 타입을 만들고

이 타입을 Date 생성자 안에 둔다.


struct Day{

int val;

explicit Day (int d) : val (d) {};

};

struct Month{

int val;

explicit Day (int m) : val (d) {};

};

struct Year{

int val;

explicit Day (int y) : val (d) {};

};


class Date {

public:

Date( const Month& m , const Day& d , const Year& y );

...

};


Date d (Month(3) , Day(3) , Yeay(1995));




적절한 타입만 제대로 준비되어 있다면, 각 타입에

값에 제약을 가하더라도 괜찮은 경우가 생긴다.

위의 경우, 월이 가질 수 있는 값은 12개 뿐이므로

enum을 사용해서 값에 제약을 줄 수 있다.

하지만 enum의 타입 안전성은 그리 믿음직하지

못하기 때문에, 유효한 Month의 집합을 미리 정의해 두자.


class Month {

private:

explicit Month(int m); --> Month 값이 새로 생성 되지 않도록 하기위해 private로 선언

--> 이미 밑의 함수에서 Month가 리턴되고 있기 때문에 생성될 필요없음.

public:

static Month Jan() {return Month(1);}  --> 왜 함수를 쓰느냐?

...                                                            --> 비지역 정적 객체들의 초기화 문제

static Month Dec() {return Month(12);}

...

};


Date d(Month::Mar(), Day(30), Year(1995));



예상되는 사용자 실수를 막는 다른 방법으로는

어떤 타입이 제약을 부여하여 그 타입을 통해

할 수 있는 일들을 묶어 버리는 방법이 있다.

제약 부여 방법으로 아주 흔히 쓰이는 예가 const 이다. 

앞 전 포스팅에서 다뤘기 때문에 패스~




사용자 정의 타입은 기본제공 타입처럼 동작하게 만들어야 한다.

기본제공 타입과 쓸데없이 어긋나는 동작을 피하는 실질적인

이유는 일관성 있는 인터페이스를 저공하게 위함이다.

STL의 모든 컨테이너는 size란 멤버 함수를 개방해 놓고 있다.

이 함수는 어떤 컨테이너에 들어 있는 원소의 개수를 알려준다.

만약 이러한 함수가 컨테이너 마다 다르다면? 혼란의 여지가 있다.


사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽다.




Investment* createInvestment(); -> 팩토리 함수

요 앞전에 다뤘던 팩토리 함수에서 반환되는 포인터를

auto_ptr 과 shared_ptr로 관리를 해주는 얘기를 했었다.

여기에서도 인터페이스에 관한 얘기를 할 수 있는데,

스마트 포인터를 사용해야 한다는 사실을 잊을 경우 이다.

--> 애초부터 팩토리 함수가 스마트 포인터를 반환하게 만들자.


shared_ptr<Investment> createInvestment();


shared_ptr을 반환하는 구조는 자원 해제에 관련된

상당수의 사용자 실수를 사전 봉쇄할 수도 있어서

여러모로 인터페이스 설계자에게 좋다.

생성 시점에 삭제자를 직접 엮을 수 있는 기능이 있기 때문.




createInvestment를 통해 얻은 investment* 포인터를

직접 삭제하지 않게 하고 getRidOfInvestment라는

함수를 준비하고 여기에서 삭제를 진행한다고 가정해보자.

--> getRidOfInvestment 대신 delete를 쓸 수 있는 가능성이 있다.


shared_ptr에는 두 개의 인자를 받는 생성자가 있다.

첫 번째 인자는 이 놈이 관리할 실제 포인터이고,

두 번째 인자는 참조 카운트가 0이 될 때 호출될 삭제자이다.

shared_ptr이 널 포인터를 물게 함과 동시에 삭제자로

getRidOfInvestment 를 갖게 하는 방법을 쓸 수 있다.




shared_ptr<Investment> pInv(0,getRidOfInvestment);

--> 컴파일 에러가 난다. 생성자의 첫 번째 매개변수로

포인터를 받아야 하는데 0은 포인터가 아니라 int이다.


캐스트를 적용하여 문제를 해결한다.

shared_ptr<Investment>

pInv(static_cast<Investment*>(0),getRidOfInvestment);




이제 위에 놈을 반환하도록 구현해보자.

shared_ptr<investment> createInvestment(){

shared_ptr<investment> retVal (static_cast<Investment*>(0),

,getRidOfInvestment);

...

retVal = ... ;      --> 실제 객체를 가리키도록 만든다.


return retVal;

}


--> 실제 객체의 포인터를 결정하는 시점이

retVal을 생성하는 시점보다 앞설 수 있으면,

위의 코드 처럼 retVal을 널로 초기화하고 대입하는 것보다

실제 객체의 포인터를 바로 생성자에 넘겨버리는 것이 낫다.




shared_ptr은 기본적으로 교차 DLL문제를 미연에 방지해준다.

교차 DLL문제란 객체 생성 시에 어떤 동적 링크 라이브러리의

new를 썻는데 그 객체를 삭제할 때는 이전의 DLL과

다른 DLL의 delete를 썼을 경우이다.


하지만 shared_ptr은 포인터마다 각각의 

삭제자를 자동으로 쓰기 때문에 자신이 가리키는

DLL을 잊지 않고 있다가 해당하는 DLL의 delete를 사용한다.




결론

사용자가 무심코 저지를 수 있는 실수를 없앰으로써

좋은 인터페이스를 만다는 데 쉽게 다가갈 수 있다.


POINT!!

1. 인터페이스 사이의 일관성 잡아주기, 기본제공 타입과 똑같이 동작하게 하기.

2. 사용자의 실수를 방지하는 방법으로 새로운 타입 만들기, 타입에 대한 연산을 제한하기,

객체의 값에 대해 제약 걸기(const), 자원 관리 작업을 사용자 책임으로 놓지 않기(팩토리 함수의 반환값을 스마트포인터로)

3.  shared_ptr은 사용자 정의 삭제자를 지원한다. 때문에 교차 DLL 문제를 막아준다.



new로 생성한 객체를 스마트 포인터에 저장하는

코드는 별도의 한 문장으로 만들자.


처리 우선순위를 알려 주는 함수가 있고,

동적으로 할당한 Data 객체에 대해 어떤

우선 순위에 따라 처리를 적용하는 함수가 있다.


int p();

void processData ( shared_ptr<Data> pd, int p );


1. processData (new Data, p);

--> 컴파일 오류가 난다.

shared_ptr의 생성자는 explicit으로

선언 되어 있기 때문에, 'new Data' 라는

표현식에 의해 만들어진 포인터가

shared_ptr 타입의 객체로 바꾸는

암시적인 변환이 가능할 수 없다.


2. pricessData( shared_ptr<Data>(new Data) , p());

--> 정상적으로 동작한다.




컴파일러는 processData 호출 코드를 만들기 전에

이 함수의 매개변수로 넘겨지는 인자를 평가 한다.

첫 번째 인자 shared_ptr<Data>(new Data)는

1. new Data 표현식을 실행하는 부분과

2. shared_ptr 생성자를 호출하는 부분으로 나눠진다.




따라서 processData 함수 호출이 이루어지기 전에

컴파일러는 다음의 세 가지 연산을 위한 코드를 만든다.


1. p를 호출.

2. new Data를 실행.

3. shared_ptr 생성자 호출.


하지만 각각의 연상이 실행되는 순서는

컴파일러마다 다르다는게 문제점이다.

따라서 new Data 부분이 shared_ptr보다 먼저

호출되어야 하지만 이 부분을 장담할 수 없다.




1. new Data를 실행.

2. p를 호출.

3. shared_ptr 생성자 호출.


위와 같이 실행이 되도 문제가 생길 수 있다.

p를 호출하는 부분에 예외가 발생했다면

new Data로 만들어졌던 포인터가 유실될 수 있다.




해결책

Data를 생성해서 스마트 포인터에

저장하는 코드를 별도의 문장 하나로 만들고,

그 스마트 포인터를 processData에 넘기는 것이다.


shared_ptr<Dara> pd(new Data);

--> new로 생성한 객체를 스마트 포인터에

담는 코드를 하나의 독립정인 문장으로 만든다.

processData(pd, p());

--> 인자의 평가 순서가 바뀌어도

자원 누출의 가능성이 없다.




POINT!!

new로 생성한 객체를 스마트 포인터로 넣는 코드는

별도의 한 문장으로 만들자. 이것이 안 되어 있으면,

예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있다.



new 및 delete를 사용할 때는 형태를 반드시 맞추자.


string* sArray = new string[100];

...

delete sArray;


new

1. 먼저 operator new 에 의해 메모리가 할당된다.

2. 할당된 메모리에 한 개 이상의 생성자가 호출된다.


delete

1. 기존 할당된 메모리에 한개 이상의 호멸자가 호출된다.

2. operator delete에 의해 메모리가 해제된다.




여기서 핵심은 삭제되는 포인터 sArray는 객체 하나만

가리키느냐, 객체 배열을 가리키고 있느냐 이다.


단일 객체와 객체 배열은 메모리 배치구조가 다르다.

객체 배열은 배열원소의 개수에 대한 정보를 가지고 있다.

하지만 단일 객체는 이러한 정보를 가지고 있지 않다.




따라서 이 둘의 배치구조가 다르기 때문에 delete를 사용해

메모리를 해제해 줄때, 배열 크기 정보가 있다는 것을

알려줘야 한다. 이 때 delete[] 를 써주면 이 포인터가

배열을 가리키고 있구나 라고 가정하게 된다.




string sPtr1 = new string;

string sPtr2 = new string[10];


delete sPtr1; --> 단일 객체로 인식하고 한 개를 삭제.

delete[] sPtr2;  --> 배열의 원소 개수 정보를 넘겨줌




주의점

1. sPtr1에 delete[]를 쓸 경우

delete는 sPtr1 앞쪽의 메모리 몇 바이트를 읽고

이 것을 배열 크기라고 해석하고, 배열 크기에

해당하는 만큼 소멸자를 호출하기 시작한다.


2. sPtr2에 delete를 쓸 경우

sPtr2를 단일 객체로 인식하고 소멸자를

단 한번만 호출하게 된다. 소멸자가 없는

기본제공 타입이라 해도 이들의 배열에 대해

[]를 쓰지 않으면 미정의 동작이 나타난다.




new 표현식에 []를 썼으면, 여기에 대응 되는

delete 표현식에도 []를 써야 한다.

반대로 new에서 []를 쓰지 않았으면

delete 에도 []를 쓰지 않으면 된다.




동적 할당된 메로리에 대한 포인터를

멤버 변수로 가지고 있는 클래스라면

이 클래스에서 제공하는 모든 생성자에서는

new 형태를 똑같이 맞춰줘야 한다.

왜?? 소멸자에서 어떠한 형태의 delete를

써야할지 구분하기 어렵기 때문이다.




주의점

배열을 typedef로 사용할 경우

typedef string Address[10];

...

stinrg *p = new Address;

어떠한 delete를 써야할까?

혼란의 여지가 다분하다.


1. delete p --> 배열인데 객체 하나만 지운다.

2. delete[] p --> 제대로 동작한다.


배열을 typedef로 만들지 말자. --> 알아보기 힘들다.

대신 stiring 또는 vector 같은 클래스 템플릿을 잘 활용하자.

잘 활용한다면 동적 할당된 배열이 필요해질 경우가 거의 없다.




POINT!!

1. new와 delete의 형식을 맞추자.

자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자.


자원 관리 클래스의 주축 아이디어

자원 획득 즉 초기화 (RAII) --> 전 시간에 포스팅


1. auto_ptr

2.shared_ptr

힙에 생기지 않는 자원은 위의

두 스마트포인터로 처리해 주지 못한다.




예제) 동적 할당되지 않는 경우의 자원 관리 클래스

스레드 동기화를 위한 Mutex 타입의 객체가 있다.

위의 객체가 제공하는 함수 중엔 lock와 unlock가 있다.


void lock(Mutex *pm) --> pm이 가리키는 뮤텍스를 잠금

void unlick(Mutex *pm)  --> 뮤텍스 잠금 해제


이제 뮤텍스의 잠금을 관리하는 클래스를 만들어 보자.

이전에 걸어 놓았던 뮤텍스의 잠금을 잊지 말고 풀어주기 위함.

이런 용도의 클래스는 RAII 법칙을 따라

생성 시에 자원을 획득하고, 소멸 시에 그 자원을 해제 한다.


class Lock{

private:

Metex* mutexPtr;

public:

explicit Lock(Mutex *pm) : mutexPtr(pm) {   // 생성과 동시에 자원 획득

lock (mutexPtr);                                     // 이 클래스에서는 잠금을 걸어 준다.

}

~Lock { unlock(mutexPtr) }                           // 자원 해제

}                                                                            // 이 클래스에서는 잠금을 해제.


int main(){

Mutex m;                             // 뮤텍스 정의

...

{

Lock m1(&m);              // 뮤텍스에 잠금을 건다.

...

}                                         // 블럭이 끝나면 Lock의 소멸자가 호출되면서

    // 뮤텍스의 잠금을 풀어주게 된다.




문제제기

Lock 객체가 복사된다면 어떻게 해야 할까?

위와 같은 스레드 동기화 객체에 대해서는

사본에 대한 의미가 없다. 위와 같은 경우가

아니라도 RAII 클래스를 만들 때 복사에 대해

주의를 기울여야 한다.


해결책

1. 복사를 금지 한다.

위와 같은 예제의 경우 사본에 대한

의미가 없기 때문에, 복사를 금지 한다.

복사를 금지하는 방법에 대해서는

지난 포스팅에서 언급했기 때문에 패스~


2. 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.

shard_ptr (RCSP)이 참조 카운팅 방식의 스마트 포인터 라는 것을

지난 포스팅을 통해 알고 있다. 그리고 그 방식 또한 알고 있다.

해당 자원을 참조하는 객체의 개수를 카운팅하는

방식으로 복사를 진행하겠다 이 말이다. 


때문에 자원관리 객체 내에서 shared_ptr을 써보겠다.

Lock 클래스 내에서 Mutex* mutexPtr 이 아니라

shared_ptr<Mutex> mutexPtr; 로 바꿔주자는 말이다.




하지만 이 자원 관리 객체를 만든 의미를 다시 한번 생각해보자.

뮤텍스 객체를 잠그고 해제하는 용도이지 뮤텍스 객체를 사용하고

제거해버리는 용도가 아니다. 하지만 shared_ptr은 뮤텍스 객체를

가리키고 있고, 스마트 포인터 특성에 따라 객체가 없어지면서

뮤텍스 객체를 삭제해 버린다. 해결할 수 있는 방법이 없을까?


다행히도 shared_ptr은 삭제자 지정을 허용한다.

무슨말인고 하면 shared_ptr이 유지하는 참조 카운트가

0이 되면 무조건 소멸이 아니라 특정 함수 혹은 함수 객체를

호출할 수 있다는 것이다. 생성자의 두 번째 매개변수로 넣어 준다.




class Lock{

private:

shared_ptr<Mutex> mutexPtr;

public:

explicit Lock(Mutex* pm) : mutexPtr (pm, unlock) {

lock(mutexPtr.get()); --> 다음 포스팅에서 다룰 것이다.

}

};


이렇게 만들어주면 shared_ptr은 자신이 소멸 시에

가리키는 객체를 삭제해주는 것이 아니라 unlock() 함수를 호출한다.

또한 위의 클래스에는 소멸자가 없는데, 기본 소멸자를 호출한다.




3. 관리하고 있는 자원을 진짜 복사한다.

"자원을 다 썻을 때 각각의 사본을 확실히 해제한다" 는

전제 하에 이 정책을 들이도록 하자.

또한 자원관리 객체는 깊은 복사를 수행해야 한다.


4. 관리하고 있는 자원의 소유권을 옮긴다.

어디서 들어본 말인고 하니 auto_ptr의 복사 동작이다.

즉 그 자원을 실제로 참조하는 RAII 객체는 단 하나만

유지하겠다는 것이다. (흔한 경우는 아님).




POINT!!

1. RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고가기 때문에,

 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.


2. RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하는 것,

그리고 참조 카운팅을 해주는 방법이다.



자원 관리에는 객체가 그만!


class Investment { ... }  --> Base 클래스


Investment* createInvestment();

--> Drived 클래스들의 객체를 동적 할당하고

      그 포인터를 반환하는 팩토리 함수.


이 객체의 해제는 호출자 쪽에서 직접 해야 한다.


void f(){

investment *pInv = createInvestment();

....

....

delete pInv;

}


언뜻 보기엔 별 문제 없어 보인다.

1. 하지만 ... 부분에서 return; 이 있을 경우

2. continue, goto가 있을 경우

3. 예외가 발생될 경우

위의 경우 delete는 실행 되지 않고

메모리가 누출되고 자원이 새게 됩니다.




해결책

createInvestment() 함수로 만들어낸 자원이

항상 해제되도록 만드는 방법은

자원을 객체에 넣고 그 자원 해제를

소멸자가 맡도록 하며, 그 소멸자는 실행 제어가

f를 떠날 때 호출 되도록 만드는 것이다.




개발에 쓰이는 상당수의 자원이 힙에서 동적으로

할당되고, 하나의 블록 혹은 함수 안에서만 쓰이는

경우가 잦기 때문에 그 블록 혹은 함수로부터

실행 제어가 빠져 나올 때 자원이 해제되는 것이 맞다.




스마트 포인터 auto_ptr을 써보자.

가리키고 있는 대상에 대해 소멸자가

자동으로 delete를 불러주도록 설계되어 있다.


void f(){

auto_ptr<Investment> pInv (createInvestment());

...

...

}

--> 블럭이 끝나면 auto_ptr의 소멸자를 통해 pInv를 삭제한다.


위와 같이 자원 관리 객체를 사용하는 

방법의 중요한 특징에 대해 알아보자.




1. 자원을 획득한 후에 자원 관리 객체에 넘긴다.

createInvestment() 함수가 만들어준 자원을

auto_ptr 객체를 초기화 하는 데 쓰이고 있다.

--> 자원 획득 초기화 (RAII)

자원 획득과 자원 관리 객체의 초기화가 

한 문장에서 이루어지는 것이 일반적이다.


2. 자원 관리 객체는 자신의 소멸자를 사용해

자원이 확실히 해제되도록 해야한다.

소멸자는 객체가 소멸될 때 자동적으로

호출되기 때문에, 실행 제어가 어떤 경위로

블록을 떠나가는가에 상관없이 자원 해제가

제대로 이루어지게 되는 것이다.




주의점!!

auto_ptr은 자신이 소멸될 때, 자신이 가리키는

객체를 자동으로 delete를 해주게 된다.

따라서 같은 객체를 두개의 auto_ptr이

가리키고 있다면 자원이 두번 삭제되는 결과.


---> 때문에 auto_ptr은 객체를 복사하게 되면

원본객체는 null로 만들어버린다.

예제를 보도록 하자.


auto_ptr<Investment> pInv1(createInvestment());

auto_ptr<Investment> pInv2(pInv1);

--> pInv2가 객체를 가리켜 pInv1은 null

pInv1 = pInv2;

--> 이제는 pInv2가 null


문제점 : STL 컨테이너의 경우엔 원소들의 정상적인 복사 동작을

가져야 하기 때문에 auto_ptr은 최선의 방법이 아니다.


해결책

참조 카운팅 방식 스마트 포인터 (RCSP)를 쓰자!

참조 카운팅은 여러개의 객체(자원 관리 객체)들이 동일한 값을 가질 때,

그 객체들로 하여금 그 값을 나타내는 하나의 데이터를

공유하게 하여 데이터의 양을 절약하는 기법이다.


어떠한 자원을 참조하는 횟수를 기록하여 자원의 생명주기를 관리한다.

이 횟수가 있다는 것은 메모리 어딘가에서 쓰이고 있다는 것이다.

함부로 자원을 삭제하면 사용 중인 정보가 무효화 될 것이다.

따라서 참조 횟수가 0이 되기 전에는 이 자원이 삭제되지 않는다.


TR!에서 제공하는 std::tr1::shared_ptr이 대표적인 RCSP이다.


void f(){

shared_ptr<Investment> pInv1 (createInvestment());

shared_ptr<INvestment> pInv2(pInv);

pInv1 = pInv2;

}


--> 복사와 대입 문제없이 실행된다.


주의점!

스마트 포인터의 소멸자에서는 delete 연산자를

사용하고 있다는 것이다. delete[]가 아니라는 점!

따라서 동적 할당된 배열에 대해 스마트 포인터를

사용하지 말자는 것. 동적 할당된 배열은 vector이나

string을 통해 거의 대체할 수 있기 때문이다.



POINT!!

1. 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고

소멸자에서 그것을 해제하는 RAII객체를 사용하자.


2. 널리 쓰이는 RAII는 대표적으로 auto_ptr, shared_ptr이 있다.


객체의 모든 부분을 빠짐없이 복사하자.


이 항목의 핵심은

복사생성자, 복사 대입연산자를 (복사 함수)

내가 직접 선언할 경우에 대한 문제점이다.


이러한 복사 함수를 직접 선언할 경우

어떤 문제점이 있는지 들여다 보자.




class AAA{

private:

int a;

public:

AAA(int _a) : a(_a){};

AAA(const AAA& ra){

a = ra.a;

}

AAA& operator=(const AAA& ra){

a = ra.a;

return *this;

}

}


모든 멤버(하나밖에 없긴 하지만)를

복사하는 복사함수이다. 여기까진 문제가 없어보이지만

클래스의 멤버 변수로 int b; 라는 놈이 선언되었다면

이 복사함수 내에서 a는 복사가 되지만 b는 복사가 되지 않는다.

더 큰 문제점은 컴파일러가 어떠한 경고 메세지도 주지 않는다는 점!


클래스의 상속에서 이러한 문제는 더 뚜렷하게 드러난다.



출력 결과

1

2


파생클래스에서 복사 대입연산자를 선언하고

자신의 멤버를 빠짐없이 대입한다.

하지만 기본 클래스의 멤버의 대입은 이루어 지지 않는다.


만약 이 대입 연산자를 자신이 생성하지 않았다면

기본 클래스의 멤버까지 말끔하게 복사 대입해준다.

(복사 생성자도 위와 같은 현상을 보인다.)


위의 경우에 복사생성자는 선언해 주지 않았다.

기본 클래스의 멤버 변수까지 완벽하게 복사가 되었다.

(기본 복사 생성자에 의해)




자신이 복사생성자를 선언해주려면

BBB(const BBB& rb) : AAA(rb) , b(rb.b) {};

이런 식으로 기본생성자에 대한 복사를

빠뜨리지 않도록 각별히 주의해야 한다.


복사 대입 연산자의 경우에도

BBB& operator=(const BBB& rb){

AAA::operator(rb);

...

return *this;

}




정리

1. 해당 클래스의 데이터 멤버를 모두 복사한다.

2. 이 클래스가 상속한 기본 클래스의 복사 함수도

꼬박 꼬박 호출을 해주어야 한다.



주의 사항!!

복사생성자 복사 대입연산자 내의

코드의 중복이 심하다고 해서

복사생성자 내에서 대입연산자를 호출한다거나

대입 연산자 내에서 복사 생성자를 호출하는 짓은 하면안된다.




왜??

1. 복사 대입 연산자에서 복사생성자를 호출하는 것은

이미 만들어져 있는 객체를 다시 생성하는 행위이다.


2. 복사생성자에서 대입 연산자를 호출 하는 것 또한 넌센스

생성자의 역할은 새로 만들어진 객체를 초기화 하는것

대입 연산자의 역할은 이미 초기화가 끝난 객체에 값을 주는 것

따라서 초기화된 객체에만 적용되는 것이다.




대신 코드의 내용이 비슷하다면

겹치는 부분을 별도의 함수로 만들어 놓고

private 멤버로 두고 호출해서 사용하는 방법도 있다.




POINT!!

1. 복사 함수는 모든 데이터멤버를 포함해

기본 클래스의 부분도 빠뜨리지 말아야 한다.


2. 두개의 복사 함수 한쪽을 이용해서 다른 한쪽을

구현하려는 시도는 절대로 하지 말아야한다.





+ Recent posts