자원 관리

주상돈·2024년 12월 30일

TIL

목록 보기
6/53

이번에는 자원을 관리하는 것에 대해 공부해보자.

스택 메모리

일반 변수들은 대부분 스택 메모리 공간을 차지 한다.
스택 메모리의 특징은 변수의 생존 주기가 끝나면 변수 선언시 할당되었떤 메모리도 저절로 회수가 된다. 따라서 사용자가 따로 메모리를 관리 해줄 필요가 없다.
변수의 생존주기는 선언된 라인을 기준으로 가장 가까운 마침 괄호"}"이다.

함수 내에서 선언된 변수 생존 주기

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

반복 내에서 선언된 변수 생존 주기

#include <iostream>
using namespace std;

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

int main() {
    func3();  // func3 호출
    return 0;
}

중첩함수 내에서 선언된 변수 생존 주기

#include <iostream>
using namespace std;

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

void func1() {
    int a = 10;  // 지역 변수 'a', stack 메모리에서 관리됨
    cout << "func1: a = " << a << endl;
    func2();  // func2() 호출
}  // func1() 종료 시 'a'는 소멸되고, func2() 종료 후 'b'도 소멸됨

int main() {
    func1();  // func1() 호출
    return 0;
}

힙 메모리

스택 메모리가 메모리를 자동 회수해주는 장점이 있는 반면, 단점도 존재 한다.
가장 큰 단점은 아래와 같다.
1. 스택은 메모리 영역 자체가 크지 않다.
2. 스택 메모리는 생존 영역을 벗어나면 자동으로 해지 된다.
이를 해결할 수 있는 방법이 힙 메모리를 활용하는 것이다.
1.선언시 new연산자를 해제시 delete연산자를 사용한다.
2.스택처럼 자동으로 해지하지 않는다.
3.생존주기는 사용자가 선언한 순간부터 해지하기 전 까지 이다.

변수 하나의 영역에 대한 동적할당

#include <iostream>
using namespace std;

void func1() {
    int* ptr = new int(10);  // 힙 메모리에 정수 10 할당
    cout << "Value: " << *ptr << endl;
    delete ptr;             // 메모리 해제
}

int main() {
    func1();
    return 0;
}

배열의 동적 할당과 해제

#include <iostream>
using namespace std;

void func2() {
    int* arr = new int[5];  // 힙 메모리에 정수 배열 5개 할당
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 10;
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }
    delete[] arr;  // 배열 메모리 해제
}

int main() {
    func2();
    return 0;
}

메모리 해지를 하지 않는 예시

#include <iostream>
using namespace std;

void func3() {
    int* ptr = new int(20);  // 힙 메모리에 정수 20 할당
    cout << "Value: " << *ptr << endl;
    // 메모리 해제를 하지 않음
}

int main() {
    func3();  // 메모리 누수 발생
    return 0;
}

사용자 입력 기반으로 배열 동적 할당

#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


메모리 해지를 하면 특별히 알려주지 않는다.
따라서 해지된 메모리 영역의 위치정보를 가지고 있으면 위험하다.
이런 포인터를 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;

    cout << *ptr2 << endl;
}

int main() {
    func5();
    return 0;
}

아래와 같이 ptr이 해지가 되었기때문에 값이 이상하게 -5726...과 같은 값으로 나오는 것을 확인할 수 있다.

#include <iostream>
using namespace std;

void func4() {
    int* ptr = new int(30);  // 힙 메모리에 정수 30 할당
    cout << "Value: " << *ptr << endl;
    delete ptr;             // 첫 번째 해제
    // delete ptr;          // 두 번째 해제 (코드 활성화 시 문제 발생)
}

int main() {
    func4();
    return 0;
}

스마트 포인터

앞에서 본것처럼 힙은 여러가지 장점이 있지만, 메모리를 직접 관리해야하는 부담이 있다.
Dagling Pointer가 발생하지 않게 알아서 관리해주면 참 좋겠는데..
C++에서는 이를 위해 스마트 포인터를 제공한다.
스마트 원리의 핵심 원리는 레퍼런스 카운터 이다.
delete를 직접 하는 대신, 아래처럼 자신을 참고하고 있는 포인터의 개수가 0이 되면 자동으로 해지하는 방식이다.

unique_ptr

unique_ptr은 레퍼런스 카운터가 최대 1인 스마트 포인터이다.
따라서 소유권을 다른 사람에게 주는 건 가능하나, 동시에 두개 이상 소유할 수 없다.
최대 레퍼런스 카운터가 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;
}

move로 unique_ptr 소유권 이전

대입은 레퍼런스 카운터가 2가 되므로 불가능 핮지만 소유권 이전은 가능하다. move를 사용해서 소유권을 이전할 수 있다.

#include <iostream>
#include <memory>
using namespace std;

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

    // 소유권 이동 (move 사용)
    unique_ptr<int> ptr2 = move(ptr1);

    if (!ptr1) {
        cout << "ptr1은 이제 비어 있습니다." << endl;
    }
    cout << "ptr2의 값: " << *ptr2 << endl;

    return 0;
}

일반 클래스에서 unique_ptr 사용하는 방법

일반적인 클래스에서 unique_ptr을 사용하는 방법이다.

#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사용법

최대 레퍼런스 카운터가 N이기 때문에 복사 혹은 대입이 된다.

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

일반 클래스에서 shared_ptr 사용법 얕은 복사와 깊은 복사

일반적인 클래스에서 shared_ptr을 사용하는 방법이다.

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


깊은 복사(Deep Copy)란 독립된 메모리 영역을 할당해서 위치가 아닌 가리키는 내용이 복사 되는 것을 말 한다. 독립된 영역이므로 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;
}

간단하게 두 문제를 풀어보며 오늘 배운 내용을 정리해보자
1. 메모리 누수 발생 코드 분석하여 보완하기

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

정답

#include <iostream>
using namespace std;

class MyClass {
private:
    int* ptr;

public:
    // 생성자
    MyClass() {
        ptr = new int(10); // 동적 메모리 할당
        cout << "메모리 할당 완료!" << endl;
    }

    // 소멸자
    ~MyClass() {
        delete ptr;
    }

    void print() const {
        cout << "값: " << *ptr << endl;
    }
};

int main() {
    MyClass obj;
    obj.print();

    // main 함수 종료
    return 0;
}

클래스가 소멸될때 동적 메모리 할당시킨 ptr변수를 해제해 주어야 메모리 누수가 우려되지 않는다. 그래서 클래스 소멸자부분에

delete ptr

을 넣어서 해결해준다.
2. 스마트 포인터를 활용한 로그분석기 구현
로그 기록기는 프로그램에서 발생한 중요한 사건을 기록한다. 로그 기록기의 몇가지 기능을 구현해보자.
요구사항
• 로그 메시지는 중요도에따라(info,Warning,Error)로 분류되어 기록될 수 있다. 로그 앞에 중요도가 태그가 붙어 표시된다.
- [info] 사용자 메시지
- [Warning] 사용자 메시지
- [Error] 사용자 메시지
• 로그 기록기는 단 하나의 인스턴스만 존재해야 하며, unique_ptr를 통해 이를 보장해야 한다.
• 지금까지 기록된 로그의 총 개수를 출력할 수 있어야 한다.
아래 사진은 완성된 모습이다.

[로그 출력 예시]

[INFO]: System is starting.
[WARNING]: Low disk space.
[ERROR]: Unable to connect to the server.
Total logs recorded: 3
Logger instance destroyed.

정답

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Logger {
private:
    int logCount;
public:
    Logger() : logCount(0){}
    void logInfo(const string& message) {
        logCount++;
        cout << "[INFO]: " << message << endl;
    }
    void logWarning(const string& message) {
        logCount++;
        cout << "[WARNING]: " << message << endl;
    }
    void logError(const string& message) {
        logCount++;
        cout << "[ERROR]: " << message << endl;
    }
    void showTotalLogs() {
        cout << "Total logs recorede: " << logCount << endl;
    }
    ~Logger() {
        cout << "Logger instance destroyed." << endl;
    }
};

int main() {
    // Logger 인스턴스를 unique_ptr로 관리
    unique_ptr<Logger> logger = make_unique<Logger>();

    // 다양한 로그 기록
    logger->logInfo("System is starting.");
    logger->logWarning("Low disk space.");
    logger->logError("Unable to connect to the server.");

    // 총 로그 개수 출력
    logger->showTotalLogs();

    // 아래 코드는 에러를 발생시킴 (unique_ptr은 복사할 수 없음)
    // unique_ptr<Logger> anotherLogger = logger;

    return 0;
}

0개의 댓글