Media Log

[서진택]에 해당되는 글 1

  1. h 게임 개발자를 위한 C++ -서진택 2 2008.05.11
게임 개발자를 위한 C++ - 8점
서진택 지음/민프레스(민커뮤니케이션)

대학교 2학년 2학기 때, C언어에 어느 정도 익숙해지고 C++을 혼자서 조금씩 공부해 가던 때에, 친구의 추천으로 이 책을 읽었다.

당시 C++을 잘 모르던 내게 이 책은 너무나 버겨웠는데, 그럼에도 불구하고 이 정도 고급 주제를 다룬 국내 서적은 찾아 보기 힘들었기 때문에, 이해 될 때까지 연구실 책상에서 보고 또 보고 했던 기억이 난다.

그렇게 여러번 보다 보니 처음에는 전혀 모르겠던 내용들도 어렴풋이 이해하게 되고 나는 C++에 조금씩 익숙해져 갔다.

이 책은 내용은 괜찮았지만, 편집이 엉터리였다. 인쇄가 아마 -> 연산자가 모두 공백으로 나왔었언가? 그런 류의 심각한 결함이 있었다.

하지만 이 책에서 C++의 여러 문법들과, 오버로딩 오버라이딩, 상속 관계에서의 생성자와 소멸자의 호출 순서, 복사 생성자, this 포인터, 가상함수와 가상함수 테이블, 연산자 오버로딩등을 배울 수 있었다.

지금도 기억에 강하게 남아있는 부분은 복사 생성자인데, 이 책의 저자는 복사생성자를 참 중요하게 강조했던 것 같다.

복사 생성자란, 말 그대로 객체가 복사 될 때 호출되는 생성자이다.
CString s1(_T("string"));
CString s2( s1 ); // 또는 CString s2 = s1;


두번째 줄에서 s1을 s2에 복사하면서 복사 생성자가 호출 된다.
그냥 생성자가 아니라 CString::CString( const CString& ) 의 시그내쳐를 가진 복사 생성자 말이다.

하지만 코드를 이렇게 작성한다면,
CString s1(_T("string"));
CString s2;
s2 = s1;


위와 같은 경우에는 s2를 기본 생성자를 통해 객체를 생성한 뒤 operator=() 을 통해서 s1을 s2에 복사한다. 그러므로 위 코드에서는 복사 생성자가 호출 되지 않는다.

간단한 내용이지만, 생성자에서 멤버를 new하고 소멸자에서 그 멤버를 delete하는 경우에 복사 생성자를 정의해주지 않으면 복사 될 때 프로그램은 죽는다.
복사 생성자를 정의하지 않았을 때 컴파일러가 알아서 복사 생성자를 만들어 주지만 멤버에 포인터가 있다면 그 포인터 값을 그대로 복사하도록 코드를 생성한다.

이런 식으로 말이다.
class A
{
public:
    //somemethod();
private:
    int* pPointer;
};

A::A( const A& copy )
{
    this->pPointer = copy.pPointer;
}// 컴파일러는 이런식으로 복사생성자 코드를 만들어준다.


근데 A 클래스 생성자와 소멸자가 다음과 같았다고 하면,
A::A()
{
    this->pPointer = new int();
}

A::~A()
{
    delete this->pPointer;
}


이 클래스를 이런식으로 사용할 때,
{
    A obj1;
    A obj2 = obj1;
}


블락이 끝날 때에 프로그램은 죽는다.
- 사실 죽는다는 표현 보다는 정의되지 않은 동작을 수행한다. 라고 하는 것이 좀 더 정확하다. 하지만 일반적으로 거의 모든 경우에 뒤진다. :)

왜냐하면 obj1을 생성 할 때 불리는 기본 생성자에서 멤버 변수에 메모리를 동적 할당했고,
obj1을 obj2에 복사하면서는 복사생성자를 통해 obj2를 생성했는데, 이때는 pPointer에 새로운 메모리를 할당한 것이 아니라 obj1의 멤버 변수 pPointer의 주소만을 복사했기 때문이다.
블락이 끝나면서 소멸자가 호출 될 때는 각 객체의 소멸자가 모두 호출 되면서 pPointer를 해제하려고 한다.
하지만 2번째 delete할 때 이미 해제한 포인터를 다시 delete하려고 하므로 오류.

이럴 때는 복사 생성자를 컴파일러에 맡기지 말고 다음처럼 직접 작성해주어야 한다.
A::A( const A& copy )
{
    this->pPointer = new int( *copy.pPointer );
}


복사 될 때도 메모리를 동적으로 할당하고 복사본의 값을 가져오면서 deep copy가 되었다.

복사 생성자가 언제 호출되는지 아는 것도 중요하다.
첫번째 경우는 위 코드에서 설명했고,
두번째 경우는 함수 파라메터로 넘길 때이다.
void SomeMethod( A obj );

이런 함수가 있다면,
A obj;
SomeMethod( obj );

이렇게 호출 할때에 객체가 복사되면서 복사 생성자가 호출된다.

만일 함수가 void SomeMethod( A& obj ) 이렇게 참조를 받도록 정의되어 있다면, 이런 함수들은 물론 호출 할때 객체가 복사되지 않으므로 복사생성자도 호출되지 않는다.

마지막은 객체를 리턴할 때이다.
{
    A obj;
    // some codes...
    return obj;
}


이 때 역시 객체를 복사하므로 복사생성자가 호출 된다.

물론 C#이나 자바 등의 언어에서는 참조자만 사용되고 garbage collector 가 자원 해제 몫을 처리해 주기 때문에 이런 종류의 고민은 하지 않아도 된다.