나는 대부분의 OO 이론을 잘 이해하고 있지만 나를 혼란스럽게 만드는 것은 가상 소멸자입니다.
체인의 모든 객체에 대해 소멸자가 항상 호출된다고 생각했습니다.
언제 가상으로 만들어야합니까?
virtual
중간 대신 맨 위에서 시작해야합니다.
나는 대부분의 OO 이론을 잘 이해하고 있지만 나를 혼란스럽게 만드는 것은 가상 소멸자입니다.
체인의 모든 객체에 대해 소멸자가 항상 호출된다고 생각했습니다.
언제 가상으로 만들어야합니까?
virtual
중간 대신 맨 위에서 시작해야합니다.
답변:
가상 소멸자는 기본 클래스에 대한 포인터를 통해 파생 클래스의 인스턴스를 잠재적으로 삭제할 수있는 경우에 유용합니다.
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
여기에서 Base의 소멸자를로 선언하지 않았다는 것을 알 수 있습니다 virtual
. 이제 다음 스 니펫을 살펴 보겠습니다.
Base *b = new Derived();
// use b
delete b; // Here's the problem!
자료의 소멸자가 아니므로 virtual
하고 b
A는 Base*
A와 가리키는 Derived
객체 delete b
가 정의되지 않은 동작 :
[In
delete b
]에서 삭제할 객체의 정적 유형이 동적 유형과 다른 경우 정적 유형은 삭제할 객체의 동적 유형의 기본 클래스 여야하며 정적 유형에는 가상 소멸자 또는 동작이 정의되지 않았습니다 .
대부분의 구현에서 소멸자에 대한 호출은 가상이 아닌 코드와 같이 해결됩니다. 즉, 기본 클래스의 소멸자는 호출되지만 파생 클래스 중 하나는 호출되지 않으므로 리소스가 누출됩니다.
요약하면 항상 기본 클래스의 소멸자를 만듭니다. virtual
는 다형성으로 조작 될 때 해야합니다.
기본 클래스 포인터를 통한 인스턴스 삭제를 방지하려면 기본 클래스 소멸자를 보호 및 비 가상화로 만들 수 있습니다. 그렇게하면 컴파일러가 호출 할 수 없습니다.delete
에서 기본 클래스 포인터 .
Herb Sutter의이 기사에서 가상 및 가상 기본 클래스 소멸자에 대해 자세히 알아볼 수 있습니다 .
Base
와 Derived
있는 모든 자동 스토리지 변수를? 즉, 소멸자에서 실행할 "특별한"또는 추가 사용자 정의 코드가 없습니다. 그러면 소멸자를 작성하지 않아도 될까요? 아니면 파생 클래스에 여전히 메모리 누수가 있습니까?
가상 생성자는 불가능하지만 가상 소멸자는 가능합니다. 실험 해 보자 .......
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
위의 코드는 다음을 출력합니다.
Base Constructor Called
Derived constructor called
Base Destructor called
파생 객체의 생성은 생성 규칙을 따르지만 "b"포인터 (기본 포인터)를 삭제하면 기본 소멸자 만 호출됩니다. 그러나 이것은 일어나서는 안됩니다. 적절한 작업을 수행하려면 기본 소멸자를 가상으로 만들어야합니다. 이제 다음에서 어떤 일이 발생하는지 봅시다.
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
virtual ~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
출력은 다음과 같이 변경되었습니다.
Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called
따라서 기본 포인터의 파괴 (파생 객체에 할당 됨)는 파괴 규칙, 즉 먼저 파생 된 다음베이스를 따릅니다. 반면에 가상 생성자와 같은 것은 없습니다.
다형성 기본 클래스에서 소멸자를 가상으로 선언하십시오. Scott Meyers의 Effective C ++ 항목 7입니다 . 마이어스는 클래스가있는 경우 것으로 요약에 간다 어떤 가상 함수를,이 가상 소멸자가 있어야하고, 그 클래스는 기본 클래스로 설계되지 또는 다형 적으로 사용할 수 있도록한다 설계되지 아니를 가상 소멸자를 선언합니다.
const Base& = make_Derived();
. 이 경우 Derived
가상이 아닌 경우에도 prvalue 의 소멸자 가 호출되므로 vtables / vpointer에서 발생하는 오버 헤드가 절약됩니다. 물론 범위는 매우 제한적입니다. Andrei Alexandrescu는 그의 책 Modern C ++ Design 에서 이것을 언급했다 .
또한 가상 소멸자가 없을 때 기본 클래스 포인터를 삭제하면 동작 이 정의되지 않습니다 . 내가 최근에 배운 것 :
나는 수년 동안 C ++을 사용해 왔지만 여전히 교수형에 처해있다.
struct Base {
virtual void f() {}
virtual ~Base() {}
};
struct Derived : Base {
void f() override {}
~Derived() override {}
};
Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived
가상 소멸자 호출은 다른 가상 함수 호출과 다르지 않습니다.
의 경우 base->f()
호출이에 전달되고 재정의 함수 Derived::f()
와 동일 하게 호출됩니다.base->~Base()
Derived::~Derived()
소멸자가 간접적으로 호출되는 경우에도 마찬가지입니다 (예 :) delete base;
. delete
문 의지 호출 base->~Base()
에 파견 될 것이다Derived::~Derived()
.
기본 클래스에 대한 포인터를 통해 객체를 삭제하지 않으려는 경우 가상 소멸자가 필요하지 않습니다. protected
실수로 호출되지 않도록하기 만하면 됩니다.
// library.hpp
struct Base {
virtual void f() = 0;
protected:
~Base() = default;
};
void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.
//-------------------
// application.cpp
struct Derived : Base {
void f() override { ... }
};
int main() {
Derived derived;
CallsF(derived);
// No need for virtual destructor here as well.
}
~Derived()
그냥 파생 된 경우에도 모든 파생 클래스에서 명시 적으로 선언해야 ~Derived() = default
합니까? 아니면 언어에 의해 암시됩니까 (생략해도 안전합니까)?
protected
섹션을 넣거나을 사용하여 가상인지 확인하십시오 override
.
인터페이스와 인터페이스 구현에 대해 생각하고 싶습니다. C ++에서 speak 인터페이스는 순수한 가상 클래스입니다. 소멸자는 인터페이스의 일부이며 구현 될 것으로 예상됩니다. 따라서 소멸자는 순수 가상이어야합니다. 생성자는 어떻습니까? 객체는 항상 명시 적으로 인스턴스화되므로 생성자는 실제로 인터페이스의 일부가 아닙니다.
virtual
기본 클래스에서 virtual
선언되면 선언되지 않은 경우에도 자동으로 파생 클래스에 있습니다.
기본 클래스 포인터를 통해 객체를 삭제하는 동안 다른 소멸자가 올바른 순서를 따라야 할 경우 소멸자에 대한 가상 키워드가 필요합니다. 예를 들면 다음과 같습니다.
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
기본 클래스 소멸자가 가상이면 객체는 순서대로 소멸됩니다 (먼저 파생 된 객체 다음 base). 기본 클래스 소멸자가 가상이 아닌 경우 기본 클래스 오브젝트 만 삭제됩니다 (포인터가 기본 클래스 "Base * myObj"이므로). 따라서 파생 객체에 대한 메모리 누수가 발생합니다.
간단하게 말하면 가상 소멸자는 파생 클래스 객체를 가리키는 기본 클래스 포인터를 삭제할 때 적절한 순서로 리소스를 파괴하는 것입니다.
#include<iostream>
using namespace std;
class B{
public:
B(){
cout<<"B()\n";
}
virtual ~B(){
cout<<"~B()\n";
}
};
class D: public B{
public:
D(){
cout<<"D()\n";
}
~D(){
cout<<"~D()\n";
}
};
int main(){
B *b = new D();
delete b;
return 0;
}
OUTPUT:
B()
D()
~D()
~B()
==============
If you don't give ~B() as virtual. then output would be
B()
D()
~B()
where destruction of ~D() is not done which leads to leak
delete
기본 포인터를 호출 하면 정의되지 않은 동작이 발생합니다.
shared_ptr
(unique_ptr이 아닌 shared_ptr 만 사용 ) 기본 클래스 소멸자를 가상으로 가질 필요는 없습니다.
#include <iostream>
#include <memory>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){ // not virtual
cout << "Base Destructor called\n";
}
};
class Derived: public Base
{
public:
Derived(){
cout << "Derived constructor called\n";
}
~Derived(){
cout << "Derived destructor called\n";
}
};
int main()
{
shared_ptr<Base> b(new Derived());
}
산출:
Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called
virtual
키워드는 많은 고통에서 당신을 구할 수 있습니다.
가상 소멸자 또는 가상 소멸자를 사용하는 방법
클래스 소멸자는 클래스 앞에 할당 된 메모리를 재 할당하기 위해 ~로 시작하는 클래스와 동일한 이름을 가진 함수입니다. 가상 소멸자가 필요한 이유
일부 가상 기능이있는 다음 샘플을 참조하십시오.
이 샘플은 또한 글자를 상한 또는 하한으로 변환하는 방법을 알려줍니다
#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
~convertch(){};
};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
Letter[i] = Letter[i] + 32;
return Letter;
}
private:
char *Letter;
bool tolower;
};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
Letter[i] = Letter[i] - 32;
return Letter;
}
virtual ~MakeUpper()
{
cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};
int _tmain(int argc, _TCHAR* argv[])
{
convertch *makeupper = new MakeUpper("hai");
cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";
delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" ";
delete makelower;
return 0;
}
위 샘플에서 MakeUpper 및 MakeLower 클래스의 소멸자가 호출되지 않았 음을 알 수 있습니다.
가상 소멸자가있는 다음 샘플을 참조하십시오.
#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
virtual ~convertch(){}; // defined the virtual destructor
};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] + 32;
}
return Letter;
}
private:
char *Letter;
bool tolower;
};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] - 32;
}
return Letter;
}
virtual ~MakeUpper()
{
cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};
int _tmain(int argc, _TCHAR* argv[])
{
convertch *makeupper = new MakeUpper("hai");
cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";
delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";
delete makelower;
return 0;
}
가상 소멸자는 클래스에서 가장 파생 된 런타임 소멸자를 명시 적으로 호출하여 적절한 방식으로 오브젝트를 지울 수 있습니다.
또는 링크를 방문하십시오
기본 클래스에서 파생 클래스 소멸자를 호출해야 할 때. 기본 클래스에서 가상 기본 클래스 소멸자를 선언해야합니다.
이 질문의 핵심은 소멸자가 아닌 가상 방법과 다형성에 관한 것이라고 생각합니다. 보다 명확한 예는 다음과 같습니다.
class A
{
public:
A() {}
virtual void foo()
{
cout << "This is A." << endl;
}
};
class B : public A
{
public:
B() {}
void foo()
{
cout << "This is B." << endl;
}
};
int main(int argc, char* argv[])
{
A *a = new B();
a->foo();
if(a != NULL)
delete a;
return 0;
}
인쇄합니다 :
This is B.
virtual
그것이 없으면 인쇄됩니다 :
This is A.
이제 가상 소멸자를 언제 사용해야하는지 이해해야합니다.
B b{}; A& a{b}; a.foo();
. 점검 NULL
하는이어야 - nullptr
- 전에 delete
잘못된 indendation으로 - - 보내고 필요하지 않습니다 : delete nullptr;
. 무 조작으로 정의된다 아무것도, 당신은 호출하기 전에이 문제를 확인해야하는 경우 ->foo()
, 달리 new
실패 하면 정의되지 않은 동작이 발생할 수 있습니다 .)
delete
하는 것이 안전합니다 NULL
(즉, if (a != NULL)
경비원이 필요하지 않습니다 ).
가상 정의자가없는 기본 클래스 (/ struct)를 통해 삭제하거나 더 정확하게 vtable이없는 경우 "정의되지 않은"동작 또는 적어도 "충돌"정의되지 않은 동작에 대해 논의하는 것이 도움이 될 것이라고 생각했습니다. 아래 코드는 몇 가지 간단한 구조체를 나열합니다 (클래스에서도 마찬가지입니다).
#include <iostream>
using namespace std;
struct a
{
~a() {}
unsigned long long i;
};
struct b : a
{
~b() {}
unsigned long long j;
};
struct c : b
{
~c() {}
virtual void m3() {}
unsigned long long k;
};
struct d : c
{
~d() {}
virtual void m4() {}
unsigned long long l;
};
int main()
{
cout << "sizeof(a): " << sizeof(a) << endl;
cout << "sizeof(b): " << sizeof(b) << endl;
cout << "sizeof(c): " << sizeof(c) << endl;
cout << "sizeof(d): " << sizeof(d) << endl;
// No issue.
a* a1 = new a();
cout << "a1: " << a1 << endl;
delete a1;
// No issue.
b* b1 = new b();
cout << "b1: " << b1 << endl;
cout << "(a*) b1: " << (a*) b1 << endl;
delete b1;
// No issue.
c* c1 = new c();
cout << "c1: " << c1 << endl;
cout << "(b*) c1: " << (b*) c1 << endl;
cout << "(a*) c1: " << (a*) c1 << endl;
delete c1;
// No issue.
d* d1 = new d();
cout << "d1: " << d1 << endl;
cout << "(c*) d1: " << (c*) d1 << endl;
cout << "(b*) d1: " << (b*) d1 << endl;
cout << "(a*) d1: " << (a*) d1 << endl;
delete d1;
// Doesn't crash, but may not produce the results you want.
c1 = (c*) new d();
delete c1;
// Crashes due to passing an invalid address to the method which
// frees the memory.
d1 = new d();
b1 = (b*) d1;
cout << "d1: " << d1 << endl;
cout << "b1: " << b1 << endl;
delete b1;
/*
// This is similar to what's happening above in the "crash" case.
char* buf = new char[32];
cout << "buf: " << (void*) buf << endl;
buf += 8;
cout << "buf after adding 8: " << (void*) buf << endl;
delete buf;
*/
}
가상 소멸자가 필요한지 여부는 제안하지 않지만 일반적으로 가상 소멸자를 사용하는 것이 좋습니다. 기본 클래스 (/ struct)에 vtable이없고 파생 클래스 (/ struct)가 있고 기본 클래스 (/ struct)를 통해 객체를 삭제하면 충돌이 발생할 수있는 이유를 지적하고 있습니다. 바늘. 이 경우 힙의 사용 가능 루틴으로 전달한 주소가 유효하지 않으므로 충돌의 원인이됩니다.
위의 코드를 실행하면 문제가 발생했을 때 명확하게 표시됩니다. 기본 클래스 (/ struct)의 this 포인터가 파생 클래스 (/ struct)의 this 포인터와 다르면이 문제가 발생합니다. 위의 샘플에서 구조체 a와 b에는 vtable이 없습니다. 구조체 c와 d에는 vtable이 있습니다. 따라서 ac 또는 d 객체 인스턴스에 대한 a 또는 b 포인터는 vtable을 설명하기 위해 고정됩니다. 이 a 또는 b 포인터를 전달하여 삭제하면 주소가 힙의 사용 가능 루틴에 유효하지 않기 때문에 충돌이 발생합니다.
기본 클래스 포인터에서 vtable이있는 파생 인스턴스를 삭제하려는 경우 기본 클래스에 vtable이 있는지 확인해야합니다. 이를 수행하는 한 가지 방법은 가상 소멸자를 추가하는 것입니다. 가상 소멸자는 어쨌든 리소스를 올바르게 정리할 수 있습니다.
기본 정의 virtual
는 클래스의 멤버 함수가 파생 클래스에서 재정의 될 수 있는지 여부를 결정하는 것입니다.
클래스의 D-tor는 기본적으로 범위의 끝에서 호출되지만 예를 들어 힙 (동적 할당)에서 인스턴스를 정의 할 때 문제가 발생하므로 수동으로 삭제해야합니다.
명령어가 실행 되 자마자 기본 클래스 소멸자가 호출되지만 파생 된 클래스 소멸자는 호출되지 않습니다.
실제 예는 컨트롤 필드에서 이펙터, 액추에이터를 조작해야하는 경우입니다.
스코프의 끝에서 전원 요소 (액추에이터) 중 하나의 소멸자가 호출되지 않으면 치명적인 결과가 발생합니다.
#include <iostream>
class Mother{
public:
Mother(){
std::cout<<"Mother Ctor"<<std::endl;
}
virtual~Mother(){
std::cout<<"Mother D-tor"<<std::endl;
}
};
class Child: public Mother{
public:
Child(){
std::cout<<"Child C-tor"<<std::endl;
}
~Child(){
std::cout<<"Child D-tor"<<std::endl;
}
};
int main()
{
Mother *c = new Child();
delete c;
return 0;
}
다형성이든 아니든 공개적으로 상속되는 클래스에는 가상 소멸자가 있어야합니다. 다시 말해, 기본 클래스 포인터로 가리킬 수 있으면 기본 클래스에 가상 소멸자가 있어야합니다.
가상 인 경우 파생 클래스 소멸자가 호출되면 기본 클래스 생성자가 호출됩니다. 가상이 아닌 경우 기본 클래스 소멸자 만 호출됩니다.