프로그래밍에서 자원(Resource)이란, 사용 후에 시스템에 돌려주는 모든 것을 통칭한다. C++ 프로그래밍에서는 동적으로 할당한 메모리는 메모리 누수가 발생하지 않도록 적절히 해제해주어야 한다.

 

목차

     

     


     

     

    Effective C++ : Chapter 3  자원관리


     

     

    항목 13:  자원 관리에는 객체가 그만!

     

    Q. 아래 코드를 사용하면 어떤 상황이 벌어질까요?

    shared_ptr<int> spi(new int[1024]); 
    
    1. 컴파일도 잘 된다. 동적 배열에 대한 메모리가 문제없이 잘 해제된다.
    2. 컴파일 에러가 발생한다. 메모리가 해제되지 않는다.
    3. 컴파일 에러가 발생하지 않는다. 메모리가 해제되지 않는다.
    더보기

    shared_ptr 는 소멸자 내부에서 delete[] 연산자가 아니라 delete 연산자를 사용하므로 동적으로 할당한 배열에 대해 shared_ptr 를 사용하면 메모리 누수가 발생합니다.

    대신 동적 할당 배열을 위해서 vector나 string으로 사용할 수 있습니다.

     

     

    Q. 자원 관리에 객체를 사용하는 방법으로 무엇이 있을까요?

    더보기

    자원을 획득한 후에 자원 관리 객체에게 넘기는 방버이 있습니다.

    자원 획득 즉 초기화(Resource Acquisition Is Initialization: RAII)라고 불리는 이 방식은 자원 획득과 자원 관리 객체의 초기화가 한 문장에서 이루어집니다.

     

     

    Q. RAII ( Resource Acquisition is Initalization )에 대해 설명해보세요.

    더보기

    RAII 방식은 생성자에서 자원을 획득(초기화)하고 소멸자에서 그것을 해제합니다.

     

    RAII 패턴은 생성자 함수에서 생성하고, 소멸자 함수에서 정리하는 패턴을 의미합니다. RAII의 경우 뮤텍스와 같이 멀티 스레드 환경에서도 많이 사용하는 걸로 알고, 생성과 동시에 소멸을 진행하고 싶을 때 하나의 클래스로 묶어서 관리합니다. ( Mutex는 RAII 패턴으로 만들어준다. )

     

     

    Q. RAII ( Resource Acquisition is Initalization )의 방식으로 자원을 획득 했을 때 장점이 무엇일까요?

    더보기

    RAII 방식을 사용하면 자동으로 호출하는 호출자에 의해서 delete가 호출되어 자원 해제를 할 수 있습니다.

    delete 호출을 잊어버리고 넘어가기 쉽지만 소멸자에서 자동 호출해주기 때문에 실수를 방지할 수 있습니다.

     

     

    Q. 언리얼엔진에서 RAII과 비슷한 역할을 하는 클래스는 무엇일까?

    더보기

    TObjectPtr

    확인 필요.

     

     

    #include <iostream>
    using namespace std;
    
    class Investment {
    public:
        //...
    };
    
    Investment* createInvestment()
    {
        Investment* pInv_{};
    
        return pInv_;
    }
    
    void func_1() 
    {
        Investment* pInv = createInvestment();
    
        // ...
        // return;         // 이게 있으면 도중 하차 가능성
    
        delete pInv;       // 반환된 pInv에 대한 까먹고 삭제안할 수도 있다. 
    }
    
    void func_2()
    {
        std::shared_ptr<Investment> pInv1(createInvestment()); // 자동 관리
    
        std::shared_ptr<Investment> pInv2(pInv1);
    
        pInv1 = pInv2;
        // ...
    
        // return;         // 이게 있으면 도중 하차 가능성
    
        // plnv1 및 plnv2는 소멸되며 이들이 가리키고 있는 객체도 
        // 자동으로 삭제됩니다.
        // => 자원을 관리하는 객체를 써서 자원을 관리하는 것이 중요하다.
    
        // 아직 완전판이 아니고 항목 18을 읽어야 한다.
    }
    
    int main() {
        func_2();
        return 0;
    }

     

     


     

     

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

     

    Q. RAII 객체의 복사 동작에 대해 설계할 때 할 수 있는 방법 중 하나인 복사를 금지하는 방법은 어떻게 할까요?

    더보기

    복사 연산를 private 멤버로 만들면 됩니다.

     

     

    Q. RAII 객체가 복사될 때 어떤 동작이 이루어져야 할까요?

    더보기

    해답#1:  복사 금지 private 사용

    복사를 금지합니다. 어떤 스레드 동기화 객체에 대한 ‘사본’이라는게 실제로 거의 의미가 없습니다. 복사하면 안 되는 RAII 클래스에 대해서는 반드시 복사가 되지 않도록 막아야 합니다.

     

    해답#2:  참조 카운팅. Shared_ptr

    관리하고 있는 자원에 대해 참조 카운팅을 수행합니다. 자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 유지해야하는 경우가 있습니다 . 이럴 경우에는, 해당 자원을 참조하는 객체의 개수에 대한 카운트를 증가시키는식으로 RAII 객체의 복사 동작을 만들어야 합니다.

     

    해답#3:  깊은 복사(deep copy)

    관리하고 있는 자원을 진짜로 복사합니다.

     

    해답#4:  소유권 이전

    관리하고 있는 자원의 소유권을 옮깁니다.

     

     

    Q. RAII 패턴으로 이루어진 객체에 대해 복사를 어떻게 진행해야할까?

    더보기

    해답#1:  아예 지원하지 않는다.
    해답#2:  참조 카운트를 사용한다.
    해답#3:  관리하고 있는 자원을 진짜 복사한다.
    해답#4:  move(or auto_ptr)과 같이 자원을 옮긴다.

     

     

    Q. 아래 코드는 컴파일러가 생성한 소멸자를 호출할까요? 아니라면 어떻게 자원 해제를 하게 될까요?

    class Lock {
    public:
    	explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) {
    		lock(mutexPtr.get());
    	}
    private:
    	shared_ptr<Mutex> mutexPtr;
    }
    

     

    더보기

    클래스의 소멸자(컴파일러가 만들었든 사용자가 정의했든)는 비정적 데이터 멤버의 소멸자를 자동으로 호출하게 되어 있습니다. 이 때 비정적 데이터 멤버가, Mutex를 가리키는 shared_ptr 인 mutexPtr 인데 shared_ptr 는 삭제자(deleter) 지정이 허용되므로 shared_ptr가 유지하는 참조 카운트가 0이 될 때 (Mutex객체에 대한 참조 카운트가 0이 될 때) 삭제자가 자동으로 호출됩니다.

     

    126쪽 참고

     

     

    Q. 아래 코드에서 s1의 값을 수정하면 s2의 값도 수정될까요?

    #include <string>
    
    int main() {
    		...
        std::string s1 = "scott";  // 원본 문자열 생성
        std::string s2 = s1;       // s1을 복사하여 s2 생성
        ...
        return 0;
    }
    

     

     

    더보기

    답:  수정되지 않습니다.

    string 타입으로 생성한 객체를 복사하면 그 사본은 포인터 및 그 포인터가 가리키는 (새로운) 힙 메모리를 갖게 됩니다. 즉 string 타입의 객체를 복사하면 자동으로 깊은 복사가 이루어집니다. 즉, 새 객체는 원본 객체의 문자열 데이터의 별도 사본을 가지게 되어, 원본 객체와는 독립적으로 데이터를 관리하게 됩니다.

    위 코드에서 s1과 s2는 같은 값을 가지지만, 서로 독립적인 메모리 영역에 데이터를 저장합니다. 이는 s1을 수정해도 s2에 영향을 미치지 않습니다. 자원 관리 객체를 복사하면 그 객체가 둘러싸고 있는 자원까지 복사되어야 합니다. 즉 깊은 복사를 수행해야 합니다.

     

    참고: 127쪽


     

     

     

     

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

     

    Q. 아래의 코드에서 에러가 난 이유를 설명하고 에러가 난 부분을 찾고 고치십시오.

    int dayHeld(const Investment *pi) // 투자금이 유입된 이후로 경과한 날 수.
    {
    	...
    }
    
    int main() {
      std::tr1::shared_ptr<Investment> pInv(createInvestment());
      
      dayHeld(pInv); // 경과날을 파악
    }
    더보기

    get이라는 멤버 함수를 사용하여 각 타입으로 만든 스마트 포인터 객체에 들어있는 실제 포인터(의 사본)을 사용합니다. (=스마트 포인터에 들어있는 실제 포인터의 사본을 얻어낼 수 있음)

    dayHeld(pInv.get()); // 포인터로 전달
    

     

     

    Q. shared_ptr에서 실제 포인터에 접근하려면 어떻게 해야 할까요?

    더보기

    get()함수를 호출하면 됩니다. (사본입니다)

    명시적/암시적으로 형변환이 가능합니다.

     

     

     

    Q. RAII 클래스의 auto_ptr을 사용했더니, 객체지향의 캡슐화를 위반하는 일이 발생하였다. 해결하려면 어떤 RAII클래스를 사용하여야 하는가?

    더보기

    shared_ptr 를 사용하여야 합니다.


     

     

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

     

    Q. 아래 코드에서 동적 할당된 자원을 소멸하기 위한 구문으로 올바른 것을 골라주세요.

    string *strintPtr = new string[100];
    
    1. delete stringPtr;
    2. delete [] stringPtr;
    3. delete [100] stringPtr;
    더보기

    답:  2번, 3번 모두 정답입니다.  (참고 133쪽)

    delete가 '포인터가 배열을 가리키고 있구나' 라는 걸 알게 해주려면 [] 대괄호 쌍을 delete 뒤에 붙여줘야 합니다.

    delete는 앞쪽의 메모리 몇 바이트를 읽고 이것을 배열 크기라고 해석하고, 배열 크기에 해당하는 횟수만큼 소멸자를 호출하기 시작합니다.

     

    배열의 이름은 곧 인덱스 0번째의 주소입니다. 따라서 delete 배열이름은 배열 전체가 아닌 배열[0]의 메모리만 해제하는 것입니다. 어떤 포인터에게 delete연산자로 하여금 ‘배열 크기 정보가 있다’를 알려줘야 합니다. 이때, 대괄호 쌍을 delete 뒤에 붙여줘야 합니다.

     

    1번을 테스트 해보면 아래와 같은 오류를 뱉으며 컴파일 되지 않는다.

     

    3번을 테스트 해보니 오류도 없고 컴파일도 잘 된다.

     

     

    Q. [ 빈 칸 ]을 작성하여 pal에 할당된 메모리를 해제시키세요.

    [ 빈 칸 ]을 작성하여 메모리를 해제시켰을때 소멸자 호출 횟수는 몇 번일까요?

    typedef std::string AddressLines[20]
    std::string *pal = new AddressLines;
    
    [  빈  칸  ] 
    
    더보기

    delete [ ] pal;

    20개의 객체에 delete 연산자가 적용되기 때문에 20번의 소멸자 호출이 발생합니다.

    delete [ ] 표현식을 쓸 경우에는 기존에 할당된 메모리에 대해 한 개 이상의 소멸자가 호출되고, 그 후에 그 메리가 해제됩니다.

     

     

    Q. 아래의 빈칸에 들어갈 것으로 알맞은 것은?

    //...
    int main()
    {
        typedef vector<string> ArraySandwiches;
        ArraySandwiches* Sandwiches = new ArraySandwiches;
    
        [ 빈 칸 ]
        return 0;
    }
    1. delete[ ] Sandwiches
    2. delete Sandwiches
    더보기

    답: 2번 delete Sandwiches

     

    1번으로 실행 시 아래와 같은 오류 발생.


     

     

     

     

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

     

    Q. 위에 코드는 문제가 있고 아래의 코드는 문제가 없는 이유를 설명해보세요.

    int priority();
    void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
    std::tr1::shared_ptr<Widget> pw(new Widget);
    
    int priority();
    void processWidget(pw, int priority);
    더보기

    [ 위 코드 ]

    컴파일러는 processWidget 호출 코드를 만들기 전에, 우선 이 함수의 매개변수로 넘겨지는 인자를 평가합니다. 여기서 각각의 연산이 실행되는 순서는 컴파일러 마다 다릅니다. priority 호출 부분에서 예외가 발생한다면 tr1::shared_ptr에 저장되기도 전에 예외가 발생하여 “new Widget”으로 만들어졌던 tr1::shared_ptr포인터가 유실될 수 있습니다. 자원이 생성되는 시점(”new Widget”을 통한)과 그 자원이 자원 관리객체로 넘어가는 시점 사이에 예외가 끼어들 수 있기 때문입니다.

     

    [ 아래 코드 ]

    new로 생성한 객체를 shared_ptr로 담는 코드를 하나의 독립적인 문장으로 만들었습니다.

    이렇게 하면 “new Widget” 표현식과 tr1::shared_ptr 생성자는 한 문장에 들어 있고, priority를 호출하는 코드는 별도의 문장에 있습니다. 그렇기 때문에 컴파일러가 priority 호출을 둘 사이로 옮기고 싶어도 허용이 안 됩니다.

     

    ※ 정리: new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만듭시다!

     

     

    Q. 아래 코드처럼, 처리 우선순위를 알려주는 함수가 하나 있고, 동적으로 할당한 Widget 객체에 대해 어떤 우선순위에 따라 처리를 적용하는 함수가 하나 있다고 가정하겠습니다.

    이 때 Widget 객체를 어떻게 생성하면 좋을지 1~3번 중에서 골라주세요 (객관식)

    // 문제 코드
    int priority();
    processWidget(shared_ptr<Widget> pw, int priority);
    
    // 1번. 
    	processWidget(new Widget, priority());
    // 2번. 
    	processWidget(shared_ptr<Widget>(new Widget), priority());
    // 3번. 
        shared_ptr<Widget> pw(new Widget);
        processWidget(pw, priority());
    더보기

    답:  3번  (135~137쪽 참고)

    • 1번은 컴파일이 됩니다. 그러나 자원을 흘릴 가능성이 있습니다. 자원이 생성되는 시점(new Widget을 통한) 과 그 자원이 자원 관리 객체로 넘어가는 시점 사이에 예외가 끼어들 수 있기 때문입니다.
    • 2번은 컴파일이 안됩니다. 포인터를 받는 shared_ptr 의 생성자는 explicit 로 선언되어 있으므로 new Widget 표현식에 의해 만들어진 포인터가 shared_ptr 타입의 객체로 바꾸는 암시적인 변환이 불가능하기 때문입니다.
    • 3번에서는 new로 생성한 객체를 스마트 포인터에 담는 코드를 하나의 독립적인 문장으로 만들었기에, 한 문장 안에 있는 연산들보다 문장과 문장 사이에 있는 연산들이 컴파일러의 재조정을 받을 여지가 적으므로 자원 누출 가능성이 없습니다.

     

     

    Q. 다음 코드에서 processWidget에 들어갈 첫 번째 매개변수를 어떻게 넣어야 할까 ?

    void processWidget( std::shared_ptr<Widget> , int priority);
    
    processWidget( ?? , priority());
    1. std::shared_ptr<Widget> pw(new Widget);
      pw
    2. new Widget
    3. shared_ptr<Widget>(new Widget)
    더보기

    답:  1번

    void processWidget( std::shared_ptr<Widget> , int priority);
    
    std::shared_ptr<Widget> pw(new Widget);
    processWidget( pw, priority() );

     

     

    ※ 참고

    • shared_ptr (스마트포인터 등장시기 C++11)
    • weak_ptr (C++14 등장)
    • auto_ptr (C++11/14 사용금지 권고, C++17 제거)