C++에서 진짜 너무 많이 등장하는 개념이 바로 Pointer이다. 메모리의 주소를 관리하고~ 뭐시기~ 항상 등장하는데, 막상 지속적으로 사용하지 않으니 게속해서 까먹는? 대표적인 개념이라고 할 수 있다. 그렇기 때문에 이번에 포인터든 스마트포인터든 한번에 다 정리해보도록 하겠다.
포인터는 그냥 메모리 주소를 담는 변수이다. int* p 는 p가 가르키는 곳에 int가 있다는 뜻이다.
변수 x [주소 0x1000] 값: 5
포인터 p [주소 0x2000] 값: 0x1000 ← p는 x의 주소를 저장
int x = 5;
int* p = &x;
void f(int* p)
{
*p = 10;
}
포인터 자체를 바꾸고 싶을때 사용한다. 더블포인터는 포인터를 가르키는 포인터이다.
변수 x [0x1000] 값: 5
포인터 p [0x2000] 값: 0x1000 ← p는 x를 가리킴
더블포인터 pp [0x3000] 값: 0x2000 ← pp는 p를 가리킴
int x = 5;
int* p = &x; // p: int를 가리키는 포인터, p의 값은 x의 주소
int** pp = &p; // pp: "int*를 가리키는 포인터", pp의 값은 p의 주소
void allocate(int** pp) {
*pp = new int(42); // 호출자의 p가 이제 새 메모리를 가리키게 됨
}
int* p = nullptr;
allocate(&p);
// 이제 p != nullptr, *p == 42
⚠️ 여기서 잠깐!
void makeNull(int* p) {
p = nullptr;
}
int x = 5;
int* ptr = &x;
makeNull(ptr);
// ptr은 여기서 nullptr일까요, 여전히 &x일까요?
똑같은 논리를 이용해서 더블포인터 자체를 바꿀 수 있다.
void createMatrix(int*** outMatrix, int rows, int cols) {
*outMatrix = new int*[rows]; // 호출자의 matrix가 이 배열을 가리키게 함
for (int i = 0; i < rows; i++)
(*outMatrix)[i] = new int[cols];
}
int** matrix;
createMatrix(&matrix, 3, 4);
두 개의 int* 변수 p1, p2가 있을 때, 이 둘이 서로 가리키는 대상을 맞바꾸고 싶다고 해봅시다 (예: p1이 x를 가리키고 p2가 y를 가리켰다면, 함수 호출 후엔 p1이 y를, p2가 x를 가리키게).
void swapPointers( /* 여기 파라미터 타입은? */ ) {
// 여기 구현은?
}
int main() {
int x = 10, y = 20;
int* p1 = &x;
int* p2 = &y;
swapPointers(p1, p2); // 호출 후 p1은 y를, p2는 x를 가리켜야 함
}
(* 여기서 x와 y의 값은 그대로 두고 포인터가 가르키는걸 서로 바꿔야 하는거임)
int main() {
int a = 2;
const int *ptr = &a;
a = 1; // 컴파일 통과
*ptr = 10; // 오류
}
int main() {
int a = 2;
int b = 1;
int *const ptr = &a;
a = 10; // 컴파일 통과
ptr = &b; // 오류
}
| 기호 | 쓰이는 위치 | 의미 |
|---|---|---|
* | 타입 선언 (int* p) | “p는 포인터 타입이다”라고 선언 |
* | 표현식 (*p) | 역참조 — “p가 가리키는 곳으로 가서 그 값을 읽거나 써라” |
& | 타입 선언 (int& r) | “r은 레퍼런스 타입이다”라고 선언 |
& | 표현식 (&x) | 주소 연산자 — “x라는 변수의 주소를 알려줘” |
int x = 10;
int& r = x; // 여기 &는 주소 연산자가 아니라, "r은 int에 대한 레퍼런스다"라는 타입 선언!
x (별명: r)
┌────────┐
│ 10 │ ← 주소는 하나뿐, 이름표만 x와 r 두 개
└────────┘
int x = 10, y = 20;
int* p = &x;
p = &y; // OK! 이제 p는 y를 가리킴 (포인터는 재할당 가능)
int& r = x;
r = y; // 이건 "r이 y를 가리키게 재바인딩"이 절대 아님!
// 그냥 "r(=x)에 y의 값을 대입"하는 거라, x가 20이 됨. r은 여전히 x의 별명.
int x = 10;
int* p = &x;
int** pp = &p;
int*** ppp = &pp;
int***& r0 = ppp; // ppp 자체의 별명 (타입: int***)
int**& r1 = *ppp; // *ppp (=pp)의 별명 (타입: int**)
int*& r2 = **ppp; // **ppp (=p)의 별명 (타입: int*)
int& r3 = ***ppp; // ***ppp (=x)의 별명 (타입: int, 진짜 값 10)
void foo() {
int* p = new int(10);
// ... 여러 로직 ...
if (someCondition) return; // 여기서 return하면 delete를 못 함! → 메모리 누수
// ... 예외가 발생할 수도 있음 ...
delete p;
}
C++에는 “스택에 있는 객체는 스코프를 벗어나는 순간 소멸자가 호출된다”라는 강력한 보장이 있다. 이를 이용해서 소멸할때 삭제되게 만든 것이다.
template<typename T>
class SimpleSmartPtr {
T* ptr;
public:
SimpleSmartPtr(T* p) : ptr(p) {}
~SimpleSmartPtr() { delete ptr; } // 스코프를 벗어나면 자동 호출!
T& operator*() { return *ptr; }
T* operator->() { return ptr; }
};
하지만 smart_pointer<int> sp 처럼 모든 걸 아우르는 템플릿은 없고 보통 3가지로 나뉘어서 사용한다.
가장 기본이 되는 스마트 포인터다. “이 자원은 딱 한곳에서만 소유할 수 있다”는 규칙을 강제한다.
unique_ptr<int> p1 = make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // 컴파일 에러! 복사 금지
unique_ptr<int> p2 = move(p1); // OK: 소유권 이전, p1은 이제 nullptr
A* a = new A();
shared_ptr<A> pa1(a);
shared_ptr<A> pa2(a);
A* a = new A();
auto pa1 = std::make_shared<A>();
auto pa2 = std::make_shared<A>();
auto pa3 = pa1;
<특징>
shared_ptr<Node> a = make_shared<Node>();
weak_ptr<Node> weakRef = a; // 참조 카운트 증가 안 함
if (shared_ptr<Node> locked = weakRef.lock()) {
// 객체가 아직 살아있으면 locked로 안전하게 접근
} else {
// 이미 소멸된 뒤라면 locked는 nullptr
}
class Child;
class Parent {
public:
vector<shared_ptr<Child>> children; // 부모가 자식들을 소유
};
class Child {
public:
weak_ptr<Parent> parent; // 자식은 부모를 "약하게"만 참조
void notifyParent() {
if (auto p = parent.lock()) { // 부모가 아직 살아있는지 확인 후 접근
p->onChildEvent();
}
}
};
class ResourceCache {
unordered_map<string, weak_ptr<Resource>> cache;
public:
shared_ptr<Resource> get(const string& key)
{
if (auto it = cache.find(key); it != cache.end())
{
if (auto sp = it->second.lock())
{
return sp; // 아직 살아있으면 캐시된 걸 재사용
}
}
auto sp = make_shared<Resource>(key);
cache[key] = sp; // 캐시는 소유권 없이 그냥 지켜만 봄
return sp;
}
};
c++을 배우게 되면 pointer에 대해서 굉장히 자주 사용하고, 이거를 잘 사용해야 c++다운 코드를 작성할 수 있다. 가장 기본이 되는 부분에 대해서 학습을 했는데, 분명 나중 계속해서 까먹을 수 있기 때문에 자주와서 나 스스로도 복습을 해야할 것 같다.
Race Condition 서로 다른 Thread에서 같은 메모리를 공유할 때 같은 자원을 사용하면서 값이 이상하게 되는…
서론 c++을 이용해서 개발을 하다보면 복사 대신에 이동연산을 통해서 여러 이점을 얻는다는 정보를 확인할 수…
서론 이번시간에는 Casting이라는 개념에 대해서 학습해 보도록 하겠다. 하지만 그 전에 Composition과 Aggregation의 차이에 대해서…
서론 c++ basic을 공부하기 위해서는 다른 언어들과의 기본적인 차이를 이해해야 한다. 그 차이는 바로 메모리…
View Comments
포인터 이해하기 어려운 개념인데 쉽게 풀어서 잘 정리해두신 것 같아서 좋네요