构造函数可以调用虚函数吗,析构函数可以调用虚函数吗 ? 存在虚构造函数吗?
-
构造函数跟虚构函数里面都可以调用虚函数,编译器不会报错。
-
C++ primer中说到最好别用
-
由于类的构造次序是由基类到派生类,所以在构造函数中调用虚函数,虚函数是不会呈现出多态的
-
类的析构是从派生类到基类,当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉,所以也不会呈现多态
-
因此如果在基类中声明的纯虚函数并且在基类的析构函数中调用之,编译器会发生错误。
在构造函数中调用虚函数
class Base
{
public:
Base()
{
Fuction();
}
virtual void Fuction()
{
cout << "Base::Fuction" << endl;
}
};
class A : public Base
{
public:
A()
{
Fuction();
}
virtual void Fuction()
{
cout << "A::Fuction" << endl;
}
};
这样定义一个A的对象,会输出什么?
A a;
首先调用应该是没有问题的,但是得到的结果呢?
很多人会说输出:A::Fuction A::Function. 他们会觉得在 先调用父类构造函数时, 是子类对象去调用的Function.
如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样,实际的输出结果是:
Base::Fuction
A::Fuction
在析构函数中调用虚函数.
本意是想实现 对应的 对象调用虚函数, 能实现对应的析构函数(调用虚函数的虚析构)
#include <iostream>using namespace std;
class A
{
public:
A()
{
cout << "A构造函数";
Test();
}
~A()
{
cout << "A析构函数";
cout << "A::Test()" << endl;
}
virtual void Test()
{
cout << "A::Test()" << endl;
}
};
class B:public A
{
public:
B()
{
cout << "B构造函数";
Test();
}
~B()
{
cout << "B析构函数";
Test();
}
virtual void Test()
{
cout << "B::Test()" << endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pA = new B(); 调用构造函数 输出A构造函数:A::Test() B调用构造函数B::Test()
cout << "动态调用:";
pA->Test(); 原指针类型是PA,实际指针类型是B,由于是虚函数,所以按实际类型调用 B::Test()
delete pA; 由于A的析构不是虚函数,所以按照原类型指针调用,如果在A的析构函数中加上virtual 则输出为B析构函数B::Test A析构函数A::Test
return 0;
}
虚函数根据指针实际类型调用,其他函数都是根据原类型调用。
#include<iostream>
using namespace std;
class A
{
public:
void virtual f()
{
cout<<"f() A"<<endl;
}
void func()
{
cout<<"func() A"<<endl;
}
A()
{
cout<<"A()"<<endl;
}
virtual ~A()
{
cout<<"~A()"<<endl;
}
};
class B : public A
{
public:
void virtual f()
{
cout<<"B"<<endl;
}
void func()
{
cout<<"func() B"<<endl;
}
B()
{
cout<<"B()"<<endl;
}
virtual ~B()
{
cout<<"~B()"<<endl;
}
};
int main ()
{
A* pa=new A();
pa->f(); //这个很明显A
pa->func();
B* pb=(B*)pa;
pb->f(); //这个强制将pa复制到pb,所以pb实际指向A
pb->func();
/*输出结果 虚函数根据指针实际类型调用,其他函数都是根据原类型调用。没有调用B的构造函数,却调用了B类中的对象
A()
f() A
func() A
f() A
func() B
*/
delete pa;
delete pb;
/*
~A()
只会调用A的析构函数// 因为B都没构造
*/
return 0;
}
补充:
构造函数不能为虚函数,而析构函数可以且常常是虚函数。
这就要涉及到C++对象的构造问题了,C++对象在三个地方构建:
(1)函数堆栈;(2)自由存储区,或称之为堆;
(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
这个就好理解了,因为此时 vtable 已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。
为什么构造函数不能声明为虚函数,析构函数可以
构造函数不能声明为虚函数,析构函数可以声明为虚函数,而且有时是必须声明为虚函数。不建议在构造函数和析构函数里面调用虚函数。
构造函数不能声明为虚函数的原因是:
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。
2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
2 构造函数不能声明为虚函数的原因主要有以下几点:
构造函数不能声明为虚函数的原因主要有以下几点:
- 初始化顺序:
- 构造函数的目的是初始化对象,而继承关系中的类需要按照从基类到派生类的顺序进行构造。如果构造函数是虚函数,那么就无法保证这种构造顺序。
在C++中,构造函数的目的是初始化对象。当一个类继承自另一个类时,派生类的对象包含了基类的所有数据成员和成员函数。因此,在创建派生类对象时,必须先调用基类的构造函数来初始化基类部分,然后才能调用派生类的构造函数来初始化派生类特有的部分。
如果构造函数可以声明为虚函数,那么编译器将无法保证这种从基类到派生类的构造顺序。因为虚函数调用是动态绑定的,这意味着它是在运行时根据对象的实际类型来决定调用哪个函数的。然而,对于构造函数来说,我们必须确保按照正确的顺序进行初始化,这与虚函数的运行时绑定机制相冲突。
具体来说,假设我们有一个如下的类层次:
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor" << std::endl; }
};
当我们创建一个Derived
对象时,期望的构造顺序应该是:
- 调用
Base
的构造函数来初始化基类部分。 - 调用
Derived
的构造函数来初始化派生类特有的部分。
但是,如果构造函数可以声明为虚函数,那么这个顺序就可能被打乱。例如,如果我们有一个指向Base
类型的指针,并试图通过它来创建一个Derived
对象:
Base* base = new Derived();
在这种情况下,由于构造函数是虚函数,编译器可能会尝试首先调用Derived
的构造函数,然后再调用Base
的构造函数。这样就破坏了从基类到派生类的构造顺序,导致未初始化的基类部分被使用,进而引发各种难以预料的问题。
因此,为了避免这样的问题,C++不允许构造函数成为虚函数。
- 虚函数表(vtable)访问:
- 虚函数的调用依赖于一个指向虚函数表(vtable)的指针(vptr)。这个指针是在对象构造过程中设置的,但在构造函数执行之前,对象尚未完全构造,因此还没有可供使用的vptr。
在C++中,虚函数的调用依赖于一个指向虚函数表(vtable)的指针(vptr)。这个指针是在对象构造过程中设置的,但在构造函数执行之前,对象尚未完全构造,因此还没有可供使用的vptr。
也就是说,在创建一个对象时,编译器会在对象内存布局的开始部分添加一个隐藏的指针(vptr),这个指针指向对应的虚函数表。然而,由于对象尚未完全构造,此时的vptr实际上并没有被初始化,也就无法用来访问虚函数表。
这是因为虚函数表的初始化过程是发生在构造函数执行之后的。只有当构造函数执行完毕,对象的所有成员变量和基类部分都已经被正确地初始化后,vptr才会被设置为指向正确的虚函数表。
因此,虽然vptr在对象构造过程中就已经存在,但是在构造函数执行之前,它还不能被用来访问虚函数表。
-
构造函数的目的:
- 虚函数的主要目的是支持多态性,允许通过基类指针或引用调用子类的实现。然而,构造函数的目的是初始化对象,其功能与多态性并不相符。
-
内存布局:
- 如果构造函数是虚函数,它将尝试在对象尚未被分配内存的情况下访问虚函数表。由于对象还没有实例化,也就没有存储空间来存放vptr。
-
避免意外覆盖:
- 允许构造函数成为虚函数可能会导致子类无意中重写父类的构造函数,这可能导致错误的行为,因为父类的构造可能未完成。
这句话是基于假设构造函数可以是虚函数的情况,实际上这是不可能的。在C++中,构造函数不能被声明为虚函数。
然而,如果我们假设构造函数可以是虚函数,那么可能会发生以下情况:
-
子类无意中重写父类的构造函数:
- 如果允许构造函数成为虚函数,那么子类就可以像其他虚函数一样覆盖父类的构造函数。这可能会导致意外的行为,因为子类可能不知道它正在覆盖父类的构造函数。
-
父类的构造可能未完成:
- 构造函数的主要目的是初始化对象,包括它的数据成员和基类部分。如果子类的构造函数覆盖了父类的构造函数,那么父类的部分可能没有得到正确的初始化。
-
继承关系中的构造顺序无法保证:
- 在多态性的情况下,虚函数调用是在运行时根据对象的实际类型来决定的。但是,在构造过程中,必须按照从基类到派生类的顺序进行。如果构造函数可以是虚函数,那么这个顺序就无法保证。
这些原因共同决定了构造函数不应该、也不允许被声明为虚函数。
- 使用场景不匹配:
- 在创建对象时,我们总是知道要创建的具体类型,而虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。对于构造函数来说,我们不需要这样的动态绑定,因为创建过程应该是明确且固定的。
这些原因共同决定了构造函数不应该、也不允许被声明为虚函数。
虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。
析构函数设为虚函数的作用:
解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。