我试图熟悉OOP概念,但却不太了解virtual
的概念。
virtual destructor
但不能创建virtual constructor
。为什么?virtual destructors
如何在内部处理?我的意思是链接Virtual Destructors说明了这个概念,但我的问题是如何调用vptr
s(派生和基础)的vtable
? (对于虚拟成员函数,当这种情况发生时,通常只会调用派生类所指向的vptr
函数)virtual destructor
?任何人都可以通过链接/示例帮助我理解上述概念吗?
答案 0 :(得分:7)
首先,关于虚拟功能和非虚拟功能之间的区别:
在编译或链接过程中,可以解析代码中的每个非虚函数调用。
通过已解决,我们的意思是函数的地址可以由编译器或链接器计算。
因此,在创建的目标代码中,函数调用可以替换为操作码,以便跳转到内存中该函数的地址。
使用虚函数,您可以调用只能在运行时解析的函数。
不要解释它,让我们来看一个简单的场景:
class Animal
{
virtual void Eat(int amount) = 0;
};
class Lion : public Animal
{
virtual void Eat(int amount) { ... }
};
class Tiger : public Animal
{
virtual void Eat(int amount) { ... }
};
class Tigon : public Animal
{
virtual void Eat(int amount) { ... }
};
class Liger : public Animal
{
virtual void Eat(int amount) { ... }
};
void Safari(Animal* animals[], int numOfAnimals, int amount)
{
for (int i=0; i<numOfAnimals; i++)
animals[i]->Eat(amount);
// A different function may execute at each iteration
}
正如您可能理解的那样,Safari
功能可以让您灵活地喂养不同的动物。
但由于每个动物的确切类型直到运行时才知道,因此要调用的确切Eat
函数也是如此。
类的构造函数不能是虚拟的,因为:
通过对象类的V-Table执行对象的虚函数调用。
每个对象都有一个指向其类的V-Table的指针,但只有在创建对象时才会在运行时初始化此指针。
换句话说,只有在调用构造函数时才会初始化此指针,因此构造函数本身不能是虚拟的。
除此之外,构造函数首先不是虚拟的。
虚函数背后的想法是你可以在不知道调用它们的对象的确切类型的情况下调用它们。
当您创建一个对象时(即,当您隐式调用构造函数时),您确切地知道要创建的对象类型,因此您不需要此机制。
基类的析构函数必须是虚拟的,因为:
当你静态地分配一个类继承自基类的对象时,那么在函数的末尾(如果对象是本地的)或程序(如果对象是全局的),类的析构函数是自动的调用,然后调用基类的析构函数。
在这种情况下,析构函数是虚拟的,没有任何意义。
另一方面,当您动态分配(new
)一个类继承自基类的对象时,您需要在稍后的某个位置动态释放(delete
)它。执行该计划。
delete
运算符获取指向对象的指针,其中指针的类型可能是基类本身。
在这种情况下,如果析构函数是虚拟的,那么delete
运算符将调用类的析构函数,而析构函数又调用基类的析构函数。
但是如果析构函数不是虚拟的,那么delete
运算符将调用基类的析构函数,并且永远不会调用实际类的析构函数。
考虑以下示例:
class A
{
A() {...}
~A() {...}
};
class B: public A
{
B() {...}
~B() {...}
};
void func()
{
A* b = new B(); // must invoke the destructor of class 'B' at some later point
...
delete b; // the destructor of class 'B' is never invoked
}
答案 1 :(得分:3)
可以创建虚拟析构函数,但不能创建虚拟构造函数。为什么呢?
根据要调用的对象的类型调度虚函数。当调用构造函数时,没有对象 - 它是构造函数创建一个对象的工作。没有对象,就不可能进行虚拟调度,因此构造函数不能是虚拟的。
虚拟析构函数如何在内部处理?
虚拟调度的内部细节是实现定义的;语言没有指定实现,只指定行为。通常,析构函数通过 vtable 调用,就像任何虚函数一样。
如何调用vtable(Derived和Base)的vptr?
虚拟地只调用派生最多的析构函数。所有析构函数(无论是否为虚函数)都将隐式调用所有成员和直接基类子对象的析构函数。 (在存在虚拟继承的情况下,情况稍微复杂一些;但这超出了本问题的范围。)
是否还有其他可能需要使用虚拟析构函数的场景?
您需要一个以支持多态删除;也就是说,能够通过指向基类型的指针删除派生类型的对象。如果没有基类型的虚拟析构函数,则不允许这样做,并且会给出未定义的行为。
答案 2 :(得分:2)
因为在运行阶段调用了虚函数,但是在初始化阶段调用构造函数,而不构造对象。所以拥有一个虚拟构造函数是没有意义的。
一个。在链接中只调用基类析构函数的原因,析构函数未标记为虚拟,因此析构函数地址在编译/链接时链接到Base类析构函数,显然指针的类型是Base而不是Derived在编译时。
湾为什么在将虚拟添加到Base desctructor之后调用Base和Derived构造函数。它的行为如下: 衍生d; //当d退出生命周期时,将调用Derived和Base的析构函数。
假设您至少拥有一个虚拟函数,则应该有一个虚拟析构函数。
答案 3 :(得分:2)
可以创建虚拟析构函数,但不能创建虚拟构造函数。 为什么呢?
我会用外行的话来解释这个问题。 c ++中的类只有在构造函数完成后才存在。在初始化派生类及其成员(包括vtable链接)之前,每个基类都存在。因此,拥有一个虚拟构造函数是没有意义的(因为构造,你需要知道类型)。此外(在c ++中),从构造函数调用虚函数不起作用(因为派生类的vtable部分尚未设置)。如果仔细考虑它,允许从构造函数调用虚函数会打开一堆蠕虫(比如在成员初始化之前调用派生类的虚函数)。
就析构函数而言,在破坏点,vtable是“完整的”,我们(c ++运行时)完全了解类型(可以这么说)。找到该类型派生程度最高的部分的析构函数(如果是虚拟的,通过vtable),因此可以调用析构函数,并且可以自然地调用所有基础的析构函数。
虚拟析构函数如何在内部处理?我的意思是链接 Virtual Destructors说明了这个概念,但我的问题是如何 vtable(Derived和Base)的vptr被称为?
析构函数的处理方式与普通的虚函数相同(也就是说,如果它们是虚拟的,则会在vtable中查找地址,代价是一个(可能是2?)额外的间接级别)。此外,c ++保证在完成派生的析构函数之后,所有基本析构函数都必须执行(以相反的构造顺序依赖于声明的顺序)。
可以通过使用原型模式(或克隆)等模式或使用工厂来模仿/模拟虚拟构造。在这种情况下,要么存在真实类型的实例(要以多态方式使用),要么存在工厂(从抽象工厂派生),它会根据提供的某些知识创建一个类型(通过虚函数)。
希望这有帮助。
答案 4 :(得分:0)
我假设我们有一个基类A,它是派生的B。
1:您可以通过A指针删除B,然后正确的方法是调用B析构函数。 但是,您只是不能说,在您实际调用A构造函数时应该创建一个B对象。没有这种情况。 你可以说:
A* a = new B ();
或
B b;
但两者都直接调用了B的构造函数。
2:嗯,我不完全确定,但我想它将遍历类层次结构的相关部分,并搜索函数的最近调用。如果某个函数不是虚函数,它会停止迭代并调用它。
3:如果要从该类继承某些内容,则应始终使用虚拟析构函数。如果它是最后一堂课,你不应该。
答案 5 :(得分:0)
我浪费了几天时间试图发现为什么在发现答案之前我的派生虚拟析构函数没有被调用,所以希望我可以通过这个回复来挽救其他许多悲伤。
我开始在项目中使用三级和四级的派生类。虚函数似乎工作正常但后来我发现我有大量的内存泄漏,因为我的析构函数没有被调用。没有编译器或运行时错误 - 没有调用析构函数。
网上有大量的文档和示例,但没有一个是有用的,因为我的语法是正确的。
我决定如果编译器没有调用我的析构函数,我需要创建自己的虚拟析构方法来调用。然后我得到了解决问题的编译器错误 - &#34;如果是前向参考&#34;则。在基类中为派生类头文件添加include解决了这个问题。编译器需要类定义来调用析构函数!
我建议在创建新的派生类时,在基类和中间类中包含头文件。将条件调试代码添加到析构函数中以检查它们是否被调用可能也是一个好主意。
鲍勃·赖斯