[전문가를 위한 C++] 7장. 메모리 관리
Part 3. 전문가답게 C++ 코딩하기
Chapter 7. 메모리 관리
- 이 챕터의 포인트는 7.4 스마트 포인터, 7.5 흔히 발생하는 메모리 문제이다. 시간이 없다면 이 부분 부터 보자.
7.1 동적 메모리 다루기
메모리 작동 과정 살펴보기
- 항상 포인터 변수는 선언하자마자 nullptr 혹은 적절한 포인터로 초기화 해야 한다.
메모리 할당과 해제
-
new의 리턴값을 무시하거나 그 포인터를 담았던 변수가 스코프를 벗어나면 할당했던 메모리에 접근할 수 없고, 이를 메모리 누수(메모리 릭)라 부른다.
int leaky() { new int; // 버그! 메모리 누수가 발생했다. }
- new로 메모리를 할당할 때 스마트 포인터가 아닌 일반 포인터로 저장했다면 반드시 그 메모리를 해제하는 delete문을 new와 짝을 이루도록 작성해야 한다.
-
메모리를 해제한 포인터는 nullptr로 다시 초기화 한다.
int *ptr = new int; delete ptr; ptr = nullptr;
- malloc() vs new
- malloc() 함수는 메모리에서 일정한 영역만 따로 빼놓을 뿐 객체에 대해 알지 못하지만,
- new 연산자는 적절한 크기의 메모리 공간이 할당 될 뿐 아니라 생성자를 호출해 객체를 생성한다.
배열
-
네 개의 Simple 객체로 구성된 배열을 할당하면 생성자가 4번 호출된다. 따라서 반드시 delete[]를 호출해서 메모리를 해제해야 한다.
Simple* mySimpleArray = new Simple[4]; delete[] mySimpleArray; mySimpleArray = nullptr;
-
각 원소가 가리키는 객체가 있다면 일일이 해제해 줘야 한다.
const size_t size = 4; Simple** mySimplePtrArray = new Simple*[size]; for (size_t i = 0; i < size; i++) { mySimplePtrArray[i] = new Simple(); } // 할당된 객체 해제 for (size_t i = 0; i < size; i++) { delete mySimplePtrArray[i]; } // 배열 삭제 delete[] mySimplePtrArray; mySimplePtrArray = nullptr;
포인터에 대한 타입 캐스팅
- 포인터는 단지 메모리 주소(화살표)에 불과해서 타입을 엄격하게 따지지 않는다.
- xml 문서를 가리키는 포인터와 정수를 가리키는 포인터의 크기는 같다.
-
포인터는 C Style 캐스팅을 이용해 바꿀 수 있다.
Document* doc = getDocument(); char *chardoc = (char *)doc;
-
정적 캐스팅을 사용하면 더 안전하게 캐스팅이 가능하다.
Document* doc = getDocument(); char *chardoc = static_cast<char *>(doc);
7.2 배열과 포인터의 두 얼굴
배열 == 포인터
포인터가 모두 배열은 아니다!
- 모든 배열은 포인터로 참조할 수 있지만, 모든 포인터가 배열은 아니다.
7.3 로우레벨 메모리 연산
포인터 연산
커스텀 메모리 연산
가비지 컬렉션
객체 풀
7.4 스마트 포인터
- C++는 스마트 포인터를 지원하는 기능을 언어 차원에서 다양하게 제공한다.
- 템플릿 : 모든 포인터 타입에 대해 안전한(타입 세이프한) 스마트 포인터 클래스를 작성할 수 있다.
- 연산자 오버로딩 : 스마트 포인터 객체에 대한 인터페이스를 제공해서 스마트 포인터를 일반 포인터처럼 사용할 수 있다. (*, ->를 오버로딩하면 스마트 포인터를 일반 포인터처럼 역참조할 수 있다.)
-
헤더 파일에 정의되어 있다.
unique_ptr
- 리소스에 대한 고유 소유권을 받는 스마트 포인터.
- 스코프를 벗어나거나 리셋되면 참조하던 리소스를 해제한다.
-
unique_ptr 생성 방법
void notLeaky() { auto mySimpleSmartPtr = make_unique<Simple>(); mySimpleSmartPtr->go(); // delete를 호출하지 않아도 된다. // unique_ptr 인스턴스가 스코프를 벗어나면 (함수가 끝나거나 익셉션이 발생하면) 소멸자가 호출될 때 객체가 자동으로 해제되기 때문. }
-
make_unique()를 지원하지 않는 컴파일이라면 다음과 같이 unique_ptr을 생성한다.
unique_ptr<Simple> mySimpleSmartPtr(new Simple());
-
C++ 17 이전에는 반드시 make_ptr을 사용해야 했다. (C++ 17부터는 둘 다 안전하다.)
// Simple이나 Bar의 생성자 또는 data()함수에서 익셉션이 발생하면 메모리 누수가 발생할 수 있다. foo(unique_ptr<Simple>(new Simple()), unique_ptr<Bar>(new Bar(data()))); // make_unique를 사용하면 메모리 누수가 발생하지 않는다. foo(make_unique<Simple>(), make_unique<Bar>());
-
- unique_ptr 사용 방법
-
get() 메서드를 이용하면 내부 포인터에 직접 접근할 수 있다.
void processData(Simple* simple) {...} auto mySimpleSmartPtr = make_unique<Simple>(); processData(mySimpleSmartPtr.get());
-
reset()을 사용하면 unique_ptr의 내부 포인터를 해제하고, 다른 포인터로 변경할 수 있다.
mySimpleSmartPtr.reset(); // 리소스 해제 후, nullptr로 초기화 mySimpleSmartPtr.reset(new Simple()); // 리소스 해제 후, 새로운 Simple 인스턴스로 설정
- release()를 이용하면 unique_ptr와 내부 포인터의 관계를 끊을 수 있다. 리소스에 대한 내부 포인터를 리턴한 뒤 스마트 포인터를 nullptr로 설정한다. 스마트 포인터는 리소스에 대한 소유권을 잃기 때문에 리소스를 다 쓴 뒤 반드시 직접 해제해야 한다.
Simple* simple = mySimpleSmartPtr.release(); // 소유권을 해제한다. delete simple; simple = nullptr;
-
unique_ptr은 단독 소유권을 표현하기 때문에 복사할 수 없다. 하지만 std::move() 유틸리티를 사용해 이동할 수는 있다.
class Foo { public: Foo (unique_ptr<int> data) : mData(move(data)) {} private: unique_ptr<int> mData; }; auto myIntSmartPtr = make_unique<int>(42); Foo f(move(myIntSmartPtr));
-
- C stype 배열
-
커스텀 제거자
int* malloc_int(int value) { int* p = (int*)malloc(sizeof(int)); *p = value; return p; } int main() { unique_ptr<int, decltype(free)*> myIntSmartPtr(malloc_int(42), free); return 0; }
shared_ptr
- 리소스의 소유자를 추적하도록 레퍼런스 카운팅을 구현한 스마트 포인터.
- 레퍼런스 카운터 (공유 소유권)가 0이 되면 그 리소스를 사용하는 곳이 없기 때문에 스마트 포인터에 의해 자동으로 해제된다.
-
생성 및 사용 방법
auto mySimpleSmartPtr = make_shared<Simple>();
- unique_ptr과 같이 get(), reset() 메서드를 지원한다.
- reset()는 마지막 shared_ptr가 제거되거나 리셋될 때 리소스가 해제된다.
- release()는 지원하지 않는다.
- 현재 동일한 리소스를 공유하는 shared_ptr의 개수는 use_count()로 알 수 있다.
-
메모리 할당 및 해제는 new와 delete 연산자를 사용할 수도 있다. (커스텀 제거자)
shared_ptr<int> myIntSmartPtr(malloc_int(42), free);
- unique_ptr과 같이 get(), reset() 메서드를 지원한다.
- shared_ptr 캐스팅하기
- const_pointer_cast()
- dynamic_pointer_cast()
- static_pointer_cast()
- reinterpret_pointer_cast()
- 레퍼런스 카운팅이 필요한 이유
- 중복 삭제 문제
- shared_ptr 복사본을 만들어서 사용해야 한다.
// 중복 삭제 void DoubleDelete() { Simple* mysimple = new Simple(); shared_ptr<Simple> smartPtr1(mysimple); shared_ptr<Simple> smartPtr2(mysimple); } // 복사본으로 중복삭제 문제 해소 void nonDoubleDelete() { auto smartPtr1 = make_shared<Simple>(); shared_ptr<Simple> smartPtr2(smartPtr1); }
- 참고로 unique_ptr은 복사 생성자를 지원하지 않음을 기억하자.
- 중복 삭제 문제
- 앨리어싱
- shared_ptr은 하나의 포인터를(소유한 포인터를) 다른 shared_ptr과 공유하면서 다른 객체(저장된 포인터)를 가리킬 수 있다.
- ex) shared_ptr가 객체를 가리키는 동시에 객체의 멤버도 가리킬 수 있다.
class Foo { public: Foo (int value) : mData(value) {} int mData; }; auto foo = make_shared<Foo>(42); auto aliasing = shared_ptr<Foo>(foo, &foo->mData); // 두 shared_ptr(foo, aliasing)가 모두 삭제될 때만 Foo 객체가 삭제된다.
- ex) shared_ptr가 객체를 가리키는 동시에 객체의 멤버도 가리킬 수 있다.
- shared_ptr은 하나의 포인터를(소유한 포인터를) 다른 shared_ptr과 공유하면서 다른 객체(저장된 포인터)를 가리킬 수 있다.
weak_ptr
- 언제 사용할까?
- weak_ptr은 shared_ptr 관리용. 레퍼런스 카운트에 포함되지 않는다.
- 실은 weak reference count를 증가시키지만 이는 객체 수명과 관련이 없다.
- weak_ptr은 shared_ptr 관리용. 레퍼런스 카운트에 포함되지 않는다.
- 어떻게 사용할까?
- weak_ptr 생성자는 shared_ptr 혹은 다른 weak_ptr를 인수로 받는다.
- weak_ptr에 저장된 포인터에 접근하려면 shared_ptr로 변환해야 한다.
- weak_ptr 인스턴스의 lock() 메서드를 사용해 shared_ptr을 리턴받는다.
- shared_ptr 생성자에 weak_ptr을 인수로 전달해 shared_ptr을 새로 생성한다.
- 사용 예제
이동 의미론
- 표준 스마트 포인터인 unique_ptr, shared_ptr, weak_ptr은 모두 성능 향상을 위해 이동 의미론을 지원 한다.
enable_shared_from_this
- 믹스인 클래스 enable_shared_from_this에서는 객체의 메서드에서 shared_ptr, weak_ptr을 안전하게 리턴할 수 있다.
- 대표적인 메서드는 다음과 같다.
- shared_from_this() : 객체의 소유권을 공유하는 weak_ptr을 리턴.
- weak_from_this() : 객체의 소유권을 추적하는 weak_ptr을 리턴.
- 사용 방법
- 객체의 포인터가 shared_ptr에 이미 저장된 상태에서만 shared_from_this()를 호출할 수 있음에 주의.
- 다음과 같이 getPointer() 메서드를 구현하면 중복 삭제 문제가 발생한다.
7.5 흔히 발생하는 메모리 문제
스트링 과소 할당 문제
메모리 경계 침범
메모리 누수
-
스마트 포인터를 사용하자.
// 포인터 변수가 레퍼런스로 전달되어 포인터에 다른 값을 할당. void doSomething(Simple*& ptr) { ptr = new Simple(); // Leak! } int main() { Simple* simplePtr = new Simple(); doSomething(simplePtr); delete simplePtr; return 0; }