为什么在C++中需要显式调用构造函数?

21

我知道在C++中我们可以使用域解析运算符显式地调用一个类的构造函数,即 className::className()。我想知道在什么情况下需要进行这样的调用。


5
直接调用构造函数是不正确的说法。标准明确规定(12.1/1):“构造函数没有名称。”你只能通过其他构造方式来调用构造函数,例如函数类型转换或就地新建。 - Richard Corden
7个回答

45

有时你需要显式地使用构造函数来创建临时对象。例如,如果你有一个带有构造函数的类:

class Foo
{
    Foo(char* c, int i);
};

以及一个函数

void Bar(Foo foo);

但是如果你没有Foo,你可以这样做

Bar(Foo("hello", 5));

这类似于类型转换。如果你有一个只接受一个参数的构造函数,C++编译器将使用该构造函数来执行隐式类型转换。

在已存在的对象上调用构造函数是不合法的。也就是说,你不能这样做:

Foo foo;
foo.Foo();  // compile error!

无论你做什么。但是你可以调用构造函数而不分配内存 - 这就是 放置new 的作用。

char buffer[sizeof(Foo)];      // a bit of memory
Foo* foo = new(buffer) Foo();  // construct a Foo inside buffer
你提供一段新的内存,它在那里构造对象,而不是分配新的内存。这种用法被认为是有害的,在大多数类型的代码中很少见,但在嵌入式和数据结构代码中很常见。
例如,std::vector::push_back使用这种技术调用拷贝构造函数。这样,它只需要做一次拷贝,而不是创建一个空对象并使用赋值运算符。

7
+1 用于定位new的方法。虽然有些奇怪,但如果你知道该怎么做,它会很有用。 - Graeme Perrow
实际上,如果你有一个只接受一个参数的构造函数,C++编译器将使用该构造函数来执行隐式转换。这就是为什么很多人默认在单参数构造函数上加上explicit关键字,只有当他们确定需要从参数类型到类类型进行隐式转换时才会去掉它。 - Steve Jessop
1
你没有调用构造函数。语法是 <typename>(ctor-arg列表),严谨来说并不同于<typename>::<typename>(ctor-arg列表)。这个语法只是让它看起来像你在调用构造函数。事实上,你永远不会像调用函数一样“调用”构造函数。 - Lightness Races in Orbit
@LightnessRacesinOrbit 当你执行 <typename>::<typename> <var-name>(ctor-arg list) 时,实际上发生了什么? - user1599559
1
@AdamSchnitzer:包括内存分配、基础初始化和成员初始化等许多事情。 - Lightness Races in Orbit

14

通常情况下,在需要一些参数的子类构造函数中:

class BaseClass
{
public:
    BaseClass( const std::string& name ) : m_name( name ) { }

    const std::string& getName() const { return m_name; }

private:

    const std::string m_name;

//...

};


class DerivedClass : public BaseClass
{
public:

    DerivedClass( const std::string& name ) : BaseClass( name ) { }

// ...
};

class TestClass : 
{
public:
    TestClass( int testValue ); //...
};

class UniqueTestClass 
     : public BaseClass
     , public TestClass
{
public:
    UniqueTestClass() 
       : BaseClass( "UniqueTest" ) 
       , TestClass( 42 )
    { }

// ...
};

...例如。

除此之外,我看不到其实用之处。我只是在我还太年轻而不知道自己在做什么的时候,在其他代码中调用了构造函数...


6
当创建派生类的实例时,C++会隐式地调用父类的构造函数,但它只会调用默认构造函数——除非你在初始化列表中显式地调用特定的父类构造函数。 - sean riley
是的,在我的例子中,我确保BaseClass的唯一有效构造函数需要一些参数。我不记得有什么情况真正需要显式调用默认构造函数。也许在虚继承中? - Klaim

3
我认为编译器错误 C2585 的错误信息提供了使用作用域分辨运算符调用构造函数的最佳理由,正如Charlie的答案所述:
从基于多重继承的类或结构体类型进行转换。如果该类型多次继承相同的基类,则转换函数或运算符必须使用作用域解析(::)来指定在转换中要使用哪个继承类。
因此,假设您有BaseClass,并且BaseClassA和BaseClassB都继承自BaseClass,然后DerivedClass同时继承BaseClassA和BaseClassB。
如果你正在进行转换或运算符重载以将DerivedClass转换为BaseClassA或BaseClassB,你将需要确定在转换中使用哪个构造函数(我想是像拷贝构造函数之类的)。

2

通常情况下,您不会直接调用构造函数。new操作符会为您调用它,或者子类会调用父类的构造函数。在C++中,基类在派生类的构造函数开始之前保证完全构造。

唯一需要直接调用构造函数的情况是极其罕见的,即在不使用new管理内存的情况下。即使在这种情况下,您也不应该这样做。相反,您应该使用operator new的放置形式。


0

请考虑以下程序。

template<class T>
double GetAverage(T tArray[], int nElements)
{
T tSum = T(); // tSum = 0

for (int nIndex = 0; nIndex < nElements; ++nIndex)
{
    tSum += tArray[nIndex];
}

// Whatever type of T is, convert to double
return double(tSum) / nElements;
}

这将显式调用默认构造函数来初始化变量。


0

有一些有效的用例需要公开类的构造函数。例如,如果您希望使用竞技场分配器进行自己的内存管理,则需要两阶段构造,包括分配和对象初始化。

我采取的方法类似于许多其他语言。我只需将构造代码放在众所周知的公共方法(例如Construct()init()等)中,并在需要时直接调用它们。

您可以创建与您的构造函数匹配的这些方法的重载;您的常规构造函数仅调用它们。在代码中放置大型注释,以警告其他人您正在执行此操作,以免他们在错误的位置添加重要的构造代码。

请记住,无论使用哪个构造函数重载,都只有一个析构函数方法,因此要使您的析构函数能够处理未初始化的成员。

我建议不要尝试编写可以重新初始化的初始化程序。很难区分您是否正在查看由于未初始化的内存而仅具有垃圾数据的对象还是实际持有真实数据的对象。

最困难的问题出现在具有虚方法的类中。在这种情况下,编译器会在类的开头插入vtable函数表指针作为隐藏字段。您可以手动初始化此指针,但基本上取决于编译器特定的行为,并且很可能会让您的同事看着你发呆。
Placement new 在许多方面都存在问题;在数组的构造/销毁方面就是一个例子,所以我不太使用它。

0

我认为你通常不会在构造函数中以你描述的方式使用它。然而,如果你有两个不同命名空间中的类,你将需要它。例如,为了区分这两个虚构的类Xml :: ElementChemistry :: Element

通常,类的名称与作用域解析运算符一起使用,调用继承类的父类上的函数。因此,如果你有一个从Animal继承的Dog类,并且这些类都以不同的方式定义Eat()函数,那么可能有一种情况,当你想在名为"someDog"的Dog对象上使用Animal版本的eat时。我的C++语法有点生疏,但我认为在这种情况下,你会说someDog.Animal :: Eat()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接