c++ 챕터2(1)

김민재·2025년 1월 10일
0
post-thumbnail

어제 정리했던 동적 메모리(동적 할당)에 대해서 이야기를 다시 정리해보자.
동적 메모리는 할당받은 메모리의 크기를 변경할 수 있는 자료 구조이며,
자료형 뒤에 *가 붙는 방식으로 처리되었다.

동적 메모리가 할당(new)되었다면 이는 프로그램이 종료되기 전까지 해제되지 않으며 지속적으로 메모리 누수가 발생하게 되기에 이를 사용자가 직접 delete 해줘야할 필요가 있다.

여기까지가 어제까지 했던 내용이고, 오늘은 메모리에 대해서 보도록 하자.

메모리

RAM, ROM, 주기억장치, 보조기억장치 등의 이름으로 불리는 일종의 저장공간이라고 볼 수 있다.

여기서 우리가 일반적으로 사용하는 지역 변수나 매개 변수들은 전부 다 스택 영역에 저장된다.

스택 영역에 저장된 변수들은 자신들이 속한 스코프({} 또는 함수)가 종료되었을 때 메모리 할당이 해지되는 방식을 가지고 있다.

#include <iostream>
using namespace std;

// 스택 메모리
// 일반 변수들은 대부분 스택 메모리 공간을 차지
// 특징: 변수의 생존 주기가 끝나면 변수 선언시 할당되었던 메모리가 저절로 회수됨
// 해당 변수가 사용된 스코프가 끝날 때 사라짐

void func1() {
    int a = 10;  // 지역 변수 'a', stack 메모리에서 관리됨
    cout << "func1: a = " << a << endl;
}  // func1()이 종료되면 'a'는 소멸됨

void func2() {
    int b = 20;  // 지역 변수 'b', stack 메모리에서 관리됨
    cout << "func2: b = " << b << endl;
}

void func3() {
    for (int i = 0; i < 3; ++i) {  // 반복문 안에서 지역 변수 'i'가 매번 새로 생성됨
        int temp = i * 10;  // 반복문 안에서만 유효한 'temp'
        cout << "Iteration " << i << ": temp = " << temp << endl;
    }  // 반복문 끝날 때마다 'temp'는 소멸됨
}

void func4() {
    // static 변수 사용시 이 함수가 종료되어도 사라지지 않음
    // 이 변수는 이 함수가 호출된 한번만 초기화되어 저장되어 있음
    static int a = 10;
    a = a + 1;
    cout << "func(4)" << a << endl;
}

int main() {
    func1();  // func1() 호출
    // 'a'는 func1() 호출 중에만 존재하고, 함수 종료 후 소멸됨
    func2();
    func3();
    return 0;
}

이 코드 내에서 일반적으로 func1~3에 속한 변수들은 해당 함수가 종료되자마자 메모리 할당을 해지 당한다.(운영체제에 의해서)

그렇지만 func4의 경우에는 static으로 선언되어있어서 함수가 종료되더라도 메모리 할당이 해지되지 않으며 처음 실행하였을 때 딱 한번 초기화를 실행한다고 한다.

static - 정적 변수

static 키워드를 사용하게되면 자동 주기에서 정적 주기로 바뀌는데 이를 정적 변수라고 부르며 생성된 스코프(범위)가 종료된 후에도 해당 값을 유지하며 위에서 설명했다시피 단 한번만 초기화되며 프로그램 수명 내내 지속된다.

이렇게 동적 할당을 사용하게 되면 과연 문제가 발생하지 않는걸까??
만약 하나의 메모리에 2개 이상의 포인터가 접근하여 메모리 영역의 위치 정보를 가지고 있다면 어떻게 될까?

한쪽에서 메모리를 강제로 해지해버린다면 나머지는 메모리가 해지된지 모를 것이다.

스마트 포인터

스마트 포인터는 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제해준다.

1.unique_ptr

#include <iostream>
#include <memory> // unique_ptr 사용
using namespace std;

int main() {
    // unique_ptr 생성
    unique_ptr<int> ptr1 = make_unique<int>(10);

    // unique_ptr이 관리하는 값 출력
    cout << "ptr1의 값: " << *ptr1 << endl;

    // unique_ptr은 복사가 불가능
     unique_ptr<int> ptr2 = ptr1; // 컴파일 에러 발생!

    // 범위를 벗어나면 메모리 자동 해제
    return 0;
}

unique_ptr은 어떤 객체의 유일한 소유권을 나타내며 여기서 ptr1은 int형 메모리(값 10)의 소유권을 가지고 있고 이 소유권은 ptr2로 복사가 되지 않기 때문에 unique_ptr ptr2 = ptr1;는 성립되지 않는다.

일반적으로는 소유권이 넘어가지 않지만 move 내장 함수를 사용하여 이 소유권을 넘기는 것이 가능하다.
unique_ptr ptr2 = move(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;
}

레퍼런스 카운터

위에서 본 것처럼 Heap은 직접 메모리를 관리해야하는 부담이 있다.

위의 unique_ptr은 하나의 포인터만 소유권을 가질 수 있지만, shared_ptr은 다수의 포인터가 소유권을 가질 수 있다.

#include <iostream>
#include <memory> // shared_ptr 사용
using namespace std;

int main() {
  // shared_ptr 생성
  shared_ptr<int> ptr1 = make_shared<int>(10);

  // ptr1의 참조 카운트 출력
  cout << "ptr1의 참조 카운트: " << ptr1.use_count() << endl; // 출력: 1

  // ptr2가 ptr1과 리소스를 공유
  shared_ptr<int> ptr2 = ptr1;
  cout << "ptr2 생성 후 참조 카운트: " << ptr1.use_count() << endl; // 출력: 2

  // ptr2가 범위를 벗어나면 참조 카운트 감소
  ptr2.reset();
  cout << "ptr2 해제 후 참조 카운트: " << ptr1.use_count() << endl; // 출력: 1

  // 범위를 벗어나면 ptr1도 자동 해제
  return 0;
}

얕은 복사와 깊은 복사

얕은 복사: 객체의 값만 복사하고 참조형 데이터(포인터 등)는 원본 객체와 동일한 메모리를 공유한다.
깊은 복사: 객체의 모든 데이터를 복사한 후 새로운 메모리를 할당하여 원본과 다른 복사본을 만든다.

얕은 복사 예시 코드

#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;
}

깊은 복사 코드 예시

#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;
}

이렇게 해서 자원 관리 부분에 대해서 알아보았다.

~~ 어제 til을 쓰지못했으므로 내일 하나 더 쓰도록 하자.~~

profile
ㅇㅇ

0개의 댓글