为什么在c++中没有虚构造函数?

为什么c++没有虚构造函数?

266372 次浏览

你也不应该在构造函数中调用虚函数。参见:http://www.artima.com/cppsource/nevercall.html

另外,我不确定是否真的需要虚拟构造函数。你可以在没有它的情况下实现多态构造:你可以编写一个函数,根据所需的参数构造你的对象。

我们有,只是它不是一个构造函数:-)

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;
}

虚函数基本上提供多态行为。也就是说,当你处理一个动态类型不同于它所引用的静态(编译时)类型的对象时,它提供的行为适合对象的实际类型,而不是对象的静态类型。

现在尝试将这种行为应用于构造函数。当你构造一个对象时,静态类型总是与实际的对象类型相同,因为:

要构造一个对象,构造函数需要它要创建的对象的确切类型[…]此外[…]]则不能有指向构造函数的指针

Bjarne Stroustup (P424 c++编程语言SE)

听可靠消息。:)

来自Bjarne Stroustrup的c++风格和技术常见问题为什么我们没有虚拟构造函数?

虚拟调用是一种机制,在给定部分的情况下完成工作 信息。特别地,“virtual”允许我们调用一个函数 只知道任何接口,而不知道对象的确切类型。来 创建对象时需要完整的信息。特别是,你 需要知道你想要创建的确切类型。因此, “对构造函数的调用”不能是虚的

FAQ条目继续给出了一种不使用虚拟构造函数实现此目的的方法的代码。

当人们问这样的问题时,我喜欢对自己说:“如果这真的是可能的,会发生什么?”我真的不知道这意味着什么,但我猜这可能与能够基于所创建对象的动态类型重写构造函数实现有关。

我看到了一些潜在的问题。首先,在调用虚构造函数时,派生类不会被完全构造,因此实现中存在潜在的问题。

其次,在多重继承的情况下会发生什么?你的虚构造函数可能会被多次调用,然后你需要有某种方法知道哪个被调用了。

第三,一般来说,在构造时,对象并没有完全构造虚拟表,这意味着需要对语言规范进行很大的更改,以允许在构造时就知道对象的动态类型。这将允许基类构造函数在构造时调用其他虚函数,使用未完全构造的动态类类型。

最后,正如其他人指出的那样,您可以使用静态的“create”或“init”类型函数实现一种虚拟构造函数,基本上与虚拟构造函数所做的事情相同。

与面向对象的语言(如Smalltalk或Python)不同,这些语言的构造函数是表示类的对象的虚拟方法(这意味着你不需要GoF 抽象工厂模式,因为你可以传递表示类的对象,而不是自己创建),c++是一种基于类的语言,并且没有表示语言的任何构造的对象。该类在运行时不作为对象存在,因此不能对其调用虚方法。

这符合“不用就不用付钱”的理念,尽管我所见过的每个大型c++项目最终都实现了某种形式的抽象工厂或反射。

我能想到两个原因:

技术原因

对象只有在构造函数结束后才存在。为了使用虚拟表分派构造函数,必须有一个现有的对象和指向虚拟表的指针,但是如果对象仍然不存在,指向虚拟表的指针怎么可能存在呢?:)

逻辑的原因

当您想要声明某种多态行为时,可以使用virtual关键字。但是构造函数没有任何多态性,c++中的构造函数只是简单地将对象数据放到内存中。由于虚表(以及一般的多态性)都是关于多态行为而不是多态数据的,因此声明虚构造函数没有任何意义。

抛开语义上的原因不谈,在对象被构造之后才有虚表,因此虚表的指定是无用的。

虚机制仅在具有指向派生类对象的基类指针时才有效。构造有自己的调用基类构造函数的规则,基本上是派生的基类。虚拟构造函数如何有用或被调用?我不知道其他语言做什么,但我不知道虚拟构造函数如何有用,甚至如何实现。构建需要发生,虚拟机制有任何意义,构建也需要发生,虚表结构已经创建,提供了多态行为的机制。

尽管由于对象类型是对象创建的先决条件,所以虚拟构造函数的概念不太适合,但它并没有完全被推翻。

GOF的“工厂方法”设计模式利用了虚拟构造函数的“概念”,这在某些设计情况下很方便。

c++的虚拟构造函数是不可能的。例如,不能将构造函数标记为虚函数。试试这段代码

#include<iostream.h>
using namespace std;
class aClass
{
public:
virtual aClass()
{
}
};
int main()
{
aClass a;
}

将导致错误。这段代码试图将构造函数声明为虚函数。 现在让我们试着理解为什么要使用virtual关键字。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();
}
在main中a=new anotherClass;在声明为aClass类型的指针a中为anotherClass分配内存。这将导致构造函数(在aClassanotherClass中)自动调用。因此,我们不需要将构造函数标记为虚函数。因为当创建一个对象时,它必须遵循创建链(即首先是基类,然后是派生类)。 但是当我们试图删除delete a;时,它会导致只调用基析构函数。所以我们必须使用virtual关键字来处理析构函数。所以虚构造函数是不可能的,但虚析构函数是可以的。由于< / p >

我们不能简单地说…我们不能继承构造函数。因此没有必要将它们声明为虚拟的,因为虚拟提供了多态性。

虚函数用于根据指针所指向的对象类型调用函数,而不是指针本身的类型。但是构造函数不会被“调用”。它只在声明对象时调用一次。因此,在c++中构造函数不能被设为虚函数。

c++中的虚函数是运行时多态性的一种实现,它们将执行函数覆盖。在c++中,当你需要动态行为时,通常使用virtual关键字。只有当对象存在时,它才会工作。而构造函数用于创建对象。构造函数将在对象创建时调用。

因此,如果你创建构造函数为virtual,根据virtual关键字定义,它应该有现有的对象可以使用,但构造函数是用来创建对象的,所以这种情况将永远不存在。所以你不应该将构造函数作为虚函数使用。

因此,如果我们试图声明虚拟构造函数编译器抛出一个错误:

构造函数不能声明为虚函数

有一个非常基本的原因:构造函数实际上是静态函数,而在c++中没有静态函数可以是虚函数。

如果你有丰富的c++经验,你就会知道静态和静态之间的区别。成员函数。静态函数与CLASS相关联,而不是与对象(实例)相关联,因此它们看不到“this”指针。只有成员函数可以是虚函数,因为虚表——使“虚”工作的函数指针的隐藏表——实际上是每个对象的数据成员。

构造函数的任务是什么?它在名称中——“T”构造函数在分配T对象时初始化它们。这将自动排除它成为成员函数!对象必须先存在,然后才有this指针,从而有虚表。这意味着即使语言将构造函数视为普通函数(它并没有,由于相关原因,我不会深入讨论),它们也必须是静态成员函数。

了解这一点的一个好方法是查看“Factory”模式,特别是工厂函数。它们做你想做的事情,你会注意到,如果类T有一个工厂方法,它是ALWAYS STATIC。这是必须的。

在创建对象时创建Vpointer。创建对象前Vpointer不存在。因此,没有必要将构造函数设为虚函数。

< em > < / em >总结: c++标准可以为“虚拟构造函数”指定了一种表示法和行为,这是相当直观的,对编译器来说并不太难支持,但是当功能已经可以使用create() / clone()(见下文)清晰地实现时,为什么要为此做出标准更改呢?它远不如正在酝酿中的许多其他语言提案那么有用。

讨论

让我们假设一个“虚拟构造函数”机制:

Base* p = new Derived(...);
Base* p2 = new p->Base();  // possible syntax???

在上面的第一行中,构造了一个Derived对象,因此*p的虚拟调度表可以合理地提供一个“虚拟构造函数”供第二行使用。(本页上数十个陈述"物体还不存在所以虚拟构造是不可能的"的答案不必要地短视地关注于要构造的对象。)

第二行假设符号new p->Base()请求另一个Derived对象的动态分配和默认构造。

注:

  • 编译器必须在调用构造函数之前编排内存分配 -构造函数通常支持自动(非正式的“堆栈”)分配,静态(用于全局/命名空间范围和类/函数- __abc0对象),当使用new时,动态(非正式的“堆”)

    • p->Base()要构造的对象的大小通常在编译时无法知道,因此动态分配是唯一有意义的方法 . bb0

      • 可以在堆栈上分配运行时指定数量的内存量——例如GCC的变长数组扩展alloca()——但会导致显著的低效率和复杂性(例如分别为在这里在这里)。
      • 李< / ul > < / > 李< / ul > < / >
      • 对于动态分配,它必须返回一个指针,以便稍后可以deleted内存。

      • 假设表示法显式列出new以强调动态分配和指针结果类型。

      编译器需要:

      • 通过调用隐式virtual sizeof函数或通过RTTI获取这些信息,可以找出Derived需要多少内存
      • 调用operator new(size_t)来分配内存
      • 使用放置new调用Derived()

      • 为结合了动态分配和构造的函数创建额外的虚表项

      因此,指定和实现虚拟构造函数似乎并不是不可克服的,但最重要的问题是:它如何比使用现有的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)是为每个具有一个或多个“虚函数”的类创建的。每当创建此类的Object时,它包含一个指向相应虚表基的“虚指针”。只要有虚函数调用,虚表就用于解析函数地址。 构造函数不能是虚的,因为当一个类的构造函数执行时,内存中没有虚表,这意味着还没有定义虚指针。因此构造函数应该总是非虚的

如果你从逻辑上思考构造函数是如何工作的,以及在c++中虚函数的意义/用法是什么,那么你会意识到虚构造函数在c++中是没有意义的。在c++中声明一些虚的东西意味着它可以被当前类的子类覆盖,但是在创建对象时调用构造函数,那时你不能创建类的子类,你必须创建类,所以永远不需要将构造函数声明为虚的。

另一个原因是,构造函数的名字与其类名相同如果我们将构造函数声明为virtual,那么它应该在它的派生类中以相同的名字重新定义,但两个类的名字不能相同。所以不可能有一个虚拟构造函数。

你可以在@stefan的回答中找到一个例子和为什么不允许这样做的技术原因。在我看来,这个问题的合理答案是:

virtual关键字的主要用途是在我们不知道基类指针将指向什么类型的对象时启用多态行为。

但这是一种更原始的方式,对于使用虚拟功能,你需要一个指针。指针需要什么?一个指向的对象!(考虑程序正确执行的情况)

因此,我们基本上需要一个已经存在于内存某处的对象(我们不关心内存是如何分配的,它可能是在编译时或运行时),以便我们的指针能够正确地指向该对象。

现在,想象一下当要指向的类的对象被分配一些内存时的情况——>它的构造函数将在该实例本身被自动调用!

所以我们可以看到,我们实际上不需要担心构造函数是虚的,因为在任何情况下,你希望使用多态行为,我们的构造函数已经执行,使我们的对象准备使用!

  1. 当调用构造函数时,尽管在此之前没有创建对象,但我们仍然知道将创建的对象的类型,因为该对象所属类的具体的构造函数已经被调用。

    与函数关联的Virtual关键字意味着将调用特定对象类型的函数

    因此,我的想法是,没有必要创建虚拟构造函数,因为将要创建的对象的所需构造函数已经被调用,而将构造函数设为虚拟构造函数只是一件多余的事情,因为特定对象的构造函数已经被调用,这与调用职业专用功能是相同的,后者是通过virtual关键字实现的。

    虽然内部实现由于vptr和vtable相关的原因不允许虚拟构造函数。 < / p >


  1. 另一个原因是c++是一种静态类型语言,我们需要在编译时知道变量的类型。

    编译器必须知道类类型才能创建对象。要创建的对象类型是一个编译时决策。

    如果我们将构造函数设为虚函数,则意味着我们不需要在编译时知道对象的类型(这就是虚函数所提供的。我们不需要知道实际的对象,只需要基指针来指向一个实际的对象,调用被指向对象的虚函数,而不需要知道对象的类型),如果我们在编译时不知道对象的类型,那么它就违背了静态类型语言。因此,无法实现运行时多态性。

    因此,如果在编译时不知道对象的类型,构造函数将不会被调用。因此,创建虚拟构造函数的想法失败了

面试答案是:虚ptr和表是与对象相关的,而不是类。因此构造函数构建虚表 因此,我们不能有虚构造函数,因为在创建obj之前没有虚表

“构造函数不能是虚拟的”;

  • 有一些合理的理由可以证明这种说法是正确的。
  1. 来创建一个对象对象类的构造函数必须与类的类型相同。但是,对于虚拟实现的构造函数,这是不可能的。
  2. 在调用构造函数时,虚拟表将不会被创建解析任何虚函数调用。因此,虚拟构造函数本身将没有任何可以查找的地方。

因此,不可能将构造函数声明为虚函数。