강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

dzzzing님의 프로필 이미지
dzzzing

작성한 질문수

홍정모의 따라하며 배우는 C++

14.5 함수 try

생성자에서 throw처리가 되었을 때 질문입니다.

작성

·

85

·

수정됨

1

객체의 인스턴스화, instantiation 중 예외처리가 발생하게 되면 해당 객체의 소멸에 대해 잘 이해하고 있는게 맞는지 질문 남깁니다.

image.pngimage.png


와 같이 작성 되었다고 했을 때

  1. B *b = new B(0); 를 통해 B타입의 객체 instantiation

  2. new B를 통해서 컴파일러가 힙 메모리에 B객체의 크기만큼 메모리를 확보 ->
    확보받은 메모리 내 멤버변수 정보 초기화를 위해서 B 객체 생성자 진입 ->
    진입 직전 파생클래스인 것 확인 -> 부모 initialization_list 실행 ->
    부모 생성자 호출 -> 자식 initialization_list 실행 -> 자식 생성자 호출

  3. 위 순서에 의해 객체가 생성되는걸로 아는데, 부모 생성자에서 throw를 하여 StackUnwinding을 하면 예외처리에 의해 객체 생성을 완료하지 못하게 되고, 할당 받은 메모리의 위치(힙, 스택)에 상관없이 컴파일러가 소멸처리를 진행한다.

  4. 이때, A, B 둘다 생성자 호출 중에 Instancing이 완료되지 못했으므로 소멸자 호출에 의한 객체 소멸이 이루어지지 않는다.

image.pngimage.png

 

과 같이 A 클래스에 파생클래스가 아닌 C객체를 가지고 있고 예외처리를 자식 클래스 B에게 두면?

  1. A의 생성자는 호출이 완료되었고, B의 생성자에서 호출 시 예외처리가 발생한 것이므로

  2. "생성이 완료"된 부모객체와, 부모가 가지고있는 C객체는 "소멸자 호출"에 의해 처리된다.

  3. B의 경우 생성 도중 예외처리가 발생하였으니 컴파일러가 소멸 시켜준다.

하지만 일반함수 Stack Unwinding에서도 똑같이 함수 StackFrame 내 에서 이루어진 동적할당에 대한 소멸 책임은 프로그래머에게 있다. B객체의 catch()에서 delete[] 가 없을 시 메모리 누수가 발생한다. 

로 강의 해주신것을 전반으로 이것저것 시도해보며 정리 해보았습니다.
Q) 요약 하자면 생성이 완료된 부모 객체부분, 멤버변수 객체는 당연히 각자의 소멸자 호출에 의해 소멸이 될것이고, 생성자 호출 도중 예외처리 된 객체 부분은 "취소"처리로 보며 스택, 힙에 할당되었든 취소된 부분은 컴파일러가 처리해준다. 가 맞을까요?

답변 1

0

 

안녕하세요? 질문&답변 도우미 durams입니다.


먼저 첫 번째 코드 아래에 달아주신 코멘트에 대해 하나씩 살펴보도록 할게요.

  • "컴파일러가 메모리를 확보"

     

    • 컴파일러는 메모리를 확보하는 주체가 아닙니다. 실제 런타임에서 메모리의 할당은 운영체제에 대한 요청(시스템 콜)을 통해 이루어집니다.

  • "진입 직전 파생클래스인 것 확인"

    • 어떤 객체가 파생 클래스인지의 여부는 런타임이 아니라 컴파일 타임에 확인됩니다.

  • "B 객체 생성자 진입 -> 부모 member initializer list 실행 -> 부모 생성자 호출 -> 자식 member initializer list 실행 -> 자식 생성자 호출"

    • 흐름은 대체적으로 맞으나, 보다 정확하게 말하자면 아래와 같습니다.

    • B 클래스의 member initializer list 실행 -> 도중 부모 클래스 A에 대한 위임 생성자 호출 -> A의 member initializer list와 생성자 body 실행 -> B의 생성자 body 실행이라고 보는 것이 더 적절할 것 같습니다.

  • "부모 생성자에서 throw를 하여 StackUnwinding을 하면 예외처리에 의해 객체 생성을 완료하지 못하게 되고"

    • 인과관계가 잘못된 것 같습니다. Stack Unwinding은 예외가 발생했을 때 객체 생성이 완료되지 못하고 빠져나가는 과정에서 자동으로 수행되는 정리 절차입니다. 스택을 거슬러올라가면서 적절한 catch문을 찾고 local 객체(정확히는 스택에 있는)들의 소멸자를 호출하며 메모리를 해제하게 됩니다.

  • "할당 받은 메모리의 위치(힙, 스택)에 상관없이 컴파일러가 소멸처리를 진행한다."

    • 컴파일러는 단순히 프로그래머가 작성한 소스 코드 파일을 어셈블리어로 번역해주는 번역 프로그램입니다. 따로 런타임에 메모리 관리를 하지는 않습니다.

    • Stack Unwinding이 되도록 결과 코드를 생성하지만, 그 자체가 메모리 해제의 주체는 아닙니다.

    • 스택에 할당된 경우는 Stack Unwinding으로 해제가 되지만, 힙에 할당한 경우는 exception handler에서 직접 해제해주는 등 직접적인 제어가 필요합니다. 이는 알고 계시듯이 객체를 힙에 할당한 경우는 local scope를 벗어난다고 해서 소멸자가 호출되지 않기 때문입니다.

  • "이때, A, B 둘다 생성자 호출 중에 Instancing이 완료되지 못했으므로 소멸자 호출에 의한 객체 소멸이 이루어지지 않는다."

    • 혹시 AB의 객체가 각각 생성된다고 써주신 것일까요? 만약 그렇다면 오해가 있는 것으로 보입니다. 보여주신 예제에서는 파생 클래스 B의 객체만 생성하고 있으며, 실제로는 파생 클래스 객체의 생성 과정의 안에 기반 클래스 부분을 생성하는 과정이 포함되었다고 보는 것이 옳습니다.

    • 모든 객체의 소멸이 이루어지지 않는 것은 아닙니다. AB 모두 소멸자가 호출되지는 않았지만, 단순 멤버 변수에 해당하는 m_iAm_iB는 따로 처리를 해주지 않아도 자동으로 소멸합니다. 하지만 m_pCharA의 생성자에서 동적 할당을 진행하지만 적절한 소멸자가 호출되지 않기 때문에, exception handler(catch)에서의 처리가 필요한 것입니다.

    • 그래서 사실 본 코드에서는 A의 소멸자에도 delete[] m_pChar;가 있어야 적절합니다. throw되지 않은 경우에 대해서도 m_pChar의 소멸을 보장하기 위해서입니다.


    다음으로 개선된 버전에 대해서 살펴보면 아래와 같습니다.

  • "생성이 완료"된 부모객체와, 부모가 가지고있는 C객체는 "소멸자 호출"에 의해 처리된다."

    • 앞서 말씀드린 대로, main에서 생성하고자 하는 것은 클래스 B의 객체 하나입니다. 기반 클래스의 객체가 따로 생성된다고 말하지는 않습니다. (멤버로 객체가 있는 것은 별개입니다)

    • 생성자/소멸자가 호출되었다고 해서 각 호출이 별도의 객체에 대한 동작은 아닐 수 있습니다. 파생 클래스 객체를 생성되었다가 소멸할 때, 여러 생성자와 소멸자가 호출되는 것을 생각해보시면 될 것 같습니다.

  • "B의 경우 생성 도중 예외처리가 발생하였으니 컴파일러가 소멸 시켜준다."

    • 객체 생성 도중 예외가 발생했기 때문에 Stack Unwinding을 통해 C와 같은 객체들의 소멸자가 호출되는 것은 맞습니다만, 컴파일러가 소멸시켜주는 것은 아닙니다.

마지막 질문에 대해서는 위 내용들로 충분히 답변이 되었으리라 생각합니다.

더 궁금한 내용이 있으시다면 알려주세요.

 

dzzzing님의 프로필 이미지
dzzzing

작성한 질문수

질문하기