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);
}

Untitled

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;
}

Untitled

참고 자료