본문 바로가기

C/C++

참조 카운팅 (Reference Counting) - 2

참조 카운팅 구현

 

 참조 카운팅 기능을 갖는 String 클래스를 만드는 일은 어렵지 않습니다만고려해야할 세부사항이 있습니다그래서 멤버 함수부터 차근히 살펴볼 필요가 있습니다.

 물, String이 가지는 값에 대한 참조 카운트를 저장할 장소가 필요한 건 당연하지만굳이 String 객체에 있을 필요는 없습니다실제문자열 값 하나에 대해서 참조 카운트는 하나만 있으면 되기 때문이죠. (String 객체마다 참조 카운트를 하나씩 두지 않아요). 참조 카운트와 값을 묶어서 관리해도 된다는 뜻입니다.

 

이 클래스의 이름은 StringValue로 하고, String 클래스의 구현을 보조하는 역할이기 때문에 private 영역에 이 클래스를 중첩시킵니다덧붙여 StringValue 데이터 구조는 String의 모든 멤버 함수가 액세스할 수 있게끔 class가 아닌 struct로 선언합니다.

(* 구조체를 클래스의 private 영역에 중첩시켜 두면이 구조체는 클래스의 모든 멤버함수가 액세스할 수 있고외부에는 절대적으로 노출되지 않습니다(프렌드 예외)

* struct public/private/protected를 명시적으로 붙이지 않으면 기본 public인 반면,

class private 입니다. )

 

위에서 말한 대로 설계하면 아래와 같아요

 

String class

class String {

public:

       String(const char *initValue = "");                        

       String(const Stringrhs);

       ...                              // String의멤버함수

 

private:

       struct StringValue {             // 참조카운트와실제문자열값을가짐

             int refCount;

             char *data;

             StringValue(const charinitValue);

             ~StringValue();

       };    

       StringValue *value;              // String 객체의값

};

 

String::String(const char *initValue)

:value(new StringValue(initValue))

{}

 

StringValue.cpp

String::StringValue::StringValue(const charinitValue)

refCount(1)

{

       data = new char[strlen(initValue) + 1];

       strcpy(datainitValue);

}

 

String::StringValue::~StringValue()

{

       delete[] data;

}

 

기본 골격 입니다그런데문자열의 참조 카운팅 기능을 완벽하게 구현하는데 필요한 것들은 빠졌습니다복사 생성자나 대입연산자도 없고, refCount를 조작하는 코드도 없네요이런 기능은 String 클래스에서 제공될 것입니다.

StringValue는 문자열값을 공유하는 String 객체의 카운트와 그 문자열값을 저장하면 충분합니다.

 

아래처럼 생성자를 사용하면,

String s("More Effective C++");

 

 s   ->   1   -> ‘More Effective C++’

이런 형태가 되겠죠..

 

아래와 같이 하면,

String s1("More Effective C++");

String s2("More Effective C++");

 

 s1  ->  1  -> ‘More Effective C++’

s2  ->  1  -> ‘More Effective C++’

 

이렇게 되구요

 

그런데 위의 생성자와 달리 복사 생성자는 다릅니다꼭 필요할 뿐 아니라효율을 위한 장치입니다새로 생성된 String 객체는 복사되는 String 객체와 똑 같은 StringValue 객체를 공유하게 됩니다아래를 보시죠.

 

String 복사 생성자

String::String(const Stringrhs)

value(rhs.value)

{

       ++value->refCount;

}

 

그리고 아래의 코드를 실행하면,

String s1("More Effective C++");

String s2 = s1;

이러한 자료구조를 만들어 냅니다.

           S1  -> 

           S2  ->  2  -> ‘More Effective C++’

(s1, s2 모두 2를 가르킵니다)

 

복사된 문자열에 메모리를 추가로 할당할 필요도 없고원래 메모리도 해제할 필요도 없으며메모리와 메모리 사이의 물리적 복사도 필요 없습니다.

포인터 값만 복사하고 참조 카운트만 증가시켰어요!

 

그리고 String 소멸자도 간단합니다. String 객체가 그 문자열값의 유일한 사용자일 때, (그 값의 참조카운트가 1일 때다시 말해서 참조 카운트를 감소시키고 참조 카운트가 0이 되는 시점에 StringValue 객체를 소멸하면 됩니다.

아래 코드가 이해가 빠르겟네요

 

String 소멸자

String::~String()

{

       if (--value->refCount == 0)

             delete value;

}

 

소멸될 때마다 delete를 호출할 필요가 없어요~

 

 

이제 String의 대입 연산자(=) 입니다.

대입 연산자는 신경 써야 할 부분이 있습니다. S2 s1에 대입했을 때 s1 s2는 똑 같은 StringValue 객체를 가리키고 있어야 합니다따라서 대입 연산이 이루어짐과 동시에 이 객체의 참조 카운트가 증가시켜야겠지요.

게다가 대입 전에 s1이 가리키고 있던 StringValue 객체에 대한 참조 카운트는 감소되어야 합니다대입이란 원래의 값이 다른 값으로 바뀌는 것이니까요 s1은 원래의 값을 가지지 않는다는 표시를 해두는 것입니다만일 원래의 값에 대한 참조카운트가 1이면그 값은 소멸되어야겠죠?

코드는 다음과 같아요.

 

String 대입연산자 (String& operator=(const String& rhs);

StringString::operator=(const Stringrhs)

{

       if (value == rhs.value)                //같은값이면아무것도안해요

       {

             return *this;

       }

 

       if (--value->refCount == 0)            // 원래의값을사용하는객체가

       {                                      // 없으면*this의값을소멸시킵니다

             delete value;

       }

 

       value = rhs.value;                     // *this로하여금rhs의값을

       ++value->refCount;                     // 공유하도록합니다

 

       return *this;

}

 

 

<출처>

1. More Effective C++, 정보 문화사, 스콧 마이어스