1. 개념
1. 스마트 포인터
C++에서 제공하는 스마트 포인터는 자동 메모리 관리를 제공하는 포인터 클래스입니다. 스마트 포인터는 일반 포인터와 같이 참조/역참조 연산이 가능하며, 가리키는 객체의 수명이 끝나면 자동적으로 메모리 해제를 수행하여 메모리 릭을 방지합니다. 따라서, 사용자는 스마트 포인터 사용을 통해 동적 할당 받은 메모리 영역에 대한 명시적인 해제 작업에 대한 부담 없이 안전하고, 효율적인 코드 작성이 가능합니다.
2. auto_ptr
1. 사용 금지!!!!
C++ 11 표준 이전에 제공되던 "auto_ptr"은 심각한 단점을 안고 있습니다. "auto_ptr"은 vector와 같은 STL 컨테이너에서는 정상적을 작동되지 않습니다! 따라서, "auto_ptr"는 사용하지 맙시다!
3. unique_ptr
1. 개념
unique_ptr는 C++에서 제공하는 스마트 포인터 클래스 중 하나로, 하나의 unique_ptr 유형의 스마트 포인터는 오직 하나의 객체만 소융하는 '단일 소유권' 개념을 갖는 스마트 포인터입니다. 쉽게 말해, unique_ptr는 어떤 객체에 대하여 항상 1:1 관계가 강제되며, '복사' 작업을 방지하고 오직 '이동' 작업만 허용합니다.
2. 특징
- 단일 소유권
- 자동 메모리 해제
- 빠른 속도
3. 헤더 파일
#include <memory>
3. 코드
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include<memory>
using namespace std;
class SomeClass
{
public:
SomeClass(int a, int b)
: val1(a), val2(b)
{}
~SomeClass() = default;
int getVal1() { return val1; }
int getVal2() { return val2; }
private:
int val1;
int val2;
};
void PrintFunc()
{
// auto 사용도 가능!
unique_ptr<SomeClass> ptr = make_unique<SomeClass>(3, 5);
int val1 = ptr->getVal1();
int val2 = ptr->getVal2();
cout << val1 << " " << val2 << endl;
}
int main()
{
PrintFunc();
}
위 코드 예제에서 "SomeFunc()" 함수는 구현부에서 "SomeClass"의 객체를 동적 할당받습니다. 이때, "unique_ptr"를 통해 동적 할당받은 "SomeClass"객체는 함수의 종료와 함께 자동으로 해당 자원을 해제합니다.
추가적으로, (->)를 통해 역참조 또한 가능한것을 볼 수 있습니다!
#4. shared_ptr
1. 개념
shared_ptr는 C++에서 제공하는 스마트 포인터 클래스 중 하나로, 레퍼런스 카운팅을 통해 객체 수명을 관리합니다. shared_ptr가 가리키는 객체는 내부적으로 레퍼런스 카운팅을 수행하며, 새로운 shared_ptr가 해당 객체를 가리키면 카운트가 증가하고, 가리키고 있던 shared_ptr가 소멸되거나, 가리키는 변수를 변경하면 카운팅이 감소되며, 최종적으로 카운트가 0이되면 자동적으로 해당 객체의 자원 해제를 수행합니다. shared_ptr의 경우 unique_ptr와 달리 복사/이동 작업 모두 가능하며, 어떤 객체에 대하여 여러 소유자가 필요하고 안전한 메모리 관리를 위해 활용됩니다.
2. 특징
- 레퍼런스 카운팅
- 공동 소유권
- 자동 메모리 해제
- 스레드 안전
- 커스텀 삭제자
- 순환 참조 문제, weak_ptr 활용
3. 스레드 안정성
shared_ptr는 '스레드 안정성'을 보장합니다. shared_ptr는 공동 소유권을 제공하며 하나의 객체에 대하여 여러 shared_ptr 유형의 포인터 변수가 가리키는 것이 가능합니다. 이때, 객체에 대한 동시 읽기/쓰기 작업들이 이루어질 경우 그 결과가 달라질 수 있습니다. 따라서, 객체 내부에서 레퍼런스 카운팅을 수행할 때 원자적 연산을 수행함으로써 공동 소유권을 보장함과 동시에 객체 정보의 일관성을 유지합니다. 이러한 레퍼런스 카운팅 작업은 shared_ptr가 관리하는 별도의 제어 블록에서 이루어지며, 이는 shared_ptr 생성 시 함께 생성되는 동적 할당된 메모리 영역입니다. * 레퍼런스 카운팅과 객체는 직접적인 연관 관계가 없습니다.
4. 헤더 파일
#include <memory>
5. 코드
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include<memory>
using namespace std;
class SomeClass
{
public:
SomeClass(int a, int b)
: val1(a), val2(b)
{}
~SomeClass() = default;
int getVal1() { return val1; }
int getVal2() { return val2; }
private:
int val1;
int val2;
};
int main()
{
shared_ptr<SomeClass> ptr = make_shared<SomeClass>(3, 5);
int val1 = ptr->getVal1();
int val2 = ptr->getVal2();
cout << val1 << " " << val2 << endl;
}
"shared_ptr"도 "unique_ptr"과 별반 다르지 않습니다. 포인터 초기화를위해 "make_shared"를 사용하는 것과, 동적으로 할당받은 C 스타일 배열을 담을 수 없다는 점만 "unique_ptr"와 다릅니다. "shared_ptr"의 제일 중요한 특징은 레퍼런스 카운팅입니다!
#5. weak_ptr
1. 개념
weak_ptr는 C++에서 제공하는 스마트 포인터 클래스 중 하나로 shared_ptr가 관리하는 객체에 대한 약한 참조를 제공합니다. weak_ptr는 객체 수명에 영향을 주지 않으며, 레퍼런스 카운트를 증가시키지 않습니다. weak_ptr 활용의 주요 목적은 순환 참조 문제를 방지하고, 생성 시점에 해당 객체가 아직까지 유효한지 확인하기 위함입니다. weak_ptr를 통해 객체에 직접적으로 접근하는 것이 불가능하며, 접근을 위해선 lock() 메서드를 통해 weak_ptr를 shared_ptr로 변환해야 합니다.
2. 특징
- 약한 참조
- 순환 참조 방지
- 직접 접근 불가, lock() 사용 shared_ptr로 변환 후 접근
3. 헤더
#include <memory>
4. 코드
#include <iostream>
#include <memory>
class B; // 전방 선언
class A {
public:
std::shared_ptr<B> b_ptr;
A() { std::cout << "A 생성\n"; }
~A() { std::cout << "A 소멸\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // weak_ptr 사용
B() { std::cout << "B 생성\n"; }
~B() { std::cout << "B 소멸\n"; }
void doSomething() {
if (auto a = a_ptr.lock()) { // shared_ptr로 변환 시도
std::cout << "A 객체 존재, 작업 수행\n";
} else {
std::cout << "A 객체 이미 삭제됨\n";
}
}
};
int main() {
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
b->doSomething(); // A 객체 존재
} // a와 b의 수명이 끝남
// 메모리 누수 없이 A와 B 모두 정상적으로 소멸됨
return 0;
}
#6. 레퍼런스 카운팅
1. 개념
레퍼런스 카운팅은 동적 메모리 관리 기법 중 하나로, 특히 shared_ptr와 같은 스마트 포인터에서 중요한 역할을 합니다. 레퍼런스 카운팅은 특정 객체의 참조 횟수를 추적합니다. 따라서, 특정 객체에 대하여 새로운 참조가 생성될 때마다 카운터가 증가하며, 참조가 제거될 때마다 카운트가 감소합니다. 레퍼런스 카운팅의 단점은 추가적인 메모리 공간을 필요로 하며, 순환 참조 문제가 발생할 수 있습니다.
2. 순환 참조
순환 참조 문제는 스마트 포인터, 특히 shared_ptr를 사용할 때 발생 가능한 문제로, 두 개 이상의 객체가 서로를 참조하여 순환적인 의존 관계를 형성하는 상황을 의미합니다. 순환 참조 상황 발생 시 두 객체가 서로를 지속적으로 참조하여 레퍼런스 카운팅이 0이 되지 않아 메모리에서 자동적으로 해제되지 않습니다. 이러한 문제 발생을 방지하기 위해 적절한 설계 작업과 "weak_ptr"를 통한 약한 참조를 활용하는 방법이 있습니다.
3. 코드
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include<memory>
using namespace std;
class Nothing
{
public:
Nothing() { cout << "생성자 호출" << endl; }
virtual ~Nothing() { cout << "소멸자 호출" << endl; }
};
void DoubleDelete()
{
shared_ptr<Nothing> nothingPtr1 = make_shared<Nothing>();
shared_ptr<Nothing> nothingPtr2(nothingPtr1);
}
int main()
{
DoubleDelete();
}
위 예제 코드를 살펴보면, "DoubleDelete()" 함수 내부에서 동적 할당받은 "Nothing" 클래스 객체를 가리키는 스마트 포인터를 2개 선언했습니다. 스코프를 벗어나 해제되는 스마트 포인터가 2개임에도 불구하고, "소멸자는 한 번
만 호출됩니다!"
#7. 스마트 포인터와 이동 시맨틱
1. 스마트 포인터의 이동 시맨틱
shared_ptr<Nothing> func()
{
auto ptr = make_shared<Nothing>();
return ptr;
}
int main()
{
shared_ptr<Nothing> myPtr = func();
}
이동 시맨틱을 복기해보자면, 대입 원본 객체가 임시 객체여서 대입 대상으로의 복제 또는 대입이 끝난 후에 원본 객체의 멤버를 null 값으로 초기화시킵니다."얕은 복제"를 수행하죠! "func()" 함수는 보시다시피 "shared_ptr"를 반환합니다. 이때, C++는 자동으로 "std::move()"를 적용하여 shared_ptr의 이동 시맨틱이 동작되기 때문에 효율적입니다! unique_ptr 또한 동일하게 적용됩니다. unique_ptr의 경우 "이동 대입 연산"은 지원하는 대신에 일반 대입 연산자 혹은 복제 생성자를 지원하지 않습니다!
'언어 > Basic C++' 카테고리의 다른 글
[Basic C++] #28_static 키워드, 링킹, namespace (0) | 2022.10.09 |
---|---|
[Basic C++] #61_객체 풀, Obejct Pooling (0) | 2022.10.07 |
[Basic C++] #59_가비지 컬렉션 (1) | 2022.09.27 |
[Basic C++] #58_포인터, 배열과 포인터, 포인터 연산, 함수 포인터, 클래스 메서드 포인터 (0) | 2022.09.22 |
[Basic C++] #57_동적 메모리 (1) | 2022.09.20 |