INDEX
기존 포인터의 문제점
스마트 포인터들
- unique_ptr
- 중요!!
- shared_ptr
- weak_ptr
기존 포인터의 문제점
- 더이상 포인터가 필요하지 않을 때 메모리를 해제해야함
- 까먹으면 메모리 누수…
- 스마트 포인터를 쓰면 delete를 직접 호출할 필요가 없다
- 가비지 컬렉션보다 빠르다
unique_ptr
유니크 포인터
-
std::unique_ptr
-
포인터(원시 포인터)를 단독으로 소유
- 나 말고 누구도 쓰지 못하게 만드는 포인터
-
원시 포인터는 누구하고도 공유되지 않음
-
따라서 복사나 대입 불가
-
uique_ptr가 범위를 벗어날 때 원시 포인터는 지워짐
std::unique_ptr<vector> myVector(new Vector(10.f, 30.f)); std::unique_ptr<vector> copiedVector1 = myVector; //컴파일 에러 std::unique_ptr<vector> copiedVector2(myVector); //컴파일 에러
예시 : 유니크 포인터 만들기(C++)
#include<memory> #include"Vector.h" int main() { //포인터 부호(*)가 없음 std::unique_ptr<vector> myVector(new Vector(10.f, 30.f)); //포인터처럼 동작 myVector->Print(); //delete가 없음 return 0; }
유니크 포인터는 다음 세가지 경우에 적합하다
-
클래스에서 생성자/ 소멸자
OLD
// Player.h class Player { private: Vector* mLocation; }; // Player.cpp Player::Player(std::string name) :mLocation(new Vector()) { } Player::~Player() { delete mLocation; }
NEW
// Player.h class Player { private: std::unique_ptr<Vector> mLocation; }; // Player.cpp Player::Player(std::string name) : mLocation(new Vector()) { } Player::~Player() { //소멸자에 delete 키워드 없음 }
-
지역 변수
OLD
#include "Vector.h" int main() { Vector* vector = new Vector(10.f, 30.f); vector->Print(); delete vector; //... }
NEW
#include<memory> #include"Vector.h" int main() { std::unique_ptr<Vector> vector(new Vector(10.f, 30.f)); vector->Print(); //... }
-
STL벡터에 포인터 저장하기
OLD
#include <vector> #include "Player.h" int main() { std::vector<Plyaer*> players; player.push_back(new Player("Lulu")); player.push_back(new Player("Coco")); for(int i =0; i<player.size();i++) { delete players[i]; } players.clear(); //... }
NEW
#include <vector> #include "Player.h" int main() { std::vector<std::unique_ptr<Player>> playerList; playerList.push_back(std::unique_ptr<Player>(new Player("Lulu"))); playerList.push_back(std::unique_ptr<Player>(new Player("Lulu"))); //사실 clear()도 안해도 된다. scope를 나가면 사라지기 때문 players.clear(); //... }
문제 : 원시 포인터를 공유
Vector* vectorPtr = new Vector(10.f, 30.f); std::unique_ptr<Vector> vector(vectorPtr); std::unique_ptr<Vector> anotherVector(vectorPtr); anotherVector = nullptr;
- 위의 상황에서 포인터가 공유가 됨.
- 포인터를 단독으로 소유한다는 개념이 깨짐
C++ 14 이후 해결책
#include<memory> #include"Vector.h" int main() { // 힙 할당 불필요. 단지 std::make_unique<> 사용법을 보여주기 위함 std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f, 30.f); myVector -> Print(); return 0; }
- std::make_unique()를 이용해서 문제 보안
std::unique()가 무슨 일을 할까?
-
주어진 매개변수와 자료형으로 new키워드를 호출해줌
-
따라서 우너시 포인터와 같음
-
둘 이상으 std::unique_ptr가 원시 포인터를 공유할 수 없도록 막는게 전부
-
아래의 상황을 막아줌
Vector* vectorPtr = new Vector(10.f, 30.f); //아래는 컴파일 에러 std::unique_ptr<Vector> vector1 = std::make_unique(vectorPtr); std::unique_ptr<Vector> vector2 = std::make_unique<Vector>(vectorPtr); std::unique_ptr<Vector> vector3 = std::make_unique<Vector*>(10.f,30.f);
유니크 포인터 만들기
//non-array template< class T, class... Args > unique_ptr<T> make_unique(Args&&... args); //array template< class T> unique_ptr<T> make_unique(std::size_t size);
//C++11 (C++14를 쓸 수 없는 경우) std::unique_ptr<vector> vector(new Vector(10.f, 30.f)); std::unique_ptr<Vector[]> vectors(new Vector[20]); //C++14 (이걸 쓰자) std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f); std::unique_ptr<Vector[]> vectors = std::make_unique<Vector[]>(20);
유니크 포인터 재설정하기
-
` void reset(pointer ptr = pointer()) noexcept; `
-
포인터를 교체한다
-
std::unique_ptr가 재설정될 때, 소유하고 있던 원시 퐁니터는 자동으로 소멸됨
//std::unique_ptr<Vector> myVector; myVector.reset(new Vector(20.f, 40.f)); myVector.reset();
예시 : reset()
int main() { std::unique_ptr<Vector> vector = std::make_unique<Vector>(20.f, 30.f); vector.reset(new Vector(20.f, 40.f)); vector.reset(); //… }
reset() vs nullptr
reset()
std::unique_ptr<Vector> vector; vector.reset()
nullptr
std::unique_ptr<Vector> vector; vector = nullptr;
- 두 코드는 같음
- nullptr가 reset()보다 가독성이 높음
- 허나 rest()은 vector가 원시 포인터가 아님을 분명하게 보여줌
포인터 가져오기
-
` pointer get() const noexcept; `
-
원시 포인터를 반환한다
//std::unique_ptr<Vector> vector; Vector* ptr = vecotr.get();
예시 : get()
Vector.cpp
void Vector::Add(const Vector* other) { mX += other->mX; mY += other->mY; }
Main.cpp
#include<memory> #include"Vector.h" int main() { //힙 할당 불필요 std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f); std::unique_ptr<Vector> anotherVector = std::make_unique<Vector>(20.f, 40.f); vector->Add(anotherVector.get()); vector->Print(); return 0; }
포인터 소유권 박탁하기
-
pointer release() noexcept;
-
원시 포인터에 대한 소유권을 박탁하고 원시 포인터를 반환한다
- 이걸 쓰는건 좋은 생각은 아니다… get()을 써서 해결할 수 있으면 그걸 쓰자
- 아니면 소유권 이전을 쓰자…
-
release()호출 후 get()을 호출하면 nullptr가 반환됨
//vector는 유니크 포인터 Vector* vectorPtr = vector.release(); Vector* vectorPtr2 = vector.get(); //nullptr
예시 : release()
int main() { std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f); Vector* vectorPtr = vector.release(); //... }
유니크 포인터 소유권 이전
- std::unique_ptr는 소유한 운시 포인터를 아무하고도 공유하지 않음
- 즉, 주소를 복사하지 않는다는 뜻
- 대신 소유권을 다른 std::uniuqe_ptr로 옮길 수 있음
- 예외 : const std::unique_ptr
std::move()
- 개체 A의 모든 멤버를 포기하고 그 소유권을 B에게 주는 방법
- 메모리 할당과 해제가 일어나지 않음
- 간단하게 A에 있는 모든 포인터를 B에게 대입하고, A에는 nullptr를 대입한다고 생각하자
- “나는 멤버변수를 옮기고 있다(MOVING)”
- 이게 어떻게 도는지 알려면 r-value와 이동(move) 생성자를 배워야 함
예시 : 유니크 포인터 소유권 이전하기
#include<memory> #include"Vector.h" int main() { std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f); std::unique_ptr<Vector> anotherVector(std::move(vector)); // ... }
예시 : STL벡터에 요소 추가하기
std::vector<std::unique_ptr<Player>> players; std::unique_ptr<Player> coco = std::make_unique<Player>("Coco"); players.push_back(std::move(coco)); std::unique_ptr<Player> lulu = std::make_unique<Player>("Lulu"); players.push_back(std::move(lulu));
std::unique_ptr의 비밀 공개
매워 단순화 시킨 예시
template<typename T> class unique_ptr<T> final { public: unique_ptr(T* ptr) : mPtr(ptr) {} ~unique_ptr() { delete mPtr; } T* get() { return mPtr; } unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete; private: T* mPtr = nullptr; }
std::unique_ptr베스트 프랙티스
- 이제 다들 이걸 씀
- 직접 메모리를 관리하는 것만큼 빠름
- RAII(Resource Acquisition Is Initialization)원칙에 잘 들어맞음
- 자원 할당은 개체의 수명과 관련되어 있음
- 생성자에서 new 그리고 소멸자에서 delete
- std::unique_ptr 멤버 변수가 이걸 해줌
- 실수하기 어려움
- 모든 곳에 쓰자!
자동 메모리 관리
자동 메모리 관리
- 가비지 컬렉션
- Java와 C#에서 지원
- 참조 카운팅
- Switft와 애플 Objective-C에서 지원
GC나 RefCount를 쓰면 메모리 누수가 없다?
- 전통적인 메모리 누수는 없음
- 예) delete를 잊었다
- 하지만 여전히 메모리 누수가 발생할 수 있음
- 예) 순환참조
가비지 컬렉션 vs 참조 카운팅
- 가비지 컬렉션
- 확실히 사용하기 더 쉬움
- 실시간 또는 고성능 프로그램에 적합하지 않음
- 3분마다 2초씩 정지하는 영화를 보고 싶을까?
- 아이폰 게임보다 버벅거리는 안드로이드 게임이 있나?
- 참조 카운팅
- 여전히 사용하기 쉬움
- 실시간 또는 고성능 프로그램에 적합
- 멀티 스레드 환경에서는 순수한 포인터보다 훨씬 느림
가비지 컬렉션
가비지 컬렉션
-
보통 트레이싱 가비지 컬렉션을 의미
-
메모리 누수를 막으려는 시도
-
주기적으로 컬렉션 실행
-
충분한 여유 메모리가 없을 때 컬렉션이 실행됨
-
- 스케쥴에 따라 또는 수동으로도 실행 가능
-
매 주기마다 GC는 루트를 확인함
-
- 전역변수
- 스택
- 레지스터
-
힙에 있는 개체에 루트를 통해 접근할 수 있는지 판단
- 접근할 수 없다면, 가비지로 간주해서 해제
가비지 컬렉션의 문제점
- 사용되지 않는 메모리를 즉시 정리하지 않음
- GC가 메모리를 해제해야 하는지 판단하는 동안 어플리케이션이 멈추거나 버벅일 수 있음
참조 카운팅
참조 카운팅
-
가비지 컬렉션처럼, 개체에 대한 참조가 없을 때 개체가 해제됨
-
언제든 참조 횟수를 활용해서 특정 개체가 몇번이나 참조되고 있는지 판단 가능
-
어떤 개체 A를 다른 개체 B가 참조할 때 횟수가 늘어남
-
B가 참조를 그만둘 때 횟수가 줄어듦
-
- 예) B가 범위(scope)를 벗어나는 경우
예시 : 수동 참조 카운팅
- COM(즉 DirectX)이 수동 참조 카운팅을 지원
- std::shared_ptr는 이걸 자동으로 해줌
강한 참조
- 강한 참조란 개채 A가 개채 B를 참조할 때, 개채 B는 절대 소멸되지 않음을 의미
- 강한 참조의 수를 저장하기 위해 강한 참조 카운트를 사용
- 일반적으로 새 인스턴스, 즉 개채에 대한 참조를 만들 때 강한 참조 횟수가 늘어남
- 강한 참조 횟수가 0이 될 때 개채는 소멸됨
참조 카운팅의 문제점
-
참조 횟수는 너무 자주 바뀜
-
- 멀티 스레드 환경에서 안전하려면, lock이나 원자적(atomic)연산이 필요
- ++mRefCount보다 확연히 느림
-
순환 참조
-
- 개채 A가 B를 참조
- 개채 B가 A를 참조
- 절대 해제되지 않음
- C++에 해결책이 있음
shared_ptr
std::shared_ptr
-
두 개의 포인터를 소유
-
- 데이터(원시 데이터)를 가리키는 포인터
- 제어 블록을 가리키는 포인터
-
std::unique_ptr와 달리, 포인터를 다른 std::shared_ptr와 공유할 수 있음
-
참조 카운팅 기반
-
원시 포인터는 어떠한 std::shared_ptr에게도 참조되지 않을 때 소멸됨
std::shared_ptr만들기
//배열이 아님 template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
-
공유 포인터를 만든다
std::shared_ptr<Vector> vector = std::make_shared<Vecto>(10.f, 30.f);
예시 : std::shared_ptr 만들기
#include<memory> #include"Vectorh.h" int main() { std::shared_ptr<Vector> vector = std::make_shared<Vecto>(10.f, 30.f); // ... }
포인터 소유권 공유하기
shared_ptr& operator= (const shared_ptr& x) noexcept; template<class U> shared_ptr& operator= (const shared_ptr<U>& x) noexcept;
-
포인터의 소유권을 공유한다
std::shared_ptr<Vector> copiedVector = vector;
예시 : 포인터 공유하기
int main() { std::shared_ptr<Vector> vector = std::make_shared<Vector>(10.f, 30.f); std::shared_ptr<Vector> copiedVector = vector; // ... }
포인터 재설정하기
void reset() noexcept;
-
원시 포인터를 해제한다
-
참조 카운트가 1줄어듦
-
nullptr를 대입하는 것과 같음
// vector는 공유 포인터 vector.reset(); vector = nullptr;
예시 : 포인터 재설정하기
int main() { st::shared_ptr<Vector> vector = std::make_shared<Vector>(10.f, 30.f); std::shared_ptr<Vector> copiedVector = vector; // vector.reset()을 하면 강함 참조 횟수가 2에서 1로 줄어듦 vector.reset(); // vector = nullptr; 과 같음 // ... }
참조 횟수 구하기
` long use_count() const noexcept; `
-
원시 포인터를 참조하고 있는 std::shared_ptr의 갯수를 반환함
// vector는 공유 포인터 long number = vector.use_count()
예시 : 순환참조
//Person.h #include <iostream> #include <string> #include "Pet.h" class Pet; class Person { public: void SetPet(const std::shared_ptr<Pet>& pet); Person(const std::string& name); Person() = delete; ~Person(); private: std::string mName; std::shared_ptr<Pet> mPet; };
//Pet.h #include <iostream> #include <string> #include "Person.h" class Person; class Pet { public: void SetOwner(const std::shared_ptr<Person>& owner); Pet(const std::string& name); Pet() = delete; ~Pet(); private: std::string mName; std::shared_ptr<Person> mOwner; };
//Main.cpp #include<memory> #include"Person.h" #include"Pet.h" int main() { std::shared_ptr<Person> owner = std::make_shared<Person>("Pope"); std::shared_ptr<Pet> pet = std::make_shared<Pet>("Coco"); std::cout << "Owner : "<< owner.use_count() << "Pet : " << pet.use_count() << std::endl; owner->SetPerson(pet); pet->SetOwner(owner); std::cout << "Owner : "<< owner.use_count() << "Pet : " << pet.use_count() << std::endl; return 0; }
Result
- 소멸자 호출 안됨!
- 메모리 누수가 난다
어떻게 고칠까?
- 또다른 스마트 포인터로 해결
- std:;weak_ptr
weak_ptr
약한 참조
- 약한 참조는 원시 포인터 해제에 영향을 끼치지 않음
- 약한 참조 카운트 약한 참조의 수를 저장하는데 사용됨
- 그리고 이건 순환론적 정의
- 약한 참조로 참조되는 개체는 강한 참조 카운트가 0이 될 때 소멸됨
- 순환 참조 문제의 해결책
std::weak_ptr만들기
tempalte <class U> weak_ptr& operator= (const shared_ptr<U>& x) noexcept;
-
공유 포인터에서부터 약한 포인터를 만든다
// owner는 공유 포인터 std::weak_ptr<Person> weakOwner = owner;
예시 : std::weak_ptr 만들기
#include<memeory> #include"Person.h" int main() { std::shared_ptr<Person> owner = std::make_shared<Person>("Pope"); std::weak_ptr<Person> weakOwner = owner; return 0; }
약한 포인터로 공유 포인터 만들기
std::shared_ptr<T> lock() const noexcept;
-
원시 포인터가 존재하면 공유 포인터를 만든다
// std::shared_ptr<Person> owner = std::make_shared<Person>("Lulu"); // std::waek_ptr<Person> weakOwner = owner; std::shared_ptr<Person> lockedOwner = weakOwner.lock()
예시 : 약한 포인터로 공유 포인터 만들기
#include<memory> #include"Person.h" int main() { std::shared_ptr<Person> owner = std::make_shared<Person>("Lulu"); std::waek_ptr<Person> weakOwner = owner; std::shared_ptr<Person> lockedOwner = weakOwner.lock() return 0; }
공유 포인터가 존재하는지 확인하기
bool expired() const noexcept;
-
공유 포인터가 해제되어있으면 true를 반환한다
//std::shared_ptr<Person> owner = std::make_shared<Person>("Lulu"); //std::weak_ptr<Person weakOnwner = owner; bool bIsExpired = weakOwner.expired(); //false
예시 : 공유 포인터가 존재하는지 확인하기 1
#include<memory> #include<"Person.h" int main() { std::shared_ptr<Person> owner = std::make_shared<Person>("Lulu"); std::weak_ptr<Person> weakOwner = owner; auto ptr = weakOwner.lock(); if(ptr == nullptr) { //이하 코드 생략 } return 0; }
예시 : 공유 포인터가 존재하는지 확인하기 2
#include<memory> #include"Person.h" int main() { std::shared_ptr<Person> owner = std::make_shared<Person>("Pope"); std::weak_ptr<Person> weakOwner = owner; if(weakOwner.expired()) // false 반환 { // ... } return 0; }
#include<memory> #include"Person.h" int main() { std::shared_ptr<Person> owner = std::make_shared<Person>("Pope"); std::weak_ptr<Person> weakOwner = owner; owner = nullptr; if(weakOwner.expired()) // true 반환 { // ... } return 0; }
- 허나 완전하지 않음
- 위쪽의 경우 멀티스레드 환경에서는 불안하다….
- Lock 걸어야 한다
약한 포인터로 순환 참조를 해결해보자
Person.h
#include <iostream> #include <string> #include "Pet.h" class Pet; class Person { public: void SetPet(const std::shared_ptr<Pet>& pet); Person(const std::string& name); Person() = delete; ~Person(); private: std::string mName; std::shared_ptr<Pet> mPet; };
Pet.h
#include <iostream> #include <string> #include "Person.h" class Person; class Pet { public: void SetOwner(const std::shared_ptr<Person>& owner); Pet(const std::string& name); Pet() = delete; ~Pet(); private: std::string mName; std::weak_ptr<Person> mOwner; };
- 소멸자 호출!!
- shared_ptr에서 순환참조시 소멸자 호출 안되는 문제 해결!
비교 : 강한참조 약한참조
std::whared_ptr | std::weak_ptr |
---|---|
강한참조 | 약한참조 |
강한 참조 카운트를 늘림 | 약한 참조 카운트를 늘림 |
직접적으로 사용할 수 있음 | 직접적으로 사용할 수 없음 |
원시포인터가 확실히 존재하기 때문 | lock을 써서 std::shared_ptr가 여전히 존재하는지 확인해야 함 |