在C ++中调用构造函数而无需新建


142

我经常看到人们使用C ++创建对象

Thing myThing("asdf");

代替这个:

Thing myThing = Thing("asdf");

至少在不涉及模板的情况下,这似乎可行(使用gcc)。我现在的问题是,第一行是否正确?如果可以,我应该使用它吗?


25
两种形式都不新鲜。
丹尼尔·达拉斯

13
第二种形式将使用copy构造函数,所以不,它们不是等效的。
爱德华·斯特朗奇

我打了一下它,第一种方式似乎失败有时当模板与参数构造函数使用..
尼尔斯

1
哦,我为此获得了“好问题”徽章,真可惜!
尼尔斯

Answers:


153

实际上,这两行都是正确的,但是做的事情却稍有不同。

第一行通过调用以下格式的构造函数在堆栈上创建一个新对象 Thing(const char*)

第二个比较复杂。它基本上执行以下操作

  1. Thing使用构造函数创建类型的对象Thing(const char*)
  2. Thing使用构造函数创建类型的对象Thing(const Thing&)
  3. 呼叫 ~Thing()在步骤1中创建的对象

7
我猜想这些类型的操作是经过优化的,因此在性能方面没有显着差异。
M. Williams 2010年

14
我认为您的步骤不正确。Thing myThing = Thing(...)不使用赋值运算符,它仍然是复制构造的,就像说的那样Thing myThing(Thing(...)),并且不涉及默认构造的Thing(编辑:帖子后来被更正了)
AshleysBrain 2010年

1
因此,您可以说第二行是错误的,因为它没有明显的理由浪费资源。当然,创建第一个实例可能是出于某些副作用的目的,但是(在样式上)更糟。
MK。

3
不,@ Jared,不能保证。但是,即使编译器选择执行该优化,副本构造函数仍然需要可访问(即,不受保护或私有),即使未实现或未调用它也是如此。
罗伯·肯尼迪

3
它出现在副本可以被省略,即使拷贝构造函数有副作用-看到我的回答:stackoverflow.com/questions/2722879/...
道格拉斯Leeder先生

31

我认为第二行实际上是您的意思:

Thing *thing = new Thing("uiae");

这将是创建新动态对象(动态绑定和多态性所必需)并将其地址存储到指针的标准方法。您的代码执行JaredPar描述的操作,即创建两个对象(一个传递了一个const char*,另一个传递了一个const Thing&),然后~Thing()在第一个对象上调用destructor()const char*)。

相比之下,这是:

Thing thing("uiae");

创建一个静态对象,该对象将在退出当前范围时自动销毁。


1
不幸的是,这确实是创建新动态对象的最常用方法,而不是使用auto_ptr,unique_ptr或相关方法。
Fred Nurk 2011年

3
OP的问题是正确的,此答案完全涉及另一个问题(请参阅
@JaredPar

21

编译器可以将第二种形式优化为第一种形式,但是不必这样做。

#include <iostream>

class A
{
    public:
        A() { std::cerr << "Empty constructor" << std::endl; }
        A(const A&) { std::cerr << "Copy constructor" << std::endl; }
        A(const char* str) { std::cerr << "char constructor: " << str << std::endl; }
        ~A() { std::cerr << "destructor" << std::endl; }
};

void direct()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void assignment()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a = A(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void prove_copy_constructor_is_called()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    A b = a;
    static_cast<void>(b); // avoid warnings about unused variables
}

int main()
{
    direct();
    assignment();
    prove_copy_constructor_is_called();
    return 0;
}

gcc 4.4的输出:

TEST: direct
char constructor: direct
destructor

TEST: assignment
char constructor: assignment
destructor

TEST: prove_copy_constructor_is_called
char constructor: prove_copy_constructor_is_called
Copy constructor
destructor
destructor

静态强制转换为无效的目的是什么?
史蒂芬·克罗斯

1
@Stephen避免有关未使用变量的警告。
道格拉斯·里德

10

很简单,这两行都在堆栈上创建对象,而不是像“ new”一样在堆上创建对象。第二行实际上涉及对副本构造函数的第二次调用,因此应避免使用它(也需要按照注释中的说明进行更正)。您应该尽可能多地将堆栈用于小对象,因为它的速度更快,但是,如果对象要比堆栈框架生存更长的时间,那么显然是错误的选择。


对于那些不熟悉在堆栈上实例化对象与在堆上实例化对象之间的区别(即使用new而不是使用new)的人来说,这里是一个不错的线程。
edmqkk '19

2

理想情况下,编译器将优化第二个,但这不是必需的。第一种是最好的方法。但是,了解C ++中堆栈和堆之间的区别非常重要,因为您必须管理自己的堆内存。


编译器能否保证复制构造函数没有副作用(例如I / O)?
斯蒂芬·克罗斯

@Stephen -这并不重要,如果拷贝构造函数I / O -看到我的回答stackoverflow.com/questions/2722879/...
道格拉斯Leeder先生

好的,我知道,允许编译器将第二种形式转换为第一种形式,从而避免调用复制构造函数。
史蒂芬·克罗斯

2

我玩了一点,当构造函数不带参数时,语法似乎变得很奇怪。让我举个例子:

#include <iostream> 

using namespace std;

class Thing
{
public:
    Thing();
};

Thing::Thing()
{
    cout << "Hi" << endl;
}

int main()
{
    //Thing myThing(); // Does not work
    Thing myThing; // Works

}

因此,只写不带括号的Thing myThing实际上会调用构造函数,而Thing myThing()使编译器产生您想要创建函数指针或其他东西的作用!


6
这是C ++中众所周知的句法歧义。当您编写“ int rand()”时,编译器无法知道您的意思是“创建一个int并对其进行默认初始化”还是“声明函数rand”。规则是它会尽可能选择后者。
jpalecek

1
伙计们,这是最令人头疼的分析
Marc.2377

2

除了JaredPar答案

1个常用的ctor,第2个功能类似的ctor,带有临时对象。

使用不同的编译器在此处http://melpon.org/wandbox/处编译此源代码

// turn off rvo for clang, gcc with '-fno-elide-constructors'

#include <stdio.h>
class Thing {
public:
    Thing(const char*){puts(__FUNCTION__ );}
    Thing(const Thing&){puts(__FUNCTION__ );}   
    ~Thing(){puts(__FUNCTION__);}
};
int main(int /*argc*/, const char** /*argv*/) {
    Thing myThing = Thing("asdf");
}

然后您将看到结果。

来自ISO / IEC 14882 2003-10-15

8.5,第12部分

您的第一,第二构造称为直接初始化

12.1,第13部分

功能符号类型转换(5.2.3)可用于创建其类型的新对象。[注意:语法看起来像构造函数的显式调用。] ...以这种方式创建的对象未命名。[注:12.2描述了临时对象的生存期。] [注意:显式构造函数调用不会产生左值,请参见3.10。]


在哪里可以阅读有关RVO的信息:

12个特殊的成员函数/ 12.8复制类对象/第15部分

当满足某些条件时,即使该对象的副本构造函数和/或析构函数具有副作用允许实现忽略类对象的副本构造。

使用注释中的编译器标志将其关闭以查看此类复制行为)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.