c++

C++ Primer CH12. Dynamic Memory

big whale 2024. 1. 21. 16:33

원래 객체들은 lifetime(생명주기)가 이랬다.

- static 객체들: 프로그램 생성시 생성되고 종료시 해제됨.

- 지역 객체들: scope 벗어날 시 해제됨

- 함수 밖 객체들: 프로그램 생성시 생성되고 종료시 해제됨.

   → 어디에? data 영역에

        → data 영역: 전역변수와 static 변수를 저장하는데 사용됨. 프로그램 종료시까지 존재함

 

지금까지는 다 스택에서 메모리 할당해서 사용하는 방식이었다.

이제는 힙 영역에서 메모리 공간할당해서 사용하는 방법을 알아보자.

우선, new 메서드와 smart pointer 가 나온다.

둘 다 힙 영역을 사용해서 데이터를 할당한다.

 

동적 메모리 할당은 런타임에 일어난다.

 

왜 동적 메모리가 필요한가?

  1. 얼마나 공간을 사용할지 모르기 때문에
  2. 어떤 타입을 사용할지 모르기 때문에
  3. 여러 객체들간에 데이터를 공유하고 싶기 때문에

12.1 Dynamic Memory and Smart Pointers

동적 메모리 할당 관련 키워드: new, delete

memory leak: 할당 해제해야 했는데 까먹어서 못하면 발생

아직 포인터가 객체 가리키고 있는데 메모리 free시킴 → 포인터가 더이상 유효하지 않은 메모리 참조중

 

smart pointer

c++11에 나왔다.

자동으로 객체를 delete 시켜준다.

 

두 가지 타입

shared_ptr: 여러 포인터가 하나의 객체를 참조해도 됨

unique_ptr: 객체의 소유자가 유일함

 

weak_ptr: shared_ptr에 의해 관리되는 객체를 약한참조(참조카운트 안오름)하는 포인터

12.1.1 The shared_ptr Class

smart pointer도 template다.

→ 어떤 타입의 객체를 가리킬지 지정해줘야 함.

 

default initialize: null pointer(nullptr)

shared_ptr<string> p1;
shared_ptr<vector<int>> p2;

 

dereferencing and condition

if (p1 && p1->empty()) // p1이 null이 아니고 비어있을 때,
	*p1 = "hi"; // p1의 object를 "hi"로 할당

 

make_shared function

shared_ptr<int> p3 = make_shared<int>(42); // 42로 초기화
shared_ptr<string> p4 = make_shared<string>(10, 'a'); // "aaaaaaaaaa"로 초기화
shared_ptr<int> p5 = make_shared<int>(); // value initialization -> 0으로 초기화

auto p6 = make_shared<vector<string>>(); // auto 키워드로 쉽게 정의 가능

해당 객체 타입의 constructor들 중 하나라도 만족하는 인자 구성이 들어와야 한다.

 

Copying and Assigning shared_ptrs

shared_ptr는 자기 객체가 얼마나 참조되고 있는지 카운트를 내부에 저장해 놓고 있다.

auto p = make_shared<int>(42);
auto q(p); // q shared_ptr은 p가 가지고 있는 object를 가리키고 있다.
// 42의 user = p,q 2개

 

shared_ptr과 unique_ptr의 operations

 

reference count가 오르는 경우

  1. shared_ptr로 새로운 shared_ptr을 초기화할 때, 우항으로 할당할 때
  2. 함수 리턴값으로 shared_ptr을 반환할 때
auto r = make_shared<int>(10);
q = r; // 10의 reference count += 1

shared_ptr<int> foo() {
	auto a = make_shared<int>(20);
	return a;
}

int main(){
	auto ptr = foo(); // 20의 reference count += 1
}

 

reference count가 내려가는 경우

  1. shared_ptr이 destroy되는 경우
    • ex) local shared_ptr가 scope 벗어나서 파괴되는 경우
    • 기존 shared_ptr에 다른 shared_ptr을 할당하면 기존에 가리키던 객체가 파괴됨
    auto r = make_shared<int>(10);
    auto q = make_shared<int>(30);
    r = q; // 10은 파괴됨
    

shared_ptr의 reference count가 0이 되면 자동으로 객체의 메모리가 해제(free)된다.

reference count가 0 되면, 객체의 destructor가 호출되어 객체가 파괴된다.

 

shared_ptr 특화 operations

 

local scope가 함수 밖으로 가면 메모리 해제되는 것을 이용한 factory 기법

shared_ptr<Foo> factory(T arg)
{
	return make_shared<Foo>(arg);
}

void use_factory(T arg) 
{
	shared_ptr<Foo> p = factory(arg);
	// use p
} // p는 함수 스코프 벗어나면 p가 가리키던 객체는 메모리 해제된다.

더 자세히 설명

  • p는 use_factory 함수 호출 끝나면 파괴되면서 object의 reference count가 1 감소한다.
  • 현재 해당 object의 reference count가 0이기 때문에, 해당 객체의 destructor가 호출되면서 객체가 파괴면서 메모리 해제된다.

shared_ptr들을 컨테이너에 집어넣고 사용할 것이라면, 모든 shared_ptr를 사용하는 것도 아니고, 이미 사용이 끝난 shared_ptr들은 잘 free 시켜주어야 한다.

 

Classes with Resources That Have Dynamic Lifetime

vector<string> v1;
{
	vector<string> v2 = {"a", "an", "the"};
	v1 = v2; // v2의 원소가 모두 v1으로 복사됨
}
// v2는 스코프 벗어났기 때문에 파괴됨
// v1에는 여전히 v2에서 복사된 원소들이 남아 있음.

만약 벡터가 파괴었다면, 백터의 모든 원소들도 파괴됨

 

StrBlob 클래스

#include <vector>
#include <string>
#include <memory>
using namespace std;

class StrBlob {
public:
    typedef std::vector<std::string>::size_type size_type;
    StrBlob() :
        data(make_shared<vector<string>>()) {}
    StrBlob(std::initializer_list<std::string> il) :
        data(make_shared<vector<string>>(il)) {}

    size_type size() const {
        return data->size();
    }
    
    bool empty() const {
        return data->empty();
    }

    void push_back(const std::string &t) {
        data->push_back(t);
    }

    void pop_back() {
        check(0, "pop_back on empty StrBlob");
        data->pop_back();
    }

    std::string& front() {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    std::string& back() {
        check(0, "back on empty strBlob");
        return data->back();
    }

private:
    std::shared_ptr<std::vector<std::string>> data;
    void check(size_type i, const std::string &msg) const {
        if (i >= data->size()) {
            throw out_of_range(msg);
        }
    }
};

 

12.1.2 Managing Memory Directly

new를 사용해서 객체 동적할당 및 초기화

int *pi = new int;
string *ps = new string;

new 키워드는 unnamed, uninitialized 객체를 힙에 생성하고 객체를 가리키는 포인터를 반환한다.

할당시 해당 타입의 default constructor로 초기화된다.

 

다양한 초기화 방법

int *pi = new int(1024)
string *ps = new string(10, '9');

// list initialization
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5};

// default initialization
string *ps = new string; -> ""
int *pi2 = new int; -> undefined

// value initialization
string *ps = new string(); -> ""
int *pi2 = new int(); -> 0

 

auto 초기화

auto p1 = new auto(obj);

 

const object 동적 할당

const int *a = new const int(1024);
const string *str = new const string;

const로 동적할당하면 재할당이 불가능하므로, 초기화 시 값을 넣어줘야 사용할 수 있다.

 

Memory Exhaustion

free stroage(=heap) 공간이 꽉 차서 더이상 동적 할당이 불가능할 경우,

new로 새 공간 할당시 bad_alloc 예외가 던져진다.

방지 방법: new (nothrow) 사용 → placement new

int *p1 = new int; // bad_alloc throw
int *p1 = new (nothrow) int; // null pointer 리턴

 

Freeing Dynamic Memory

delete p;

포인터 아닌걸 delete → error

널포인터 delete → OK

포인터인데 동적할당 X → undefined

포인터이고 동적할당 O → OK

 

const로 동적할당해도 역시 delete 가능

 

new로 생성한 객체는 명시적으로 free해주기 전까지 계속 남아있는다(delete)

 

메모리 릭을 감지하기 어려운 이유가, 힙 저장소가 꽉 차기 전까지는 메모리 릭으로 인한 부작용이 나타나지 않기 때문이다.

→ new 대신 스마트 포인터를 사용해라

 

Resetting the Value of a Pointer after a delete

동적할당된 객체를 free시킨 후 해당 주소를 가리키고 있던 기존의 포인터는 dangling pointer라고 불리는 상태가 된다.

→ delete 이후 nullptr을 할당해주자.

12.1.3 Using shared_ptrs with new

일반 포인터를 스마트 포인터로 암시적 형변환할 수 없다.

shared_ptr<int> p1 = new int(1024); // ERROR 
shared_ptr<int> p1(new int(1024)); // OK, direct initialization

shared_ptr<int> clone(int p) {
	return new int(p); // error
}

 

static pointer와 dynamic pointer 혼용

// OK
shared_ptr<int> p(new int(42));
process(p);
int i = *p

// Not OK
int *x(new int(1024));
process(x); // Error. pointer는 smart pointer로 형변환 불가
process(shared_ptr<int>(x)); // 해당 표현식이 끝나는 순간 참조카운트가 0이 되어 int객체 파괴됨
int j = *x; // x는 dangling pointer라서 undefined behavior

스마트 포인터가 소유한 객체를 일반 포인터로 접근하지 마라.

→ 언제 객체가 파괴될지 모른다

 

get을 사용하지 마라.

shared_ptr<int> p(new int(42));
int *q = p.get();
{
	shared_ptr<int> (q);
}
int foo = *p; // undefined

q는 p와 독립적인 포인터로 참조카운트 1이다.

스코프 벗어나면 q는 파괴되고 q가 가리키던 객체는 메모리 해제된다.

 

get으로 얻은 포인터로 다른 스마트 포인터를 초기화하거나 할당하지 마라

그냥 포인터로 객체에 접근하는 용도로만 get 사용하고 delete 하지 않을 경우에만 사용해라

 

Other shared_ptr Operations

포인터를 shared_ptr로 만들고 싶을 때,

p = new int(1024); // error
p.reset(new int(1024)); // OK

 

12.1.4 Smart Pointers and Exceptions

함수 내에서 스마트 포인터를 이용해서 객체를 생성한 이후 예외가 발생했는데 함수 내에서 catch되지 않았다면 함수가 종료되는데, 이때 스마트 포인터 변수가 파괴되면서 참조카운트가 0 됨에 따라 해당 객체역시 파괴된다.

하지만, static 포인터인 경우, new로 동적할당했다면 delete되기 전에 예외 발생시 delete구문이 실행되지 않게 되고 따라서 메모리 릭이 발생한다.

 

Using Our Own Deletion Code

스마트 포인터는 기본적으로 동적할당된 객체를 가리키는데, connection같이 동적할당객체 아닌경우엔 커스텀한 파괴 함수를 넣어서 처리한다.

void end_connection(connection *p) { disconnect(*p); }

void f(destination &d) {
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	// 커넥션 사용
}

p가 destroy되면 이번엔 delete가 호출되지 않는다. 대신 인자로 넣어준 end_connection이 호출되어 커넥션을 끊는다.

 

놓쳤던 부분

12.1.5 unique_ptr

객체의 소유권을 가진다.

unique_ptr<double> p1;
unique_ptr<int> p2(new int(42)); // direct initialization

객체의 소유권을 가지기 때문에, 할당이나 초기화를 unique_ptr 변수로 해줄 수 없다.

unique_ptr<string> p1(new string<"hi">);
unique_ptr<string> p2(p1); // error. can't copy
unique_ptr<string> p3;
p3 = p1; // error. can't assign

 

unique_ptr operations

 

release()는 잘 써야 한다.

p2.release() // p2가 객체 소유권 버렸는데 반환값인 포인터가 임시객체로 버려져서 객체 메모리 릭
auto p = p2.release() // 이런식으로 받아와야 함.

 

Passing and Returning unique_ptrs

unique_ptr로 할당이나 초기화를 해줄 수 없다고 위에서 설명했지만, 함수 리턴값일 땐 예외이다.

unique_ptr<int> clone(int p) {
	return unique_ptr<int>(new int(p));
}

 

deleter 커스터마이징

deleter의 타입을 decltype으로 구해서 타입 지정자 옆에 추가해야 한다.

함수의 타입이기 때문에 *를 추가해줘야 한다.

void f(destination &d) {
	connection c = connect(&d);
	unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
}

12.1.6 weak_ptr

weak_ptr는 객체의 생명주기를 관리하는 포인터가 아니라, shared_ptr로 생성된 객체를 가리키는 포인터다.

auto p = make_shared<int>(42);
weak_ptr<int> q(p);

 

weak_ptr로 객체 사용하려고 할 때, 해당 객체가 이미 파괴되었을 수도 있기 때문에 lock이라는 걸 호출해서 사용한다.

lock: 사용하려는 해당 객체가 죽었는지 살았는지 확인해주는 함수

if (shared_ptr<int> np = wp.lock()) {
	// np 사용
}

wp.lock() 이 null이 아니면 shared_ptr이 있다는거라서 true이고 내부 로직 수행됨.