
사진은 강아지 품종 "포인터입니다."
가장 기본적인 포인터로, 직접 메모리 주소를 저장하고 조작할 수 있는 포인터
#include <iostream>
void memoryLeak() {
int* ptr = new int(10); // 동적 할당
std::cout << "메모리 누수 발생 가능: " << *ptr << std::endl;
// delete ptr; // 해제를 하지 않으면 메모리 누수 발생
}
int main() {
memoryLeak();
return 0;
}
메모리 누수가 발생하면 메모리 부족이 발생할 수 있다.
c++에서 적용된 메모리 관리를 자동화하는 포인터. RAII 원칙을 따르며,
new와 delete를 직접 사용하지 않고 객체의 생명 주기를 자동으로 관리한다.
#include <iostream>
#include <memory>
// unique_ptr 사용 예제
void uniquePointerExample() {
std::unique_ptr<int> ptr = std::make_unique<int>(100);
std::cout << "unique_ptr 값: " << *ptr << std::endl;
} // ptr이 범위를 벗어나면 자동으로 메모리 해제
// shared_ptr 사용 예제 (참조 카운트 확인)
void sharedPointerExample() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(200);
std::shared_ptr<int> ptr2 = ptr1; // 같은 메모리를 공유
std::cout << "shared_ptr 값: " << *ptr1 << ", 참조 카운트: " << ptr1.use_count() << std::endl;
} // 마지막 shared_ptr이 범위를 벗어나면 메모리 해제
// weak_ptr 사용 예제 (순환 참조 방지)
void weakPointerExample() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(300);
std::weak_ptr<int> weakPtr = sharedPtr; // shared_ptr을 참조하지만 소유권 없음
if (auto locked = weakPtr.lock()) { // weak_ptr을 shared_ptr로 변환 (유효한 경우만)
std::cout << "weak_ptr 값: " << *locked << std::endl;
} else {
std::cout << "weak_ptr이 가리키는 객체가 이미 삭제됨" << std::endl;
}
}
int main() {
std::cout << "=== unique_ptr 예제 ===" << std::endl;
uniquePointerExample();
std::cout << "\n=== shared_ptr 예제 ===" << std::endl;
sharedPointerExample();
std::cout << "\n=== weak_ptr 예제 ===" << std::endl;
weakPointerExample();
return 0;
}

순환 참조(Circular Reference)는 두 개 이상의 객체가 서로를 참조하면서, 참조 카운트가 0이 되지 않아 메모리가 해제되지 않는 문제이다.
C++에서는 std::shared_ptr만 사용하면 발생할 수 있으며, std::weak_ptr로 해결할 수 있다.
RAII (Resource Acquisition Is Initialization) 원칙을 지킴
객체가 생성될 때 자원을 획등하고, 객체가 소멸될 때 자동으로 자원을 해제하는 기법
변수나 객체의 주소를 직접 조작하지 않고, 해당 변수를 가리키는 별칭(Alias)를 제공하는 방식
C++의 참조(&)나 Java/Python의 참조(Reference)는 실제 메모리 주소를 다루지 않으면서도 포인터처럼 객체를 공유할 수 있다.
#include <iostream>
void referenceExperiment() {
int a = 10;
int& ref = a; // 참조 변수 (alias)
std::cout << "원래 변수 a: " << a << std::endl;
std::cout << "참조 변수 ref: " << ref << std::endl;
ref = 20; // 참조를 통해 원본 값 변경
std::cout << "참조를 변경한 후 a: " << a << std::endl;
}
void pointerExperiment() {
int b = 30;
int* ptr = &b; // 포인터 사용
std::cout << "원래 변수 b: " << b << std::endl;
std::cout << "포인터 ptr이 가리키는 값: " << *ptr << std::endl;
*ptr = 40; // 포인터를 통해 원본 값 변경
std::cout << "포인터를 사용하여 변경한 후 b: " << b << std::endl;
}
int main() {
std::cout << "=== 참조 방식 실험 ===" << std::endl;
referenceExperiment();
std::cout << "\n=== 포인터 방식 실험 ===" << std::endl;
pointerExperiment();
return 0;
}

이렇듯 참조와 포인터는 약간의 차이가 있다.
참조(&)와 포인터(*) 모두 변수의 주소를 다루지만, 사용법과 동작 방식에서 차이가 있다.
#include <iostream>
int main() {
int a = 10;
int& ref = a; // 참조 변수 (alias)
ref = 20; // a의 값도 변경됨
std::cout << "a: " << a << ", ref: " << ref << std::endl;
return 0;
}
참조는 기존 변수의 별칭일 뿐이므로 ref를 변경하면 a도 변경됨
다른 변수를 참조할 수 없으며(재할당 불가능) 항상 유효한 변수를 가리켜야 함(nullptr 불가능).
#include <iostream>
int main() {
int a = 10;
int b = 30;
int* ptr = &a; // a의 주소를 저장
*ptr = 20; // a의 값이 20으로 변경됨
ptr = &b; // ptr이 b를 가리키도록 변경 가능
*ptr = 40; // b의 값이 40으로 변경됨
std::cout << "a: " << a << ", b: " << b << std::endl;
return 0;
}
포인터는 다른 변수를 가리키도록 변경할 수 있고 *ptr을 통해 역참조해야 실제 값을 변경 가능하다.
더 이상 유효하지 않은 메모리 주소를 가리키는 포인터
다음과 같은 상황에서 발생한다.
void danglingPointer() {
int* ptr = new int(42);
delete ptr; // 메모리 해제
std::cout << "댕글링 포인터 접근: " << *ptr << std::endl; // 정의되지 않은 동작
}
int main() {
danglingPointer();
return 0;
}
정상적인 메모리 위치를 가리키는 경우

이미 해제된 메모리 위치를 가리키는 경우

해제된 메모리에서 쓰레기 값을 가져오게 된다.
#include <stdio.h>
#include <stdlib.h>
// 비정상적인 경우: 함수 종료 후 지역 변수의 주소를 반환 -> 댕글링 포인터 발생
int* danglingFunction() {
int x = 10; // 지역 변수
return &x; // 함수 종료 후 x가 사라짐 -> 댕글링 포인터
}
// 정상적인 경우: 동적 메모리 할당을 사용하여 올바르게 반환
int* validFunction() {
int* x = (int*)malloc(sizeof(int)); // 동적 메모리 할당
if (x == NULL) {
printf("메모리 할당 실패\\n");
return NULL;
}
*x = 10;
return x; // 올바른 메모리 반환 (사용 후 free 필요)
}
int main() {
int* ptr1 = danglingFunction();
// 스택을 덮어쓰기 위해 다른 변수를 추가
int y = 999;
printf("새로운 변수 y 할당: %d \n", y);
printf("비정상적인 경우 (댕글링 포인터): %d \n", *ptr1); // 댕글링 포인터 접근 (예측 불가한 결과)
int* ptr2 = validFunction();
if (ptr2 != NULL) {
printf("정상적인 경우: %d \n", *ptr2); // 정상적인 값 출력
free(ptr2); // 동적 메모리 해제
}
return 0;
}

함수 종료 이후에 함수의 지역 변수는 사라지기 때문에 지역 변수의 포인터를 보면 쓰레기 값이 들어있다.
int* ptr1 = new int(5);
int* ptr2 = ptr1;
delete ptr1; // ptr1이 가리키는 메모리 해제
*ptr2 = 10; // ptr2는 댕글링 포인터가 됨
이렇듯 댕글링은 유효하지 않은 메모리를 참조하는 포인터나 참조(Reference)에서 발생하는 문제이다
#include <iostream>
int main() {
int* ptr = new int(10);
delete ptr; // 메모리 해제
ptr = nullptr; // 안전한 처리
if (ptr == nullptr) {
// 안전한 접근이 필요한 코드
}
return 0;
}
이런식으로 해제한 포인터를 nullptr 처리를 하게 되면 다음과 같은 장점이 있다.
#include <iostream>
int main() {
int* ptr = nullptr; // nullptr로 초기화된 포인터
std::cout << *ptr << std::endl; // 오류 발생 (Segmentation Fault)
return 0;
}

nullptr에 접근하게 되면 이런식으로 세그먼트 오류가 발생하여 문제를 알아차리기 쉽다.
Null은 정수로 0으로 표현되기 때문에 컴파일러가 Null문자를 정상인 정수로 인식할 가능성이 있다. 따라서 nullptr을 사용하는것이 안전하다.