[GeeksForGeeks C++ 문제풀이] Destructors(소멸자)

Jin Hur·2022년 10월 26일
0

C++

목록 보기
17/18

https://www.geeksforgeeks.org/virtual-destructor/

Q1. private 소멸자

private 접근 지정자로 선언된 소멸자를 private 소멸자라고 한다. 객체의 파괴를 막고 싶을 때 소멸자를 private으로 지정할 수 있다.

이 private 소멸자는 언제 사용되는 것일까?
클래스의 객체 파괴를 작성자가 직접 제어하고 싶을 때마다 소멸자를 private로 지정할 수 있다.

동적으로 생성된 객체의 경우 객체에 대한 포인터를 함수에 전달하면 함수내에서 객체를 소멸할 수도 있다. 함수 호출 후 객체가 참조되면 참조는 댕글링(Dangling)이 된다.

{
   char *dp = NULL;
   /* ... */
   {
       char c;
       dp = &c;
   }
     /* c falls out of scope */
     /* dp is now a dangling pointer */
}

댕글링 포인터

댕글링 포인터를 사용하면 다음과 같은 문제점들이 야기될 수 있다.
(1) 메모리 접근시 예측 불가능한 동작이 발생할 수 있다.
(2) 메모리 접근 불가 시 Segmentation fault 런타임 에러로 프로그램이 죽을 수 있다.
(3) 잠재적인 보안 위험이 된다.

프로그래머가 객체의 생성과 소멸을 직접 관리하기 위하여 소멸자를 private으로 지정하고 관리 할 수 있다.

private 소멸자

#include <iostream>
using namespace std;
  
class Test {
private:
    ~Test() {}
};

int main() { Test t; }

먼저 위 코드에서 Test 클래스의 소멸자는 private으로 지정되어있다. 위 코드를 실행시키면 컴파일 에러가 발생한다.

// 출력
prog.cpp: In function ‘int main()’:
prog.cpp:8:5: error: ‘Test::~Test()’ is private
    ~Test() {}
    ^
prog.cpp:10:19: error: within this context
int main() { Test t; }	// 컴파일 error

main 함수 수행이 종료되고 지역변수로 선언된 객체(t)가 소멸되지 않음을 컴파일러가 알아채기 때문이다.

그렇다면 아래 코드는 어떨까?

#include <iostream>
using namespace std;
  
class Test {
private:
    ~Test() {}
};
int main() { Test* t = new Test; }	// OK

위 코드는 정상 작동한다. 무언가가 동적 할당을 통해 생성된다면 이 무언가를 소멸시키는 것은 프로그래머의 책임이 된다. 즉 컴파일러의 역할이 아니라는 것이다.

주의할 점은 private 소멸자를 가진 클래스의 객체를 외부에서 delete 하면 안된다.

#include <iostream>
using namespace std;
  
class Test {
private:
    ~Test() {}
};
  
// Driver Code
int main()
{
    Test* t = new Test;
    delete t;	// 컴파일 error
}

멤버 메서드로 private 소멸자를 호출하는 방식을 사용하든가, 프렌드 함수를 사용하여 객체를 삭제하는 방식을 사용한다.

class Test {
private:
    ~Test() {}
  
public:
    friend void destructTest(Test*);
};
  
// Only this function can destruct objects of Test
void destructTest(Test* ptr) { delete ptr; }
  
int main()
{
    // create an object
    Test* ptr = new Test;
  
    // destruct the object
    destructTest(ptr);
  
    return 0;
}

컴파일러를 통해 자동 소멸되는 방식이나, 복잡한 함수 호출 간에 알게 모르게 동적으로 할당 받은 객체가 소멸되어 댕글링 포인터가 생기는 문제를 막는 데 도움을 받을 수 있다.


Q2. 컴파일러를 통한 소멸자 호출 시점

foo() 함수에서 지역으로 ob 객체가 생성된다. 그리고 이 객체는 foo() 함수 '블록'을 벗어나는 시점에 소멸자가 호출되어 소멸된다.
따라서 return i(==3);이 먼저 수행되고 이 후에 소멸자가 호출되기에 최종적으로 3이 출력된다.

그러나 아래와 같이 추가적으로 블록으로 묶으면 10을 출력한다.

int i;
class A {
public:
	~A() {
		i = 10;
	}
};
int foo() {
	i = 3;
	{
		A ob;
	}
	return i;
}

int main() {
	cout << foo() << endl;
}

Q3. 소멸자는 클래스 당 오직 하나!

소멸자는 클래스 당 오직 하나씩만 선언할 수 있으며, 매개변수를 선언할 수 없다.


Q4. 소멸자 호출 순서

위 코드에서 객체가 하나씩 생성될 때마다 정적 변수를 통하여 증분된 값으로 id 값이 하나씩 배정받는다. 따라서 생성자 호출 순서와 출력은 예측할 수 있다.

소멸자의 경우는 블록 내 생성 순서와 반대이다. 그 이유는 나중에 생성된 객체는 먼저 생성된 객체를 참조하는 상황이 있을 수 있기 때문이다.

A a;
B b(a);
// In the above code, the object ‘b’ (which is created after ‘a’), may use some members of ‘a’ internally. 
// So destruction of ‘a’ before ‘b’ may create problems. 
// Therefore, object ‘b’ must be destroyed before ‘a’.

Q5. 가상 소멸자

가상 소멸자를 사용하는 이유: https://www.geeksforgeeks.org/virtual-destructor/

As a guideline, any time you have a virtual function in a class, you should immediately add a virtual destructor (even if it does nothing). This way, you ensure against any surprises later.

가상함수로 정의되지 않은 소멸자가 있는 기본 클래스형의 포인터로 파생 클래스 객체를 소멸시키면 정의되지 않은 동작이 발생할 수 있다.

class base {
  public:
    base()    
    { cout << "Constructing base\n"; }
    ~base()
    { cout<< "Destructing base\n"; }    
};
 
class derived: public base {
  public:
    derived()    
     { cout << "Constructing derived\n"; }
    ~derived()
       { cout << "Destructing derived\n"; }
};
 
int main()
{
  derived *d = new derived(); 
  base *b = d;
  delete b;
  getchar();
  return 0;
}

위 코드에선 다음과 같이 출력된다.

Constructing base
Constructing derived
Destructing base

즉 파생 클래스 객체의 소멸자가 호출되지 않은 것이다.
이유는 당연하다. 정적 바인딩으로 부모 클래스의 소멸자만 호출되었기 때문이다.
이에 따라 자식 클래스의 데이터 공간은 제대로 회수되지 못하여 (1) 메모리 누수(memory leak)이 발생하거나 (2) 관련된 자원(파일, 소켓 등)을 제대로 회수하지 못하는 등의 문제가 발생한다.

따라서 다음 코드와 같이 부모 클래스의 소멸자를 가상함수로 정의해야 한다.

class base {
  public:
    base()    
    { cout << "Constructing base\n"; }
    virtual ~base()
    { cout << "Destructing base\n"; }    
};
 
class derived : public base {
  public:
    derived()    
    { cout << "Constructing derived\n"; }
    virtual ~derived() override
    { cout << "Destructing derived\n"; }
};

결과는 다음과 같다.

Constructing base
Constructing derived
Destructing derived
Destructing base

0개의 댓글