일반 변수들은 대부분 스택 메모리 공간을 차지한다.
변수는 선언된 중괄호안에서 생존하게 된다. 괄호가 시작되면서 변수 선언을 하면 메모리에 할당되고, 괄호가 끝나는 순간 저절로 회수된다.
#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;
}
그러나 static
을 붙이면 변수는 사라지지 않는다. 초기화도 최초 한번 실행되고 더이상 초기화가 되지 않는다.
void func() {
static int a = 10;
a = a + 1;
cout << "func1 : a = " << a << endl;
}
int main() {
func() // func1 : a = 11
func() // func1 : a = 12
func() // func1 : a = 13
func() // func1 : a = 14
func() // func1 : a = 15
return 0;
}
static
키워드는 블록 내에서 선언된 지역 변수에도 사용할 수 있다. 지역 변수는 '자동 주기(auto duration)' 를 가지며, 정의되는 시점에서 생성되고 초기화되며, 정의된 블록이 끝나는 지점에서 소멸한다.static
키워드를 사용한 지역변수는 완전히 다른 의미가 있다.static
키워드를 사용한 지역 변수는 '자동주기(auto duration)' 에서 '정적주기(static duration)'로 바뀐다. 이것을 정적 변수라고도 부르는데, 생성된 스코프(생존 지역)가 종료한 이후에도 해당 값을 유지하는 변수다. 또한 정적 변수는 한 번만 초기화 되며 프로그램 수명 내내 지속된다.
참고 자료 : https://boycoding.tistory.com/169 소년코딩 (C++ 04.05 - static, 정적 변수)
참고로 전역변수와 정적변수는 값을 초기화했으면 Data 영역에 생성되고, 초기화하지 않았으면 BSS 영역에 생성되고 0이 들어간다.(OS 커널에 의해)
메모리에 관한 플러스 설명은 이 포스팅을 참조 : https://code4human.tistory.com/129
스택은 메모리 영역 자체가 크지 않다. 또한 스택에 저장된 메모리는 생존 영역을 벗어나면 자동으로 해지된다.
이를 해결할 수 있는 방법이 힙 메모리를 활용하는 것이다.
new
연산자를 사용한다. 해제 시 delete
연산자를 사용한다.int* ptr = new int(10);
: int
형 하나를 담을 수 있는 공간을 힙 메모리에 만들고 그 값에 10 넣겠다. 그리고 힙메모리가 할당된 위치(주소)를 ptr에 담아라. (*ptr = 10
)delete ptr;
int* arr = new int[5];
: 'int
형 변수 다섯 개를 담을 공간(배열)을 힙 메모리에 만들고 그 힙메모리가 할당된 배열의 시작위치(주소)를 arr에 담아라. arr[i]
배열 처럼 인덱스를 붙여서 할당된 배열에 접근 가능하다. delete[] arr;
#include <iostream>
using namespace std;
int main() {
int* ptr = new int(10); // 동적 할당
cout << "동적 할당 값: " << *ptr << endl;
// 여기서 ptr을 다시 선언하고 값 할당
ptr = new int(20); // 새로운 메모리 할당 (이전 메모리 참조 손실)
cout << "새로운 동적 할당 값: " << *ptr << endl;
// 기존 메모리에 대한 참조가 끊어졌기 때문에 메모리 해제가 되지 않음 (메모리 누수 발생)
delete ptr; // 현재 ptr이 가리키는 메모리 해제
return 0;
}
이 예제에서 보면 새로운 메모리는 해제했으나 기존 메모리는 해제하지 않는다. 따라서 새로운 값을 할당하거나 더이상 쓰지 않으면 반드시 해제를 하는 습관을 들이는게 중요하다.
#include <iostream>
using namespace std;
void CreatDynamicArray() {
int size;
cout << "원하는 배열의 크기를 입력하세요 : ";
cin >> size; // 런타임 중에 배열 크기 결정
if(size > 0) {
int* arr = new int[size]; // 동적 배열 할당
for(int i = 0; i < size; i++){
arr[i] = 2*i; // 동적 배열 원소 초기화
cout << arr[i] << " ";
}
delete[] arr; // 메모리 해제
}
else {
cout << "invalid size" << endl;
}
}
int main() {
CreatDynamicArray();
return 0;
}
Dangling Pointer
예를 들어 두 포인터(ptr1
, ptr2
)가 힙 메모리의 같은 곳을 참조하고 있다고 생각을 하자. 아마도 동적 변수나 동적 배열을 참조할 것이다. 따라서 ptr1
이 이제 해당 메모리를 쓰는 경우가 없어서 해제를 하면delete ptr;
힙 메모리에 있는 값은 날라간다. 그러나 ptr2
은 여전히 해당 메모리의 참조가 필요한데 해제가 되었을 수도 있다. 이럴 때 ptr2
를 Dangling Pointer
라고 한다.
실생활로 잠깐 예시를 들자면 a와 b가 사무실에서 같은 쓰레기통을 쓰고 있는데, a가 이제 쓰레기통이 필요없다고 버리면 b는 쓰레기통을 쓰고 싶어도 못쓰게 된다. 이럴때 b가 Dangling Pointer
이다.
#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;
// delete ptr; 을 한번 더 적게 되면 실행시 문제가 생기게 된다. 이중 해지도 조심!
cout << *ptr2 << endl;
}
int main() {
func5();
return 0;
}
이렇게 힙에 저장되는 데이터들은 장점도 있지만 메모리관리가 필수이므로 위험성도 크다. C++에서 이런것을 보완하기 위해 나온 것이 스마트 포인터이다. 우선 스마트 포인터의 핵심 원리는 레퍼런스 카운터 이다. 직역하면 '참조 반격?' 이라고 생각할 수 있지만 숫자를 세는 count 뜻을 받아 '참조한 숫자' 라는 뜻이다.
위의 그림 처럼 참조한 곳이 두 군데면 레퍼런스 카운터는 2이다. 하나를 해지하더라도 참조한 값은 남아있게 되며, 모두 해제되어 레퍼런스 카운트가 0이되면 알아서 자동으로 힙영역의 메모리 공간을 해제한다.
unique_ptr
:<memory>
라는 헤더파일을 추가해야 한다.#include <iostream>
#include <memory> // unique_ptr 사용
using namespace std;
int main() {
// unique_ptr 생성
unique_ptr<int> ptr1 = make_unique<int>(10); /* unique_ptr/ make_unique
하나의 짝으로 보면된다. 레퍼런스 카운터1 */
// unique_ptr이 관리하는 값 출력
cout << "ptr1의 값: " << *ptr1 << endl;
// unique_ptr은 복사가 불가능
// unique_ptr<int> ptr2 = ptr1; // 컴파일 에러 발생!
// 범위를 벗어나면 메모리 자동 해제
return 0;
}
move
를 이용한 unique_ptr
소유권 이전unique_ptr<int> ptr2 = move(ptr1)
: ptr1
의 소유권이 ptr2
로 이동if (!ptr1) {~}
: ptr1
에 아무것도 담고 있지 않으면 실행문 실행~#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
shared_ptr
은 레퍼런스 카운터가 N개가 될수 있는 스마트 포인터이다.use_count()
와 현재 포인터를 초기화 할 수 있는 reset()
을 제공한다.shared_ptr
, make_shared
공간을 만들 때 하나의 짝이라고 보면된다. unique_ptr
, make_unique
와 같음#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
가 발생할 수 있다.)
int* A = new int(30);
int* B = A;
// 이런 경우가 얕은 복사 : 실제 값이 복사가 되는게 아닌 위치를 공유
깊은 복사(Depp Copy)란 독립된 메모리 영역을 할당해서 위치가 아닌 가리키는 내용(값)이 복사되는 것을 말한다. 독립된 영역
int* A = new int(30);
int* B = new int(*A);
// 이런 경우가 깊은 복사 : new
를 "'힙(heap)'메모리에 공간을 할당하겠다." 라고 생각하면 된다.