为什么 C++ 没有虚拟构造函数?
23 回答
从马的嘴里听出来。:)
来自 Bjarne Stroustrup 的 C++ 风格和技术常见问题解答为什么我们没有虚拟构造函数?
虚拟调用是一种在给定部分信息的情况下完成工作的机制。特别是,“虚拟”允许我们调用只知道任何接口而不知道对象的确切类型的函数。要创建一个对象,您需要完整的信息。特别是,您需要知道要创建的确切类型。因此,“对构造函数的调用”不能是虚拟的。
FAQ 条目继续提供代码,用于在没有虚拟构造函数的情况下实现此目的。
虚函数基本上提供多态行为。也就是说,当您使用动态类型与引用它的静态(编译时)类型不同的对象时,它会提供适合对象实际类型而不是对象静态类型的行为。
现在尝试将这种行为应用于构造函数。构造对象时,静态类型始终与实际对象类型相同,因为:
要构造一个对象,构造函数需要它要创建的对象的确切类型[...]此外[...]你不能有一个指向构造函数的指针
(Bjarne Stroustup(P424 C++ 编程语言 SE))
与 Smalltalk 或 Python 等面向对象的语言不同,其中构造函数是表示类的对象的虚拟方法(这意味着您不需要 GoF抽象工厂模式,因为您可以传递表示类的对象而不是制作您自己的),C++ 是一种基于类的语言,并且没有代表任何语言结构的对象。该类在运行时不作为对象存在,因此您不能在其上调用虚拟方法。
这符合“不用为不用的东西付费”的理念,尽管我见过的每个大型 C++ 项目最终都实现了某种形式的抽象工厂或反射。
我能想到的两个原因:
技术原因
对象只有在构造函数结束后才存在。为了使用虚拟表调度构造函数,必须存在一个带有指向该虚拟表的指针的现有对象,但如果该对象存在指向该虚拟表的指针怎么办?还不存在?:)
逻辑原因
当您想要声明某种多态行为时,您可以使用 virtual 关键字。但是构造函数没有任何多态性,C++ 中构造函数的工作就是简单地将对象数据放在内存中。由于虚拟表(以及一般的多态性)都是关于多态行为而不是多态数据,因此声明虚拟构造函数没有任何意义。
总结:C++ 标准可以为“虚拟构造函数”指定一个符号和行为,它相当直观,编译器支持也不太难,但是当功能已经可以使用create()
/clone()
(参见以下)?它不像许多其他正在酝酿中的语言提案那样有用。
讨论
让我们假设一个“虚拟构造函数”机制:
Base* p = new Derived(...);
Base* p2 = new p->Base(); // possible syntax???
上面第一行构造了一个Derived
对象,所以*p
's virtual dispatch table 可以合理地提供一个“虚拟构造函数”供第二行使用。(此页面上的数十个答案指出“该对象尚不存在,因此不可能进行虚拟构造”,这些答案不必要地短视了要构造的对象。)
第二行假设符号new p->Base()
来请求动态分配和另一个Derived
对象的默认构造。
笔记:
编译器必须在调用构造函数之前协调内存分配- 构造函数通常支持自动(非正式“堆栈”)分配、静态(用于全局/命名空间范围和类/函数
static
对象)和动态(非正式“堆”)何时new
使用p->Base()
在编译时通常无法知道要构造的对象的大小,因此动态分配是唯一有意义的方法- 可以在堆栈上分配运行时指定的内存量 - 例如GCC 的可变长度数组扩展-
alloca()
但会导致显着的低效率和复杂性(例如,分别为此处和此处)
- 可以在堆栈上分配运行时指定的内存量 - 例如GCC 的可变长度数组扩展-
对于动态分配,它必须返回一个指针,以便
delete
稍后可以存储内存。假定的表示法明确列出
new
以强调动态分配和指针结果类型。
编译器需要:
Derived
通过调用隐式virtual
sizeof
函数或通过 RTTI 获取此类信息,找出需要多少内存- 调用
operator new(size_t)
分配内存 Derived()
使用位置调用new
。
或者
- 为结合动态分配和构造的函数创建额外的 vtable 条目
所以 - 指定和实现虚拟构造函数似乎不是不可克服的,但百万美元的问题是:它如何比使用现有 C++ 语言特性更好......?就个人而言,我认为下面的解决方案没有任何好处。
`clone()` 和 `create()`
C++ FAQ 记录了一个“虚拟构造函数”习语,包含默认构造或复制构造一个新的动态分配对象的方法virtual
create()
:clone()
class Shape {
public:
virtual ~Shape() { } // A virtual destructor
virtual void draw() = 0; // A pure virtual function
virtual void move() = 0;
// ...
virtual Shape* clone() const = 0; // Uses the copy constructor
virtual Shape* create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
public:
Circle* clone() const; // Covariant Return Types; see below
Circle* create() const; // Covariant Return Types; see below
// ...
};
Circle* Circle::clone() const { return new Circle(*this); }
Circle* Circle::create() const { return new Circle(); }
也可以更改或重载create()
以接受参数,但要匹配基类/接口的virtual
函数签名,覆盖的参数必须与基类重载之一完全匹配。有了这些明确的用户提供的设施,添加日志记录、检测、更改内存分配等很容易。
撇开语义原因不谈,直到对象被构造之后才存在 vtable,因此虚拟指定毫无用处。
我们这样做,它只是不是构造函数:-)
struct A {
virtual ~A() {}
virtual A * Clone() { return new A; }
};
struct B : public A {
virtual A * Clone() { return new B; }
};
int main() {
A * a1 = new B;
A * a2 = a1->Clone(); // virtual construction
delete a2;
delete a1;
}
C++ 中的虚函数是运行时多态性的一种实现,它们会进行函数覆盖。通常,virtual
当您需要动态行为时,在 C++ 中使用关键字。它仅在对象存在时才起作用。而构造函数用于创建对象。构造函数将在对象创建时被调用。
因此,如果您将构造函数创建为virtual
,根据 virtual 关键字定义,它应该有可用的对象,但构造函数用于创建对象,因此这种情况永远不会存在。所以你不应该将构造函数用作虚拟的。
因此,如果我们尝试声明虚拟构造函数,编译器会抛出错误:
构造函数不能被声明为虚拟的
您可以在@stefan 的回答中找到一个示例以及为什么不允许这样做的技术原因。根据我的说法,现在对这个问题的合乎逻辑的答案是:
virtual 关键字的主要用途是在我们不知道基类指针将指向什么类型的对象时启用多态行为。
但是想想这是更原始的方式,为了使用虚拟功能,你需要一个指针。指针需要什么?一个指向的对象!(考虑正确执行程序的情况)
所以,我们基本上需要一个已经存在于内存某处的对象(我们不关心内存是如何分配的,它可能是在编译时或运行时),以便我们的指针可以正确指向该对象。
现在,考虑一下当要指向的类的对象被分配一些内存的那一刻的情况 -> 它的构造函数将在该实例本身被自动调用!
所以我们可以看到我们实际上不需要担心构造函数是虚拟的,因为在任何情况下,您希望使用多态行为,我们的构造函数将已经执行,使我们的对象准备好使用!
使用虚函数是为了根据指针指向的对象类型调用函数,而不是指针本身的类型。但是没有“调用”构造函数。它仅在声明对象时调用一次。因此,构造函数不能在 C++ 中变为虚拟的。
当人们问这样的问题时,我喜欢想自己“如果这真的可能会发生什么?” 我真的不知道这意味着什么,但我想这与能够基于正在创建的对象的动态类型覆盖构造函数实现有关。
我看到了一些潜在的问题。一方面,在调用虚拟构造函数时,派生类不会完全构建,因此实现存在潜在问题。
其次,在多重继承的情况下会发生什么?您的虚拟构造函数可能会被多次调用,然后您需要通过某种方式知道正在调用哪个构造函数。
第三,一般来说,在构造时,对象并没有完全构造虚拟表,这意味着需要对语言规范进行较大的更改以允许在构造时知道对象的动态类型这一事实时间。然后,这将允许基类构造函数在构造时调用其他虚函数,并使用未完全构造的动态类类型。
最后,正如其他人指出的那样,您可以使用静态“create”或“init”类型的函数来实现一种虚拟构造函数,这些函数基本上与虚拟构造函数做同样的事情。
尽管由于对象类型是创建对象的先决条件,因此虚拟构造函数的概念不太适合,但它并没有完全被否决。
GOF 的“工厂方法”设计模式利用了虚拟构造函数的“概念”,这在某些设计情况下很方便。
面试答案是:虚拟ptr和表与对象相关,但与类无关。因此构造函数构建虚拟表,因此我们不能有虚拟构造函数,因为在创建对象之前没有Vtable。
您也不应该在构造函数中调用虚函数。见:http ://www.artima.com/cppsource/nevercall.html
此外,我不确定您是否真的需要一个虚拟构造函数。没有它你可以实现多态构造:你可以编写一个函数来根据需要的参数构造你的对象。
为每个具有一个或多个“虚拟功能”的类制作一个虚拟表(vtable)。每当创建此类类的对象时,它都包含一个“虚拟指针”,该指针指向相应 vtable 的基础。每当有虚函数调用时,vtable 用于解析到函数地址。构造函数不能是虚拟的,因为当一个类的构造函数被执行时,内存中没有vtable,意味着还没有定义虚拟指针。因此,构造函数应该始终是非虚拟的。
我们不能简单地说......我们不能继承构造函数。所以没有必要将它们声明为虚拟,因为虚拟提供了多态性。
C++ 虚拟构造函数是不可能的。例如,您不能将构造函数标记为虚拟。试试这个代码
#include<iostream.h>
using namespace std;
class aClass
{
public:
virtual aClass()
{
}
};
int main()
{
aClass a;
}
它会导致错误。此代码试图将构造函数声明为虚拟的。现在让我们试着理解为什么我们使用 virtual 关键字。虚拟关键字用于提供运行时多态性。例如试试这个代码。
#include<iostream.h>
using namespace std;
class aClass
{
public:
aClass()
{
cout<<"aClass contructor\n";
}
~aClass()
{
cout<<"aClass destructor\n";
}
};
class anotherClass:public aClass
{
public:
anotherClass()
{
cout<<"anotherClass Constructor\n";
}
~anotherClass()
{
cout<<"anotherClass destructor\n";
}
};
int main()
{
aClass* a;
a=new anotherClass;
delete a;
getchar();
}
In main为声明为 . 类型的指针a=new anotherClass;
分配内存。这会导致构造函数(In和)都自动调用。因此我们不需要将构造函数标记为虚拟。因为创建对象时,它必须遵循创建(即首先是基类,然后是派生类)。但是当我们尝试删除一个时,它会导致只调用基本的析构函数。所以我们必须使用 virtual 关键字来处理析构函数。所以虚拟构造函数是不可能的,但虚拟析构函数是。谢谢anotherClass
a
aClass
aClass
anotherClass
delete a;
虚拟机制仅在您具有指向派生类对象的基类指针时才起作用。构造函数对于基类构造函数的调用有自己的规则,基本上是基类到派生。虚拟构造函数如何有用或被调用?我不知道其他语言做什么,但我看不出虚拟构造函数如何有用甚至实现。为了使虚拟机制有意义,必须进行构造,并且还需要进行构造以创建提供多态行为机制的 vtable 结构。
有一个非常基本的原因:构造函数实际上是静态函数,而在 C++ 中,没有静态函数可以是虚拟的。
如果你对 C++ 有丰富的经验,你就会知道静态函数和成员函数之间的区别。静态函数与 CLASS 相关联,而不是对象(实例),因此它们看不到“this”指针。只有成员函数可以是虚拟的,因为 vtable(使“虚拟”工作的函数指针的隐藏表)实际上是每个对象的数据成员。
现在,构造函数的工作是什么?它的名字是——一个“T”构造函数在分配 T 对象时初始化它们。这会自动排除它成为成员函数!一个对象必须在它有一个“this”指针和一个 vtable 之前存在。这意味着即使该语言将构造函数视为普通函数(由于相关原因我不会讨论),它们也必须是静态成员函数。
了解这一点的一个好方法是查看“工厂”模式,尤其是工厂函数。他们做你想做的事,你会注意到如果类 T 有一个工厂方法,它总是静态的。它一定要是。
“构造函数不能是虚拟的”
- 有一些正当的理由证明这种说法是正确的。
- 要创建对象,对象类的构造函数必须与类的类型相同。但是,这对于虚拟实现的构造函数是不可能的。
- 在调用构造函数时,不会创建虚拟表来解析任何虚拟函数调用。因此,虚拟构造函数本身将无处可查。
因此,不可能将构造函数声明为虚拟的。
当一个构造函数被调用时,虽然到那时还没有创建对象,但我们仍然知道将要创建的对象的种类,因为该对象所属的类的特定构造函数已经被调用了。
Virtual
与函数关联的关键字意味着将调用特定对象类型的函数。所以,我的想法是没有必要创建虚拟构造函数,因为已经调用了将要创建其对象的所需构造函数,并且使构造函数虚拟只是一件多余的事情,因为已经调用了特定于对象的构造函数这与调用通过 virtual 关键字实现的特定于类的函数相同。
尽管出于 vptr 和 vtable 相关的原因,内部实现不允许使用虚拟构造函数。
另一个原因是 C++ 是一种静态类型语言,我们需要在编译时知道变量的类型。
编译器必须知道创建对象的类类型。要创建的对象类型是编译时决定的。
如果我们将构造函数设为虚拟,则意味着我们不需要在编译时知道对象的类型(这就是虚函数提供的。我们不需要知道实际的对象,只需要指向的基指针指向一个实际对象调用被指向对象的虚函数而不知道对象的类型)如果我们在编译时不知道对象的类型,那么它与静态类型语言相反。因此,无法实现运行时多态性。
因此,在编译时不知道对象类型的情况下不会调用 Constructor。所以制作虚拟构造函数的想法失败了。
如果您从逻辑上思考构造函数的工作原理以及虚函数在 C++ 中的含义/用法,那么您将意识到虚构造函数在 C++ 中毫无意义。在 C++ 中声明一些虚拟的东西意味着它可以被当前类的子类覆盖,但是在创建对象时调用构造函数,那时你不能创建该类的子类,你必须是创建类,因此永远不需要声明一个虚拟构造函数。
另一个原因是,构造函数与其类名相同,如果我们将构造函数声明为虚拟,那么它应该在其派生类中以相同的名称重新定义,但是两个类不能具有相同的名称。所以不可能有一个虚拟构造函数。
Vpointer 在对象创建时创建。vpointer 在对象创建之前不会存在。所以没有必要将构造函数设为虚拟。