챕터 2-1 : 자원 관리하기

언리얼 거토
|2024. 12. 30. 16:03

1. 스택 메모리

 

  • 일반 변수는 대부분 스택 메모리를 사용한다.
  • 스택 메모리는 변수의 생존 주기가 끝나면 자동으로 메모리를 회수한다.
    • 사용자가 별도로 메모리를 관리할 필요가 없다.(장점)
  • 변수의 생존 주기는 선언된 위치부터 가장 가까운 마침 괄호( } )까지다.
#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;
}

 

↓ 숙제 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;
}

 

답: 

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

 

↓ 숙제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 <memory> // unique_ptr를 사용하기 위해 포함
#include <string> // std::string 사용을 위해 포함

using namespace std;

class Logger {
private:
    int logCount; // 총 로그 개수를 저장하는 변수

public:
    // 생성자: logCount를 0으로 초기화
    Logger() : logCount(0) {}

    // 일반 정보 로그: 로그 메시지를 출력하고 logCount 증가
    void logInfo(const string& message) {
        logCount++;
        cout << "[INFO]: " << message << endl;
    }

    // 경고 로그: 로그 메시지를 출력하고 logCount 증가
    void logWarning(const string& message) {
        logCount++;
        cout << "[WARNING]: " << message << endl;
    }

    // 에러 로그: 로그 메시지를 출력하고 logCount 증가
    void logError(const string& message) {
        logCount++;
        cout << "[ERROR]: " << message << endl;
    }
    //const를 사용하여 전달된 문자열이 수정되지 않음을 보장하며, 참조를 사용해 복사를 방지하여 성능을 최적화.

    // 총 로그 개수를 출력
    void showTotalLogs() {
        cout << "Total logs recorded: " << logCount << endl;
    }

    // 소멸자: Logger 객체가 소멸될 때 호출되며 로그를 출력
    ~Logger() {
        cout << "Logger instance destroyed." << endl;
    }
};

int main() {
    // Logger 인스턴스를 unique_ptr로 관리
    // 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;
}

'TIL > C++' 카테고리의 다른 글

챕터 2-3 : STL 기초  (1) 2025.01.02
챕터 2-2 : 템플릿  (1) 2024.12.31
C++ 추가 자료  (1) 2024.12.27
챕터 1-3 : 객체지향 프로그래밍  (1) 2024.12.26
챕터 1-2 : Class 개념  (0) 2024.12.24