ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 자원관리
    프로그래밍 기초/C++ 2022. 6. 17. 07:30

      자원관리 

    자원 관리에는 객체가 그만!

    자원을 안전하게 사용할 수 있는 조건

    자원 관리를 수동으로 하게 되면 예상하지 못한 시점에 함수가 반환되거나 예외가 발생하는 경우 처리가 어려워지게 된다.

    이 것을 막기 위해 자원을 특정 객체에 넘기고 객체가 소멸될 때 자원을 삭제하도록 하면 안전하게 처리가 가능하다.

     

    이 자원 관리 객체는 다음의 조건을 만족해야 한다.

    • 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
    • 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실하게 해제되도록 한다.

     

    자원 관리 객체의 예와 주의 사항

    대표적인 자원 관리 객체에는 스마트 포인터가 있다.

    그런데 스마트 포인터는 객체의 제거 시 delete를 사용하기 때문에 배열을 동적 생성했을 때 안전하게 삭제해 줄 수 없다.

     

    이러한 이유는 대부분의 배열은 vector 또는 string으로 처리가 가능하기 때문인데, 만약 죽어도 배열을 동적 할당해 써야 한다면 부스트 라이브러리의 scoped_arrayshared_array를 사용하면 된다.

     

    자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자

    스마트 포인터가 적합하지 않은 경우

    힙에서 관리되는 자원이 아닌 경우에는 스마트 포인터로 처리하는 게 알맞지 않다.

    이 경우 스스로 자원 관리 클래스를 만들어야 할 수 있다.

     

    그 예로 뮤텍스를 관리하는 클래스가 있는 경우 다음과 같이 만들 수 있다.


    class Lock {

    public:

        explicit Lock(Mutex* pm)

            : mutexPtr(pm)

        { lock(mutexPtr); } // 생성자에서 락을 걸고

        ~Lock() { unlock(mutexPtr); } // 소멸자에서 락을 해제한다.

    private:

        Mutex* mutexPtr;

    };


     

    자원 관리 클래스의 복사 처리 방법

    자원 관리 클래스에서 복사가 필요한 경우 자신의 의도에 따라 여러 방식으로 처리할 수 있다.

    • 복사를 금지한다 복사 관련 함수를 private으로 처리해 복사가 되지 않도록 하는 방식이다.
    • 관리하고 있는 자원에 대해 참조 카운팅을 수행한다 shared_ptr처럼 복사마다 카운트를 적용하는 방식이다.
    • 관리하고 있는 자원을 진짜로 복사한다 깊은 복사로 자원을 복사해 두 개의 인스턴스가 되게 만드는 방식이다.
    • 관리하고 있는 자원의 소유권을 옮긴다 auto_ptr처럼 복사 시도가 된 경우 복사가 아닌 이동이 되도록 하는 방식이다.

     

    자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

    외부 접근이 필요한 이유와 방법

    API를 사용하거나 하위 클래스의 함수가 필요한 경우 형 변환이 필요할 수 있다.

    이때, 자원 관리 클래스 자체를 사용할 수 없으므로 자원에 접근할 방법이 필요하다.

     

    자원의 외부 접근 방식은 다음 두 가지로 나뉜다.

    • 암시적 변환 대입 연산을 통해 변환이 되도록 만드는 방법
    • 명시적 변환 직접적으로 함수 호출을 통해 자원을 접근하는 방법

     

    두 방법에 장단점이 있는데 암시적 변환은 코드 사용성이 증가하지만 위험성도 증가한다는 문제가 있고 명시적 변환은 그와 반대로 위험성은 낮지만 코드 사용성이 떨어지게 된다.

     

    따라서 전체 프로그램의 방향성에 따라 적절한 방법을 선택하면 된다.

     

    ※ 자원 관리 클래스는 데이터 은닉이 목적이 아니기 때문에 자원을 반환하는 게 큰 문제가 되지는 않는다.

     

    new 및 delete를 사용할 때는 형태를 반드시 맞추자

    delete의 동작 방식

    기본적인 delete는 주어진 객체의 소멸자를 호출하고 메모리에서 제거한다.

    반면에 delete[]는 컴파일러에게 주어진 객체가 배열이라는 사실을 전달하게 되고 컴파일러는 해당 포인터 앞의 몇 바이트를 읽어 배열의 크기를 확인하고 그 크기만큼 객체의 소멸자를 호출하고 메모리에서 제거한다.

     

    형태를 맞추지 않을 때 문제가 되는 경우

    • 배열이 아닌 객체를 delete[]로 제거하는 경우 객체 앞의 몇 바이트를 읽고 그만큼 제거하려고 시도하지만 해당 위치에 소멸자가 없거나 다른 객체가 있을 것이므로 에러가 발생한다.
    • 배열을 delete로 제거하는 경우 주어진 배열의 첫 번째 객체만 제거하고 나머지 요소들은 메모리에 그대로 남게 된다.

     

    new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자

    C++의 연산 실행 순서

    다른 언어와 달리 C++는 연산 실행 순서가 컴파일러 제조사마다 다르다.

    그렇기 때문에 스마트 포인터에 자원이 저장되기 전에 에러가 발생하는 경우가 있을 수 있다.

     

    다음은 문제가 될 수 있는 코드이다.


    processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());


     

    위 코드의 실행 순서는 new Widget은 항상 먼저 실행되지만 shared_ptr의 생성자와 priority()의 실행 순서는 바뀔 수 있다.

    그런데 priority() 가 먼저 실행되는 경우에 priority()에서 에러가 발생한다면 스마트 포인터에 자원이 들어가기 전에 에러가 발생했으므로 메모리가 유출될 수 있다.

     

    메모리 유출을 막는 초기화 방법

    문장의 연산 실행 순서가 정해져 있지 않다면 문장을 구분하면 된다.


    std::tr1::shared_ptr<Widget> pw(new Widget);

    processWidget(pw, priority());


     

    댓글

Designed by Tistory.