📢 공부내용에 앞서서
본 게시글은 면접준비 및 자기계발을 목적으로 작성된 게시글입니다
공부한 내용을 토대로 남들에게 설명할 수 있도록 이해하는 과정에 작성한 게시글이니 참고바랍니다
📢 포스트에 앞서서
본 게시글은 러프하게 작성된 게시글로
당장 몇일뒤의 코딩 테스트를 위해 작성된 게시글입니다
추후에 내용을 보충하여 재업로드 할 예정입니다
📌 참조자(Reference)
- 실체가 있어야 하며 선언 즉시 할당되어야함. 즉 NULL, nullptr로 할당 불가능
- 레퍼런스는 초기화 리스트를 사용하여 먼저 초기화해야하는데 (modern c++의 초기화리스트와는 다름. 생성자의 초기화리스트를 의미함), 이는 생성자 내부에서의 초기화는 먼저 null로 생성한 뒤 값을 넣는 방식이기 때문. 또한 한번 할당하면 다른 곳에 재할당 불가능
📌 포인터(Pointer)타입 변수
- 주소값을 저장할 수 있는 타입의 변수
- 실체가 없어 NULL이 가능하며, 언제든 할당할 수 있음
- 동적 메모리 할당에 사용함
📌 참고로 초기화리스트는 상수, 참조자, has-a관계의(포함한) 클래스 초기화에 사용해야 한다
📌 Malloc과 New의 차이점(C스타일 동적 할당과 C++스타일 동적 할당의 차이점)
- Malloc : 단순한 메모리 할당. 할당 시 메모리의 사이즈를 입력해서 할당받음. C스타일 Malloc은 void* 를 리턴하기 때문에 원하는 타입으로 캐스팅해서 사용
- New : 할당과 동시에 초기화 가능(초기값을 줄 수 있음). 생성자 호출됨(즉 C++ 객체 할당에 사용함). 오버로딩 가능(new도 연산자이다). 할당 시 객체의 크기를 입력하여 할당받음. C++ 스타일
📌 댕글링 포인터
- 포인터 변수를 delete나 free할 시에 메모리가 할당 해제되었다 해도 변수가 가리키는 주소값이 사라지는 것이 아니기 때문에, 그 포인터 변수를 다시 참조하려고 하면 미정의 동작을 수행한다
- 그래서 메모리를 해제하는 구문 이후, 해당 포인터 변수를 nullptr로 바꿔주고 사용할 때 마다 nullptr인지 체크하는 테크닉이 필요하다
- 비주얼 스튜디오의 경우, 메모리를 해제하면 0x0823같은 메모리로 치환하여 강제 크래시를 유발하기도 한다(의도적인 크래시가 미정의 동작보다 차라리 안전하기 때문이다)
- 단, 아무리 p를 할당 해제 후 nullptr로 치환하였다고 해도, p를 참조하는 포인터 변수가 있었다면 이는 추적하기 힘든 메모리 누수/버그를 발생시킬 수 있다. 스마트 포인터를 쓰면 이러한 문제를 어느정도 해소할 수 있다
📌 스마트 포인터
- 이전의 C++에서 동적 메모리를 할당하기 위해서는 new/delete를 이용해 포인터 변수를 정의하여 사용하여야 했다. 이러한 포인터를 원시 포인터(날 포인터, raw pointer)라고 한다.
- C++에서의 메모리는 프로그래머가 책임지고 delete를 통해 사용되지 않는 메모리를 반환해야 하였고, 메모리를 반환하지 않아 메모리 누수(memory leak)가 발생하거나 적절하지 못한 타이밍에 메모리를 delete하여 댕글링 포인터(dangling pointer)를 발생시켜 프로그램의 안전을 심히 위협하는 경우가 생기기도 하였다
- 특히 메모리 버그는 찾기 어려우므로 항상 문제가 되었다
- 스마트 포인터는 C++11에서 공식적으로 추가된 기능으로, 스마트 포인터를 사용하면 객체가 더는 필요하지 않을 때 객체에 할당된 메모리가 자동으로 해제된다. 즉 메모리 누수의 가능성을 영구적으로 제거한다
- C#이나 Java처럼 Garbage Collector를 사용하는 것은 아니고 레퍼런스 카운트를 통하여 제거 시점을 결정한다.
- 스마트 포인터는 <memory>헤더 안에 정의되어 있다.
📌 스마트 포인터와 원시 포인터의 차이점
- 스마트 포인터는 자유 공간(new/delete로 할당한 공간, malloc/free로 할당한 공간은 힙공간이라고 한다. 서로 크게 다른 개념은 아닌 것 같다.)에 할당한 메모리의 주소만 저장할 수 있다
- 원시 포인터에서 하던 증가, 감소 같은 산술 연산은 스마트 포인터에서 할 수 없다
📌 unique_ptr<T>
- unique_ptr<T>는 타입 T에 대한 포인터처럼 사용되며, 언제나 유일해야 한다. 즉 둘 이상의 unique_ptr이 같은 주소를 가질 수 없으며, unique_ptr은 복사될 수 없다. (유일성을 손상시켜서는 안된다)
- unique_ptr을 옮기려면 오직 소유권을 이동시키는 행위만이 허용된다.
utility헤더에 정의된 std::move()를 사용하면 unique_ptr객체에 저장된 주소를 다른 unique_ptr객체로 이동시킬 수 있으며, 소유권을 이전하면 기존의 unique_ptr객체는 무효화(Invalidate)된다.
- 그러므로, 객체에 대한 단일 소유권만 허용하고 싶을 때는(반드시 하나만 가지고 사용하고 싶을 때는) unqiue_ptr<T>를 이용하는 것이 좋다.
unique_ptr의 사용방법
std::unique_ptr<type\> name {new type()};
ex)
std::unique_ptr<std::string> pname {new std::string{"Algernon"}};
auto pname = std::make_unique<std::string>("Algernon");
auto pstr = std::make_unique<std::string>(6, '*');
스마트 포인터는 원시 포인터와 마찬가지로 역참조를 통해 객체에 접근할 수 있다.
std::cout < *pname << std::endl;
임의의 크기로 스마트 포인터 배열을 생성할 수도 있다.
int len = 10;
std::unique_ptr<int[]> pnumbers{new int[len]};
std::unique_ptr<int[]> pnumbers = std::make_unique<int[]>(len);
unique_ptr 객체는 복제가 불가능 하므로, 함수에 값으로 전달할 수 없다. 만약 unique_ptr객체를 함수의 인수로 쓰고 싶다면 인수를 참조 매개변수로 받아야 한다.
unique_ptr 객체는 복제될 수 없지만 암묵적 이동 연산(implicit move operation)에 의해 반환이 가능하므로 함수에서 반환될 수 있다.
- reset() : 스마트 포인터가 가리키는 원본 객체를 소멸, unique_ptr은 해제된다.
ptr.reset(new std::string{"Fred"})와 같이, reset의 인자에 새 객체를 넣으면 스마트 포인터와 새 객체를 연결해준다.
- release() : 스마트 포인터가 가리키는 원본 객체의 소유권을 해제하고, 원본 객체를 리턴
- get() : unique_ptr 객체끼리 비교하고 싶을 때는 두 객체의 get()을 호출하여 반환된 주소값을 비교한다.
📌 shared_ptr<T>
-
shared_ptr<T>객체는 타입 T에 대한 포인터처럼 행동한다
-
unique_ptr과는 반대로 shared_ptr<T>는 객체를 여러 shared_ptr<T>와 공유할 수 있다
-
shared_ptr<T>는 레퍼런스 카운팅 방식을 사용하여 메모리를 관리하는데 새로운 shared_ptr<T>객체가 주소를 공유받을 때 마다 레퍼런스 카운터가 증가하며, 공유를 받았던 shared_ptr객체가 소뮬되거나, 다른 주소를 할당받거나, nullptr을 할당받으면 해당 객체의 레퍼런스 카운트가 감소한다
-
만약 레퍼런스 카운터가 0이 되면(해당 주소를 가리키는 shared_ptr<T>객체가 없으면) 해당 주소를 위한 힙 메모리가 자동으로 해제된다.
-
새로운 shared_ptr을 정의하면 두 가지 할당을 받게 된다.
첫 번째는 shared_ptr이 가리키는 원본 객체를 위한 힙 메모리를 할당하게 된다.
두 번째는 스마트 포인터의 레퍼런스 카운트를 위한 컨트롤 블록을 위해 스마트 포인터 객체와 관련된 힙 메모리를 할당받게 된다.
shared_ptr의 사용방법
std::shared_ptr<double> pdata {new double(999.0)};
: 일반적인 할당 방법
std::shared_ptr<double> pdata = std::make_shared<double>(999.0);
: 일반적인 할당보다 효율적으로 동작한다. 추천되는 방법이다.
std::shared_ptr<double> pdata2{pdata};
: 다른 shared_ptr로 초기화하는 스마트 포인터. 레퍼런스 카운트가 증가한다.
double *pvalue = pdata.get()
: 스마트 포인터의 원시 포인터 객체를 반환한다. 반드시 사용해야 할 경우가 있을 때만 이렇게 사용해야 하며, get()을 통해 반환한 원시 포인터를 이용해서 shared_ptr<T>를 생성하는 것은 올바르지 못한 행동이며 위험을 초래할 수 있다.
pname.reset(new std::String{"Jane Austen});
- reset()을 인수없이 호출하면 레퍼런스 카운트가 1 감소하며, 해당 shared_ptr객체가 아무것도 가리키지 않게 된다.(unique_ptr의 동작과는 좀 다르다, 물론 레퍼런스 카운트가 1이었다면, 해당 shared_ptr 객체를 reset하는 순간 스마트 포인터가 해제된다.)
- reset()의 인수로 원시 포인터를 전달하면 shared_ptr이 가리키는 주소를 바꿀 수 있다.
shared_ptr끼리의 비교는 == 연산자를 사용하여 비교할 수 있다. 단 두 객체가 모두 nullptr일 수 있으므로, 같은 객체를 가리키는지만 체크하면 안된다.
- 스마트 포인터는 암묵적으로 bool로 변형될 수 있으므로(객체가 있으면 true, nullptr이면 false)
if((pA == pB && (pA != nullptr)) 혹은 if((pA == pB && pA))
로 비교할 수 있다.
- pname.use_count() : use_count()는 해당 shared_ptr이 가리키는 객체의 레퍼런스 카운트를 반환한다
shared_ptr이 nullptr을 가리키고 있다면 0을 반환한다
- unique() : 함수는 shared_ptr이 유일한지(true), 복제본이 있는지(false) 확인할 수 있다.
📌 weak_ptr<T>
- weak_ptr는 shared_ptr의 상호참조 문제를 해결할 수 있다
- weak_ptr은 shared_ptr객체에서 생성하며 연결하며, 같은 주소를 가리킨다
- 단, weak_ptr은 연결된 shared_ptr객체의 레퍼런스 카운트를 증가시키지 않으므로, 객체의 소멸에 관여하지 않는다
- shared_ptr의 메모리가 레퍼런스 카운트가 0이되어 해제되더라도 연관된 weak_ptr객체는 남아있게 된다
📚 참고출처
얌얌코딩 (게임 개발) : https://youtu.be/D-STWJ24Kcs?si=SchohSwV6LRH3nZv