메모리 관리는 cpp의 가장 큰 장점이라 할 수 있습니다.
다른 객체지향 언어들에 비해 수동적인 메모리관리로 효율적인 메모리 관리가 가능합니다.
하지만 큰 힘에는 큰 책임이 따르는 법입니다.
메모리를 효율적으로 관리하기 위해서는 메모리누수에 대해 경계해야합니다.
먼저 일반적인 그동안 했던 변수들의 어떤 메모리에 저장되는지 살펴보겠습니다.
일반 변수들은 대부분 스택 메모리 공간을 차지 합니다. 스택 메모리의 특징은 변수의 생존 주기가 끝나면 변수 선언시 할당되었던 메모리고 저절로 회수가 된다는 것 입니다. 따라서 사용자가 따로 메모리를 관리 해줄 필요가 없습니다. 변수의 생존주기는 선언된 라인을 기준으로 가장 가까운 마침 괄호”}” 입니다.
#include <iostream>
using namespace std;
void func1() {
int a = 10; // 지역 변수 'a', stack 메모리에서 관리됨
cout << "func1: a = " << a << endl;
} // func1()이 종료되면 'a'는 소멸됨
int main() {
func1(); // func1() 호출
// 'a'는 func1() 호출 중에만 존재하고, 함수 종료 후 소멸됨
return 0;
}
이런식으로 일반적인 변수는 해당 괄호 안에서 소멸됩니다.
하지만 스택메모리에도 단점이 있습니다.
1. 스택은 메모리영역 자체가 크지가 않습니다.
2. 스택메모리는 생존 영역을 벗어나면 자동으로 해지됩니다.
이를 해결할 수 있는 방법이 힙 메모리를 활용하는 것 입니다.
선언 시 new 연산자를 해제시 delete 연산자를 사용합니다.
스택처럼 자동으로 해지하지 않습니다 (리스크가 존재함)
생존주기는 사용자가 선언한 순간부터 해지하기 전 까지 입니다.
예시로 확인해보도록 하겠습니다.
#include <iostream>
using namespace std;
void createDynamicArray() {
int size;
cout << "Enter the size of the array: ";
cin >> size; // 배열 크기를 사용자로부터 입력받음
if (size > 0) {
int* arr = new int[size]; // 입력받은 크기만큼 동적 배열 생성
for (int i = 0; i < size; ++i) {
arr[i] = i * 2; // 배열 초기화
cout << "arr[" << i << "] = " << arr[i] << endl;
}
delete[] arr; // 동적으로 할당한 배열 메모리 해제
} else {
cout << "Invalid size!" << endl;
}
}
int main() {
createDynamicArray();
return 0;
}
다음으로 Dangling Pointer에 대해 알아보겠습니다.
어떤 식당을 가려고 합니다.
하지만 안타깝게도 그 식당은 이미 건물이 철거 된 상황이고.. 여러분은 그 사실을 모릅니다. 결국 식당으로 가고… 허탕을 칩니다.
C++에서도 비슷한 상황이 발생할 수 있습니다. 메모리 해지를 하면 특별히 알려주는 과정이 없습니다.
따라서 해지된 메모리 영역의 위치정보를 가지고 있으면 위험 합니다.
#include <iostream>
using namespace std;
void func5() {
int* ptr = new int(40); // 힙 메모리에 정수 40 할당
int* ptr2 = ptr;
cout << "ptr adress = " << ptr << endl;
cout << "ptr2 adress = " << ptr2 << endl;
cout << *ptr << endl;
delete ptr;
cout << *ptr2 << endl;
}
int main() {
func5();
return 0;
}
이런 포인터를 Dangling Pointer라고 합니다.
앞에서 본것처럼 Heap은 여러가지 장점이 있지만, 메모리를 직접 관리해야하는 부담이 있습니다.
Dagling Pointer가 발생하지 않게 알아서 관리해주면 참 좋겠는데 말이죠.. C++에서는 이를 위해 스마트 포인터를 제공 합니다.
스마트 원리의 핵심 원리는 레퍼런스 카운터 입니다.
delete를 직접 하는 대신,
아래처럼 자신을 참조하고 있는 포인터의 개수가 0이 되면 자동으로 해지 하는 방식 입니다.
unique_ptr은 레퍼런스 카운터가 최대 1인 스마트 포인터 입니다.
따라서 소유권을 다른 사람에게 주는 건 가능하나, 동시에 두개 이상 소유할 수 없습니다.
#include <iostream>
#include <memory>
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() {
// unique_ptr로 MyClass 객체 관리
unique_ptr<MyClass> myObject = make_unique<MyClass>(42);
// MyClass 멤버 함수 호출
myObject->display();
// 소유권 이동
unique_ptr<MyClass> newOwner = move(myObject);
if (!myObject) {
cout << "myObject는 이제 비어 있습니다." << endl;
}
newOwner->display();
// 범위를 벗어나면 newOwner가 관리하는 메모리 자동 해제
return 0;
}
그럼 이제 여러개의 포인터를 가지고 있을 수는 주소도 있을겁니다.
shared_ptr은 레퍼런스 카운터가 N개가 될수 있는 스마트 포인터 입니다. 따라서 레퍼런스 카운터 갯수를 볼수 있는 use_count() 와 현재 포인터를 초기화 할 수 있는 reset()을 제공 합니다.
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val) {
cout << "MyClass 생성: " << value << endl; // 출력: MyClass 생성: 42
}
~MyClass() {
cout << "MyClass 소멸: " << value << endl; // 출력: MyClass 소멸: 42
}
void display() const {
cout << "값: " << value << endl; // 출력: 값: 42
}
private:
int value;
};
int main() {
// shared_ptr로 MyClass 객체 관리
shared_ptr<MyClass> obj1 = make_shared<MyClass>(42);
// 참조 공유
shared_ptr<MyClass> obj2 = obj1;
cout << "obj1과 obj2의 참조 카운트: " << obj1.use_count() << endl; // 출력: 2
obj2->display(); // 출력: 값: 42
// obj2를 해제해도 obj1이 객체를 유지
obj2.reset();
cout << "obj2 해제 후 obj1의 참조 카운트: " << obj1.use_count() << endl; // 출력: 1
return 0;
}
이때 얇은 복사와 깊은 복사란 무엇일까요?
얕은 복사(Shallow Copy)란, 대입 연산자를 활용해서 두 개의 포인터가 같은 위치를 공유하는 것을 말합니다. 아래 그림과 같이 얕은 복사를 하면 dangling pointer가 발생할 수 있습니다.
#include <iostream>
using namespace std;
int main() {
// 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정
int* A = new int(30);
// 포인터 B가 A가 가리키는 메모리를 공유
int* B = A;
cout << "A의 값: " << *A << endl; // 출력: 30
cout << "B의 값: " << *B << endl; // 출력: 30
// A가 동적 메모리를 해제
delete A;
// 이제 B는 Dangling Pointer(해제된 메모리를 가리키는 포인터)
// 이 시점에서 B를 통해 접근하면 Undefined Behavior 발생
cout << "B의 값 (dangling): " << *B << endl; // 위험: 정의되지 않은 동작
return 0;
}
그렇다면 어떻게 해야할까요? 바로 독립된 메모리영역을 할당해서 위치가 아닌 가르키는 내용을 복사하면 됩니다.
이를 깊은 복사라합니다. 독립된 영역이므로 dangling pointer가 발생하지 않습니다.
#include <iostream>
using namespace std;
int main() {
// 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정
int* A = new int(30);
// 포인터 B가 A가 가리키는 값을 복사 (깊은 복사)
int* B = new int(*A);
cout << "A의 값: " << *A << endl; // 출력: 30
cout << "B의 값: " << *B << endl; // 출력: 30
// A가 동적 메모리를 해제
delete A;
// B는 여전히 독립적으로 자신의 메모리를 관리
cout << "B의 값 (깊은 복사 후): " << *B << endl; // 출력: 30
// B의 메모리도 해제
delete B;
return 0;
}
'언리얼(Unreal) > 엔진' 카테고리의 다른 글
25.01.06 언리얼 cpp 디자인패턴 (0) | 2025.01.06 |
---|---|
25.01.02 언리얼 cpp STL에 대하여 (0) | 2025.01.02 |
24.12.26 두수의 차 구하기 및 배열의 합과 평균 그리고 정렬함수 만들기 (0) | 2024.12.26 |
24.12.24 언리얼 cpp 객체지향에 대하여 (1) | 2024.12.24 |
24.12.23 언리얼 cpp 기초 복습 (0) | 2024.12.23 |