在哪些情况下调用 C++ 复制构造函数?

Posted

技术标签:

【中文标题】在哪些情况下调用 C++ 复制构造函数?【英文标题】:In which situations is the C++ copy constructor called? 【发布时间】:2014-02-07 23:47:19 【问题描述】:

我知道在 c++ 中会调用复制构造函数的以下情况:

    当一个现有对象被分配一个它自己的类的对象时

    MyClass A,B;
    A = new MyClass();
    B=A; //copy constructor called 
    

    如果函数接收作为参数,按值传递的类的对象

    void foo(MyClass a);
    foo(a); //copy constructor invoked
    

    当函数(按值)返回类的对象时

    MyClass foo ()
       
          MyClass temp;
          ....
          return temp; //copy constructor called
        
    

请随时纠正我所犯的任何错误;但是我更好奇是否还有其他调用复制构造函数的情况。

【问题讨论】:

我以为A=B; 调用了复制赋值运算符。 另请阅读返回值优化 (RVO),您的最后一个示例可能不会复制任何内容。 另外,A = new MyClass(); 不会编译。 这不是有效的 C++。 @BWG,只有在声明A之后完成。例如:A a; ... a=b;。如果在声明本身完成,则A a=b 等同于A a(b) 【参考方案1】:

当一个现有对象被分配一个它自己的类的对象时

    B = A;

不一定。这种赋值称为copy-assignment,意思是调用类的赋值运算符对所有数据成员进行成员赋值。实际功能是MyClass& operator=(MyClass const&)

此处不调用复制构造函数。这是因为赋值运算符引用了它的对象,因此不执行复制构造。

复制分配与复制初始化不同,因为复制初始化仅在对象初始化时进行。例如:

T y = x;
  x = y;

第一个表达式通过复制x 来初始化y。它调用复制构造函数MyClass(MyClass const&)

如前所述,x = y 是对赋值运算符的调用。

(还有一个叫做copy-elison的东西,编译器会省略对复制构造函数的调用。你的编译器很可能会使用它)。


如果一个函数接收作为参数,按值传递,一个类的对象

    void foo(MyClass a);
    foo(a);

这是正确的。但是,请注意,在 C++11 中,如果 a 是一个 xvalue,并且如果 MyClass 具有适当的构造函数 MyClass(MyClass&&),则 a 可以将 moved 放入参数中。

(copy-constructor和move-constructor是类的两个默认编译器生成的成员函数。如果你自己不提供,编译器会在特定情况下慷慨地为你提供) .


当函数(按值)返回类的对象时

    MyClass foo ()
    
        MyClass temp;
        ....
        return temp; // copy constructor called
    

通过return-value optimization,正如一些答案中提到的,编译器可以删除对复制构造函数的调用。通过使用编译器选项-fno-elide-constructors,您可以禁用复制elison,并查看在这些情况下确实会调用复制构造函数。

【讨论】:

我不认为最后一个例子是真的。 “return temp”不会调用复制构造函数,但如果添加“MyClass & ref = temp;”和“return ref;”,这一次将调用复制构造函数。 @chenlian 现在我回到这个答案,我发现它有点不准确。如果-fno-elide-constructors 未启用,则实际上是 move-constructor 如果可用则首先调用它,如果不可用则调用 copy-constructor。 MyClass& ref=temp; return ref 调用复制构造函数的原因是因为返回值优化需要一个 id 表达式。在这种情况下,您需要一个明确的 std::move -fno-elide-constructors点赞。没有它,我的一些测试永远无法符合我的假设。【参考方案2】:

其他人提供了很好的答案,并附有解释和参考。

此外,我编写了一个类来检查不同类型的实例化/分配(C++11 就绪),在一个广泛的测试中:

#include <iostream>
#include <utility>
#include <functional>


template<typename T , bool MESSAGES = true>
class instantation_profiler

private:
    static std::size_t _alive , _instanced , _destroyed ,
                       _ctor , _copy_ctor , _move_ctor ,
                       _copy_assign , _move_assign;


public:
    instantation_profiler()
    
        _alive++;
        _instanced++;
        _ctor++;

        if( MESSAGES ) std::cout << ">> construction" << std::endl;
    

    instantation_profiler( const instantation_profiler& )
    
        _alive++;
        _instanced++;
        _copy_ctor++;

        if( MESSAGES ) std::cout << ">> copy construction" << std::endl;
    

    instantation_profiler( instantation_profiler&& )
    
        _alive++;
        _instanced++;
        _move_ctor++;

        if( MESSAGES ) std::cout << ">> move construction" << std::endl;
    

    instantation_profiler& operator=( const instantation_profiler& )
    
        _copy_assign++;

        if( MESSAGES ) std::cout << ">> copy assigment" << std::endl;
    

    instantation_profiler& operator=( instantation_profiler&& )
    
        _move_assign++;

        if( MESSAGES ) std::cout << ">> move assigment" << std::endl;
    

    ~instantation_profiler()
    
        _alive--;
        _destroyed++;

        if( MESSAGES ) std::cout << ">> destruction" << std::endl;
    



    static std::size_t alive_instances()
    
        return _alive;
    

    static std::size_t instantations()
    
        return _instanced;
    

    static std::size_t destructions()
    
        return _destroyed;
    

    static std::size_t normal_constructions()
    
        return _ctor;
    

    static std::size_t move_constructions()
    
        return _move_ctor;
    

    static std::size_t copy_constructions()
    
        return _copy_ctor;
    

    static std::size_t move_assigments()
    
        return _move_assign;
    

    static std::size_t copy_assigments()
    
        return _copy_assign;
    


    static void print_info( std::ostream& out = std::cout )
    
        out << "# Normal constructor calls: "  << normal_constructions() << std::endl
            << "# Copy constructor calls: "    << copy_constructions()   << std::endl
            << "# Move constructor calls: "    << move_constructions()   << std::endl
            << "# Copy assigment calls: "      << copy_assigments()      << std::endl
            << "# Move assigment calls: "      << move_assigments()      << std::endl
            << "# Destructor calls: "          << destructions()         << std::endl
            << "# "                                                      << std::endl
            << "# Total instantations: "       << instantations()        << std::endl
            << "# Total destructions: "        << destructions()         << std::endl
            << "# Current alive instances: "   << alive_instances()      << std::endl;
    
;

template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_alive       = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_instanced   = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_destroyed   = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_ctor        = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_copy_ctor   = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_move_ctor   = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_copy_assign = 0;
template<typename T , bool MESSAGES>
std::size_t instantation_profiler<T,MESSAGES>::_move_assign = 0;

这是测试:

struct foo : public instantation_profiler<foo>

    int value;
;



//Me suena bastante que Boost tiene una biblioteca con una parida de este estilo...
struct scoped_call

private:
    std::function<void()> function; 

public:
    scoped_call( const std::function<void()>& f ) : function( f ) 

    ~scoped_call()
    
        function();
    
;


foo f()

    scoped_call chapuza( []() std::cout << "Exiting f()..." << std::endl;  );

    std::cout << "I'm in f(), which returns a foo by value!" << std::endl;

    return foo();



void g1( foo )

    scoped_call chapuza( []() std::cout << "Exiting g1()..." << std::endl;  );

    std::cout << "I'm in g1(), which gets a foo by value!" << std::endl;


void g2( const foo& )

    scoped_call chapuza( []() std::cout << "Exiting g2()..." << std::endl;  );

    std::cout << "I'm in g2(), which gets a foo by const lvalue reference!" << std::endl;


void g3( foo&& )

    scoped_call chapuza( []() std::cout << "Exiting g3()..." << std::endl;  );

    std::cout << "I'm in g3(), which gets an rvalue foo reference!" << std::endl;


template<typename T>
void h( T&& afoo )

    scoped_call chapuza( []() std::cout << "Exiting h()..." << std::endl;  );

    std::cout << "I'm in h(), which sends a foo to g() through perfect forwarding!" << std::endl;

    g1( std::forward<T>( afoo ) );



int main()

    std::cout << std::endl << "Just before a declaration ( foo a; )"                << std::endl;                                        foo a;
    std::cout << std::endl << "Just before b declaration ( foo b; )"                << std::endl;                                        foo b;
    std::cout << std::endl << "Just before c declaration ( foo c; )"                << std::endl;                                        foo c;
    std::cout << std::endl << "Just before d declaration ( foo d( f() ); )"         << std::endl;                                        foo d( f() );

    std::cout << std::endl << "Just before a to b assigment ( b = a )"              << std::endl;                                        b = a;
    std::cout << std::endl << "Just before ctor call to b assigment ( b = foo() )"  << std::endl;                                        b = foo();
    std::cout << std::endl << "Just before f() call to b assigment ( b = f() )"     << std::endl;                                        b = f();



    std::cout << std::endl << "Just before g1( foo ) call with lvalue arg ( g1( a ) )"                         << std::endl;             g1( a );
    std::cout << std::endl << "Just before g1( foo ) call with rvalue arg ( g1( f() ) )"                       << std::endl;             g1( f() );
    std::cout << std::endl << "Just before g1( foo ) call with lvalue ==> rvalue arg ( g1( std::move( a ) ) )" << std::endl;             g1( std::move( a ) );

    std::cout << std::endl << "Just before g2( const foo& ) call with lvalue arg ( g2( b ) )"                          << std::endl;     g2( b );
    std::cout << std::endl << "Just before g2( const foo& ) call with rvalue arg ( g2( f() ) )"                        << std::endl;     g2( f() );
    std::cout << std::endl << "Just before g2( const foo& ) call with lvalue ==> rvalue arg ( g2( std::move( b ) ) )"  << std::endl;     g2( std::move( b ) );

  //std::cout << std::endl << "Just before g3( foo&& ) call with lvalue arg ( g3( c ) )"                         << std::endl;           g3( c );
    std::cout << std::endl << "Just before g3( foo&& ) call with rvalue arg ( g3( f() ) )"                       << std::endl;           g3( f() );
    std::cout << std::endl << "Just before g3( foo&& ) call with lvalue ==> rvalue arg ( g3( std::move( c ) ) )" << std::endl;           g3( std::move( c ) );



    std::cout << std::endl << "Just before h() call with lvalue arg ( h( d ) )"                         << std::endl;                    h( d );
    std::cout << std::endl << "Just before h() call with rvalue arg ( h( f() ) )"                       << std::endl;                    h( f() );
    std::cout << std::endl << "Just before h() call with lvalue ==> rvalue arg ( h( std::move( d ) ) )" << std::endl;                    h( std::move( d ) );

    foo::print_info( std::cout );

这是使用GCC 4.8.2-O3-fno-elide-constructors 标志编译的测试摘要:

普通构造函数调用:10 复制构造函数调用:2 移动构造函数调用:11 复制分配调用:1 移动分配调用:2 析构函数调用:19

总实例数:23 破坏总数:19 当前活跃实例:4

最后启用复制省略的相同测试:

普通构造函数调用:10 复制构造函数调用:2 移动构造函数调用:3 复制分配调用:1 移动分配调用:2 析构函数调用:11

总实例数:15 破坏总数:11 当前活跃实例:4

Here 是在 ideone 运行的完整代码。

【讨论】:

【参考方案3】:

这基本上是正确的(除了您在#1 中的错字)。

另外一个需要注意的特定情况是,当容器中有元素时,这些元素可能会在不同时间被复制(例如,在向量中,当向量增长或某些元素被删除时)。这实际上只是 #1 的一个示例,但很容易忘记它。

【讨论】:

【参考方案4】:

我可能错了,但是这个类可以让你看到什么时候被调用:

class a 
public:
    a() 
        printf("constructor called\n");
    ;  
    a(const a& other)  
        printf("copy constructor called\n");
    ;    
    a& operator=(const a& other) 
        printf("copy assignment operator called\n");
        return *this; 
    ;
;

那么这段代码:

a b; //constructor
a c; //constructor
b = c; //copy assignment
c = a(b); //copy constructor, then copy assignment

产生这个作为结果:

constructor called
constructor called
copy assignment operator called
copy constructor called
copy assignment operator called

另一个有趣的事情,假设你有以下代码:

a* b = new a(); //constructor called
a* c; //nothing is called
c = b; //still nothing is called
c = new a(*b); //copy constructor is called

这是因为当你分配一个指针时,它对实际对象没有任何作用。

【讨论】:

还有一个a c = b;也调用了拷贝构造函数 不要忘记按值传递对象作为参数,或按值返回对象。 我的代码并不是为了演示所有可能的事件,它显示了一个可以用来查看事件的类。 @Swapnil 我认为它应该是复制赋值运算符,因为您使用的是 = 运算符。据我所知,如果你使用 = 运算符,它总是调用 operator=,除非是第一次初始化。 如果你需要测试向量行为,当你像这样声明复制构造函数(和赋值操作)时,默认情况下不会定义 move 构造函数(和赋值操作)由编译器!因此,在某些情况下,移动构造函数可能比复制更受青睐。但你无法分辨,因为这样复制构造函数总是会被调用。【参考方案5】:

情况 (1) 不正确,无法按照您编写的方式编译。应该是:

MyClass A, B;
A = MyClass(); /* Redefinition of `A`; perfectly legal though superfluous: I've
                  dropped the `new` to defeat compiler error.*/
B = A; // Assignment operator called (`B` is already constructed)
MyClass C = B; // Copy constructor called.

在情况(2)中你是正确的。

但是在情况(3)中,复制构造函数可能不会被调用:如果编译器没有检测到副作用,那么它可以实现返回值优化以优化掉不必要的深度复制。 C++11 使用 右值引用 将其形式化。

【讨论】:

【参考方案6】:

以下是调用复制构造函数的情况。

    在实例化一个对象并使用来自另一个对象的值对其进行初始化时。 按值传递对象时。 当对象按值从函数返回时。

【讨论】:

你只是重复了问题的内容。答案应该是“不”。【参考方案7】:

复制构造函数被调用的三种情况: 当我们复制一个对象时。 当我们通过值将对象作为参数传递给方法时。 当我们从一个方法中按值返回一个对象时。

这些是唯一的情况......我认为......

【讨论】:

以上是关于在哪些情况下调用 C++ 复制构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

c++的复制构造函数

在没有 new 的情况下在 C++ 中调用构造函数

是否为数组/向量插入调用了赋值运算符或复制构造函数?

c++,类的对象作为形参时一定会调用复制构造函数吗?

c++拷贝构造函数

C ++复制构造函数中的异常[关闭]