게임학원, 게임프로그래머 취업 전문 교육기관 DirectX11/12 자체엔진 게임개발과정,서버프로그래밍,자료구조,알고리즘,유니티,언리얼 게임학원, 언리얼학원 C++ 댕글링포인터 방지 스마트포인터 만들기 2부
본문 바로가기

C++ 댕글링포인터 방지 스마트포인터 만들기 2부

자 그럼 직접 스마트 포인터를 만드는 3가지 방법에 대해서 알아보도록 하겠습니다.

방식1. 스마트포인터가 RefCount와 자신이 관리할 데이터에 대한 포인터를 가지는 방식.

이방식의 경우 스마트 포인터 하나당 8바이트(32비트 빌드)의 메모리를 사용하게 됩니다.

기본적으로 SHARED_PTR<T>과 비슷한 방식일 것이라 예상됩니다.

 

방식2. 스마트포인터가 들어오는 데이터들에 대한 리스트와 레퍼런스 카운트를 자료구조로 관리하는 방식.

방식3 스마트포인터가 레퍼런스 카운팅을 할 수 있는 부모클래스를 상속받는 클래스들만 관리하는 방식 이와 같은 방식을 침습형 관리 방식이라고 합니다.

자 각자의 방식에는 다른 방식에 비교할 만한 장단점이 존재합니다.

 

방식 1

장점 -> 상속이나 자료구조를 통한 관리가 필요 없고 구현이 간단하다.

단점 -> 메모리를 각 스마트포인터당 8바이트를 사용한다. 하나의 데이터포인터를 가지고 있는

스마트포인터에서 다른 스마트포인터로만 레퍼런스 관리방식의 이동이 가능하다.

 

방식 2

장점 -> 상속이 필요 없고 스마트포인터에서 스마트포인터를 통한 이동을 하지 않고 데이터의

포인터만으로도 카운트 관리가 가능하다. 포인터와 다르지 않은 4(32비트)바이트의 메모리를

사용한다.

단점 -> 레퍼런스 카운터 연산시 상시 자료구조에서 검색을 해야 하는 문제점이 있다.

관리하는 자료구조의 검색 속력에 따라서 연산이 많아지고 전체적으로 저하될 가능성이 높다.

 

방식 3

장점 -> 스마트포인터에서 스마트포인터를 통한 이동을 하지 않고 데이터의 포인터만으로도 카운트 관리가 가능하다. 포인터와 다르지 않은 4(32비트)바이트의 메모리를 사용한다.

단점 -> 기본자료형에 대한 메모리 관리가 불가능하다. 관리하는 개체들이 레퍼런스 카운트를

관리해주는 클래스를 상속받아야 한다.

 

그럼 저는 방식 3을 통해서 구현해 보도록 하겠습니다. 일단 연산량이 제일 적고 실제적으로 기본자료형을 레퍼런스 카운트 방식을 통해서 관리하는 경우는 저의 경우에는 거의 없기 때문입니다. 또한 스마트포인터를 사용할 때의 편의성이 굉장히 향상되게 됩니다. 일단 상속을 통해서 구현되는 방식이므로 상속클래스를 구현해 보도록 하겠습니다.

레퍼런스카운팅을 위해서 카운트를 기록할 REFCOUNT가 핵심 맴버변수이며 유일한 맴버변수 입니다. 그리고 HADDREF()는 카운트의 증가 HDECREF는 레퍼런스카운트를 감소시키고 만약 레퍼런스 카운트가 0이되면 스스로를 지우게 됩니다. 자신을 지우는 것이 문제가 될까요? 1부에서 설명한 this의 의미를 생각한다면 문제가 없다는 것을 알 수가 있을 겁니다.

또한 핵심맴버변수인 HDECREF(), HADDREF()private:접근제한 지정자를 사용하기 때문에 이 클래스를 상속받은 맴버를 관리할 HPTR클래스를 friend로 지정하여 이 문제에 대한 해결을 합니다. 그리고 이것을 통해서 부모클래스 만으로는 레퍼런스 카운트 관리를 할 수는 없다는 사실을 알 수 있을 겁니다.

또한 소멸자가 protected:인 이유는 혹여라도 이 클래스가 값형으로 만들어져 소멸되는 일이 존재해서는 안되기 때문입니다. HREF를 상속받는 클래스가 HPTR로 관리될때는 동적할당으로 생성된 객체만을 관리할 수 있습니다.

그리고 그를 위해서 이를 전문적으로 관리해줄 보조 클래스를 만들게 되는데 이 보조클래스의 명칭이 HPTR 클래스며 이 클래스가 스마트포인터의 역할을 해주게 됩니다. 그럼 직접적인 스마트 포인터 클래스의 구현을 보겠습니다.

핵심 기능은 역시나 생성자와 소멸자 그리고 대입연산자에 존재합니다. 일단 HPTR이 생성될 때 자신이 관리할 HREF를 상속받은 클래스의 포인터를 인자로 받게 되면 그 포인터를 저장하면서 레퍼런스 카운트를 증가시킵니다. 이에 대한 함수는 당연히 상속받은 HREF의 맴버함수를 통해서 실행됩니다.

이렇게 레퍼런스 카운트가 증가되며 소멸될 때는 당연히 자신이 가진 데이터의 소유권이 파괴되는 것이므로 레퍼런스 카운트를 하나 감소시키고 그 순간 데이터를 참조하고 있는 다른 HPTR이 존재하지 않는다면 그 객체는 자동으로 파괴됩니다. 그리고 그렇기 때문에 만약 그 데이터를 소유한 HPTR이 하나라도 존재하게 된다면 그 포인터는 삭제가 되지 않을 것이고 그러므로 댕글링 포인터를 예방할 수 있습니다.

그럼 핵심 함수들과 오버로딩에 대해서 알아보겠습니다.

대입연산자와 생성자에 관련해서는 다른 HPTR을 받는 경우와 nullptr_t를 받는 경우가 있습니다. 아시겠지만 nullptr_tnullptr 상수의 자료형입니다. 즉 스마트포인터에 nullptr이 들어올 때에는 다른 처리를 해줘야 한다는 것입니다. 만약 함수의 인자로 nullptr이 들어왔다면 초기 자신이 보유하고 있던 데이터의 카운트만을 감소시키고 자신은 nullptr을 가지게 되었기 때문에 카운트를 증가시키는 함수를 사용하지 않는 것을 확인할 수 있습니다.

그럼 잘 동작하는지 시험해 보겠습니다. 테스트용 클래스를 새롭게 만들고 거기에 HREF를 상속만 내리면 HPTR<T> 클래스에서 관리해줄 수가 있습니다.

DTest클래스는 HREF를 상속받으면서 HPTR로 관리되는 클래스가 되었습니다. 이렇게 관리되게 된다면 이제 저 포인터를 HPTR로 관리하게 된다면 이 클래스는 따로 delete해주지 않고 스마트포인터로만 관리한다면 댕글링포인터 문제는 거의 일어나지 않게 될 겁니다.

하지만 단순히 포인터를 대체하는 스마트포인터가 메모리 관리를 위한 대입연산자와 생성자만 만들어서는 안됩니다. 일반적으로 포인터를 그 자체를 대체해야 하기 때문에 더 많은 연산자 오버로딩을 해야 합니다. 대표적으로 -> * 비교 연산자 등등이 있습니다. 그 내용들은 다음과 같습니다.

자 다음과 같이 연산자 오버로딩을 모두 해 놓으면 여러가지 포인터형은 물론 자기자신과의 비교가 가능해집니다. 하지만 맴버연산자 겹지정은 모두 클래스 당사자가 왼쪽에 오는 것을 기본으로 하기 때문에 전역 연산자 겹지정을 해야만 오른쪽에 놓고도 비교가 가능해지므로 특히 nullptr비교를 위해서는 연산자를 오른쪽에 놓는 것이 좋습니다. 이렇게 현재 필요한 연산자를 겹지정 해놓으면 다음과 같은 코딩이 가능해집니다.

자 이와 같이 포인터와 동일하게 사용할 수 있게 되면 스마트포인터를 좀더 효율적으로 사용 할 수 있게 됩니다. 그리고 기본적으로 HPTR은 클래스이기 때문에 다양한 함수들을 만들어서 여러가지 편의 기능도 추가할 수 있게 됩니다.

그럼 여기까지 스마트 포인터의 제작과정을 알아봤습니다. 당연히 스마트포인터가 만능은 아닙니다. 하지만 메모리를 관리할 때 한가지 방편으로 생각해 볼 수 있으므로 자신만의 동적할당 관리방법을 만들어 보는 것은 프로그래밍 실력을 기르는데 굉장히 도움이 되니 꼭 한번은 프로젝트에서 사용해 보시는 것을 추천합니다.