#include <iostream>
using namespace std;
void nestedFunction() {
int b = 20; // 지역 변수 'b', stack 메모리에서 관리됨
cout << "nestedFunction: b = " << b << endl;
} // 'b'는 nestedFunction() 종료 후 소멸됨
void functionExample() {
int a = 10; // 지역 변수 'a', stack 메모리에서 관리됨
cout << "functionExample: a = " << a << endl;
for (int i = 0; i < 3; ++i) { // 반복문 내에서 매번 생성되는 지역 변수 'i'
int temp = i * 10; // 반복문 내에서만 유효한 'temp'
cout << "Iteration " << i << ": temp = " << temp << endl;
} // 반복문이 끝날 때마다 'temp'는 소멸됨
nestedFunction(); // 중첩 함수 호출
} // 'a'는 functionExample() 종료 후 소멸됨
int main() {
functionExample(); // functionExample() 호출
return 0;
}
BUT, 단점:
스택은 메모리 영역 자체가 크지 않다.
생존 영역을 벗어나면 자동으로 해지 된다.
2. 힙 메모리
힙 메모리는 위의 단점을 해결:
선언 시 new 연산자를, 해제시 delete 연산자를 사용한다.
스택처럼 자동으로 해지하지 않는다. (리스크가 존재함)
생존주기는 사용자가 선언한 순간부터 해지하기 전 까지이다.
#include <iostream>
using namespace std;
void handleDynamicMemory() {
int size;
cout << "배열 크기를 입력하세요: ";
cin >> size; // 사용자로부터 배열 크기 입력받기
if (size > 0) {
int* arr = new int[size]; // 배열 크기만큼 동적 할당
for (int i = 0; i < size; ++i) {
arr[i] = i * 10; // 배열 초기화
cout << "arr[" << i << "] = " << arr[i] << endl;
}
delete[] arr; // 동적 할당한 배열 메모리 해제
} else {
cout << "잘못된 크기입니다!" << endl;
}
int* ptr = new int(20); // 정수 하나 동적 할당
cout << "값: " << *ptr << endl;
delete ptr; // 동적 할당한 메모리 해제
}
int main() {
handleDynamicMemory();
return 0;
}
3. Dangling Pointer
비유 설명:
어떤 식당을 가려고 하는데, 이미 그 식당이 철거된 상황이라 가게에 도착했을 때 허탕을 치는 상황.
C++에서의 유사한 문제:
메모리 해지 후, 해지된 메모리 영역에 대한 정보가 특별히 알림 없이 그대로 남아 있을 수 있음.
위험성:
해지된 메모리 영역을 참조하면, 예상치 못한 결과를 초래할 수 있음.
이런 포인터를 **Dangling Pointer**라고 부름.
#include <iostream>
using namespace std;
void func() {
int* ptr = new int(30); // 정수 30에 대한 메모리 할당
int* ptr2 = ptr; // 같은 메모리를 가리키는 두 번째 포인터
cout << "삭제 전 값: " << *ptr << endl;
delete ptr; // 첫 번째 해제
// 아래 라인을 주석 해제하면 이중 해지 오류가 발생합니다
// delete ptr; // 두 번째 해지 (이중 해지 오류)
cout << "첫 번째 삭제 후 값: " << *ptr2 << endl; // Dangling Pointer 사용
}
int main() {
func();
return 0;
}
#include <iostream>
using namespace std;
class MyClass {
private:
int* ptr;
public:
// 생성자
MyClass() {
ptr = new int(10); // 동적 메모리 할당
cout << "메모리 할당 완료!" << endl;
}
// 소멸자
~MyClass() {
}
void print() const {
cout << "값: " << *ptr << endl;
}
};
int main() {
MyClass obj;
obj.print();
// main 함수 종료
return 0;
}
답:
~MyClass() {
delete ptr; // 동적 메모리 해제 (ptr이 가리키는 메모리)
cout << "메모리 해제 완료!" << endl;
}
main 함수 종료 시, obj의 소멸자가 호출되어 메모리 해제가 이루어진다. 그러나 소멸자에서 delete ptr;가 없으면, ptr이 가리키는 동적 메모리가 해제되지 않아 메모리 누수가 발생한다. 동적 메모리를 할당할 때는 반드시 소멸자에서 메모리를 해제해야 한다.
4. 스마트 포인터(unique_ptr & shared_ptr)
Heap은 여러 가지 장점이 있지만, 메모리를 직접 관리해야 하는 부담이 있음.
Dangling Pointer 방지: 자동으로 메모리 관리가 되어 Dangling Pointer가 발생하지 않도록 하는 방식이 필요함.
C++의 스마트 포인터: 스마트 포인터는 레퍼런스 카운터를 이용해 메모리를 자동으로 관리함.
스마트 포인터의 원리:
delete를 직접 호출하지 않고,
자신을 참조하는 포인터의 개수가 0이 되면 자동으로 메모리를 해제함.
4.1 unique_ptr
레퍼런스 카운터가 최대 1인 스마트 포인터.
소유권을 한 번에 하나의 객체만 가질 수 있음, 안전하게 관리
복사 불가능: unique_ptr은 복사나 대입이 불가능하며, 이를 시도하면 컴파일 에러가 발생함.
소유권 이전 가능: move를 사용하여 소유권을 다른 unique_ptr로 이전할 수 있음.
자동 메모리 해제: unique_ptr은 범위를 벗어나면 자동으로 메모리를 해제하여 관리 용이.
#include <iostream>
#include <memory> // unique_ptr 사용
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val) {
cout << "MyClass 생성: " << value << endl;
}
~MyClass() {
cout << "MyClass 소멸: " << value << endl;
}
void display() const {
cout << "값: " << value << endl;
}
private:
int value;
};
int main() {
// 1. 기본적인 unique_ptr 사용
unique_ptr<int> ptr1 = make_unique<int>(10);
cout << "ptr1의 값: " << *ptr1 << endl;
// 2. unique_ptr 소유권 이동
unique_ptr<int> ptr2 = move(ptr1);
if (!ptr1) {
cout << "ptr1은 이제 비어 있습니다." << endl;
}
cout << "ptr2의 값: " << *ptr2 << endl;
// 3. 일반 클래스에서 unique_ptr 사용
unique_ptr<MyClass> myObject = make_unique<MyClass>(42);
myObject->display();
unique_ptr<MyClass> newOwner = move(myObject);
if (!myObject) {
cout << "myObject는 이제 비어 있습니다." << endl;
}
newOwner->display();
return 0;
}
기본 사용법:
unique_ptr<int> ptr1 = make_unique<int>(10); unique_ptr을 사용하여 동적 메모리 할당.
*ptr1로 값을 출력하고, 소유권 이동은 move를 통해 처리.
소유권 이동 (move):
unique_ptr은 소유권을 이동할 수 있으며, move(ptr1)로 ptr1의 소유권을 ptr2로 이동.
이동 후 ptr1은 비어있게 됨.
클래스에서 사용:
unique_ptr<MyClass>를 사용하여 객체를 관리하고, 범위를 벗어나면 자동 메모리 해제가 이루어짐.
4.2 shared_ptr
여러 포인터가 동일한 메모리를 공유하는 상황에서 유용
레퍼런스 카운트가 여러 개 존재할 수 있는 스마트 포인터.
복사 및 대입이 가능하고, use_count()로 참조 카운트를 확인할 수 있음.
reset()을 통해 포인터를 초기화하고 참조 카운트를 조정할 수 있음.
범위를 벗어나면 자동 해제됨.
#include <iostream>
#include <memory> // shared_ptr 사용
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val) {
cout << "MyClass 생성: " << value << endl;
}
~MyClass() {
cout << "MyClass 소멸: " << value << endl;
}
void display() const {
cout << "값: " << value << endl;
}
private:
int value;
};
int main() {
// 1. 기본적인 shared_ptr 사용
shared_ptr<int> ptr1 = make_shared<int>(10);
cout << "ptr1의 참조 카운트: " << ptr1.use_count() << endl; // 출력: 1
// 2. 복사 및 참조 카운트 증가
shared_ptr<int> ptr2 = ptr1;
cout << "ptr2 생성 후 참조 카운트: " << ptr1.use_count() << endl; // 출력: 2
// 3. reset을 통한 참조 카운트 감소
ptr2.reset();
cout << "ptr2 해제 후 참조 카운트: " << ptr1.use_count() << endl; // 출력: 1
// 4. 클래스에서 shared_ptr 사용
shared_ptr<MyClass> obj1 = make_shared<MyClass>(42);
shared_ptr<MyClass> obj2 = obj1;
cout << "obj1과 obj2의 참조 카운트: " << obj1.use_count() << endl; // 출력: 2
obj2->display(); // 출력: 값: 42
// 5. obj2 해제 후 obj1이 객체를 유지
obj2.reset();
cout << "obj2 해제 후 obj1의 참조 카운트: " << obj1.use_count() << endl; // 출력: 1
return 0;
}
기본 사용법
shared_ptr<int> ptr1 = make_shared<int>(10); shared_ptr을 사용해 동적 메모리 할당.
use_count()로 참조 카운트를 확인할 수 있으며, 참조 카운트의 변화에 따라 리소스 공유 상태를 알 수 있음.
복사 및 참조 공유
shared_ptr은 복사 가능하며, 여러 포인터가 하나의 리소스를 공유함.
예: shared_ptr<int> ptr2 = ptr1;
복사 후 참조 카운트가 증가하고, 동일 객체를 여러 포인터가 공유하게 됨.
reset()으로 참조 카운트 관리
reset()을 사용하여 shared_ptr을 초기화하고, 참조 카운트를 조정할 수 있음.
예: ptr2.reset(); — ptr2를 해제하고 ptr1의 참조 카운트를 유지.
클래스에서의 사용
shared_ptr은 클래스를 관리할 때도 동일하게 사용됨. 여러 포인터가 동일한 객체를 공유하고, 참조 카운트에 따라 메모리가 자동으로 해제됨.
객체를 관리하는 클래스에서 shared_ptr을 사용할 때, 포인터가 소멸하면 객체도 자동으로 해제됨.
5. 얕은 복사와 깊은 복사
얕은 복사(Shallow Copy)
얕은 복사는 대입 연산자를 사용하여 두 포인터가 동일한 메모리 위치를 공유하는 방식.
예: int* B = A; — B는 A가 가리키는 메모리 영역을 공유.
이 방식에서 A가 메모리를 해제하면 B는 더 이상 유효한 메모리를 가리키지 않게 되어 Dangling Pointer가 발생할 수 있음.
Dangling Pointer는 해제된 메모리를 참조하게 되므로 Undefined Behavior를 일으킬 수 있음.
#include <iostream>
using namespace std;
int main() {
int* A = new int(30); // A가 30을 가리키는 메모리 할당
int* B = A; // B는 A가 가리키는 메모리 공유
cout << "A의 값: " << *A << endl; // 출력: 30
cout << "B의 값: " << *B << endl; // 출력: 30
delete A; // A가 메모리 해제
cout << "B의 값 (dangling): " << *B << endl; // 위험: 정의되지 않은 동작
return 0;
}
깊은 복사(Deep Copy)
깊은 복사는 독립적인 메모리 영역을 새로 할당하고, 그 내용만 복사하는 방식.
예: int* B = new int(*A); — B는 A의 값을 복사하여 독립된 메모리 영역을 관리.
이 방식에서는 A가 메모리를 해제하더라도 B는 여전히 독립적으로 메모리를 관리하므로 Dangling Pointer가 발생하지 않음.
각 포인터가 독립적인 메모리 공간을 할당받기 때문에, 안전하게 메모리 관리를 할 수 있음.
#include <iostream>
using namespace std;
int main() {
int* A = new int(30); // A가 30을 가리키는 메모리 할당
int* B = new int(*A); // B는 A의 값을 복사하여 독립된 메모리 할당
cout << "A의 값: " << *A << endl; // 출력: 30
cout << "B의 값: " << *B << endl; // 출력: 30
delete A; // A의 메모리 해제
cout << "B의 값 (깊은 복사 후): " << *B << endl; // 출력: 30
delete B; // B의 메모리 해제
return 0;
}