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

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


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

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의 형식을 맞추자.

+ Recent posts