포인터
포인터, 포인터 타입캐스팅, 배열과 포인터, 스마트포인터
7장 - 포인터
1) 포인터란
포인터(*
)는 다른 변수, 혹은 그 변수의 메모리 공간주소를 가리키는 변수를 말한다. 또한 포인터가 가리키는 값을 가져오는 것을 역참조라고 한다. &
연산자를 통해 특정 메모리의 주소를 얻을 수도 있다.
2) 포인터 타입 캐스팅
포인터는 주소를 저장하고 있기에 정수 포인터나 다른 포인터나 그 크기가 같다. 그래서 C스타일 캐스팅을 진행하면 변환이 가능하다.
// 포인터 생성
int* intPtr = nullptr;
long* longPtr = nullptr;
// 그저 대입 -> 오류
intPtr = longPtr;
longPtr = intPtr;
// C스타일 캐스팅 -> 작동
intPtr = (int*)longPtr;
longPtr = (long*)intPtr;
// C++ 정적 캐스팅 -> 오류
intPtr = static_cast<int*>(longPtr);
longPtr = static_cast<long*>(intPtr);
3) 배열과 포인터
앞서 배열은 힙 배열과 스택 배열이 있다고 하였다. 힙 배열에서만 포인터를 사용을 보여주었는데, 스택 배열에서도 포인터를 사용할 수 있다. 애초에 배열의 주소는 첫 번째 원소(배열[0])의 주소이기 때문이다.
int myArray[10] = {};
int* myArrayPtr = myArray;
myArrayPtr[3] = 5;
이와 같이 스택 배열을 포인터로 접근해도 문제되지 않는다. 오히려 함수에 전달할 때 매우 유용하다. 매개변수를 포인터로 받는다면 힙 배열과 스택 배열을 모두 받을 수 있다.
4) 스마트 포인터
메모리의 할당과 해제는 매우 중요한 문제이다. 이 중요한 문제를 사람이 전부 처리하기 보다는, 스마트 포인터를 사용하여 해결하는 것이 좋다. 스코프를 벗어나거나 리셋되면 할당된 리소스가 자동으로 해제된다니 얼마나 좋은가?
스마트 포인터의 종류는 다양하다.
- 리소스 고유 소유권 → 스마트 포인터가 스코프를 벗어나거나 리셋되면 리소스 해제(unique_ptr)
- 래퍼런스 카운팅 → 리소스가 참조된 횟수를 계산하여 참조 카운트가 0이 되거나 리셋되면 리소스 해제(shared_ptr)
5) unique_ptr
unique_ptr은 스마트 포인터의 일종이다. 포인터가 스코프를 벗어나면 직접적인 delete호출 없이도 리소스를 해제한다.
class Fruit{ /*...*/ }
auto myUniquePtr = make_unique<Fruit>(); // C++14 이후
unique_ptr<Simple> myUniquePtr(new Fruit()); // C++14 이전
사용법은 기존의 포인터와 동일하다. *
, ->
를 그대로 사용한다.
myUniquePtr -> name;
(*myUniquePtr).name;
(myUniquePtr.get()).name; // get() 메소드는 포인터가 가리키는 메모리 주소를 반환
get()
이외에도 reset()
함수도있다. 그냥 reset()
만 사용하면 nullptr로 초기화 하는 것이고, reset(new Fruit())
처럼 사용하면 새로운 리소스로 연결하는 것이다.
6) shared_ptr
이것 또한 스마트 포인터이며, unique_ptr의 사용법과 비슷하다. 다른 점은 reset()
호출시 카운팅 메커니즘에 따라 마지막 포인터가 제거되거나 리셋될 때 리소스가 해제된다는 것이다. 사용시 중복 삭제 문제에 주의해야 한다.
한 객체를 두 포인터가 추적할 때 생성자는 한 번 호출되고 소멸자는 두 번 호출되는 현상을 중복 삭제라고 한다.
class Fruit {
public:
Fruit() { cout << "생성" << endl; }
~Fruit() { cout << "소멸" << endl; }
};
int main() {
Fruit* myFruit = new Fruit();
shared_ptr<Fruit> smartPtr1(myFruit);
shared_ptr<Fruit> smartPtr2(myFruit);
}
unique_ptr, shared_ptr에서 모두 볼 수 있는 증상이다. 이를 방지하기 위해서는 한 포인터가 다른 포인터를 추적하는, 다르게 말해서 복사본을 만들면 된다.
Fruit* myFruit = new Fruit();
shared_ptr<Fruit> smartPtr1(myFruit);
shared_ptr<Fruit> smartPtr2(smartPtr1); // 기존 참조를 복사
*이미지 첨부가 안된다… 결과는 생성, 소멸이 각각 1회 출력된다.
7) weak_ptr
shared_ptr가 가리키는 리소스의 레퍼런스를 관리하는데 사용된다. weak_ptr은 리소스를 직접 소유하지 않기 때문에, 리소스의 생성과 삭제에 관여하지 않는다. 다만 shared_ptr이 그 리소스를 해제했는지 알아낼 수는 있다.
weak_ptr에 접근하는 방법은 두 가지이다.
- weak_ptr의
lock()
메소드를 사용하여 shared_ptr 리턴받기 - shared_ptr의 생성자에 weak_ptr을 인수로 전달해서 새로운 shared_ptr 생성하기
class Fruit {
public:
Fruit() { cout << "생성" << endl; }
~Fruit() { cout << "소멸" << endl; }
};
void useResource(weak_ptr<Fruit>& weak) {
auto resource = weak.lock();
if (resource) {
cout << "리소스 살아 있음" << endl;
}
else {
cout << "리소스 전부 죽음" << endl;
}
}
int main() {
auto mySharedPtr = make_shared<Fruit>(); // shared_ptr 생성
weak_ptr<Fruit> myWeakPtr(mySharedPtr);
useResource(myWeakPtr); // 리소스 살아 있음
mySharedPtr.reset(); // 포인터 해제
useResource(myWeakPtr); // 리소스 전부 죽음
return 0;
}