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

36

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

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

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
       } 
    
    请随意指出我可能犯的任何错误;但我更好奇是否存在其他调用复制构造函数的情况。

  • 14
    我认为 A=B; 会调用复制赋值运算符。 - BWG
    6
    请参阅有关返回值优化(RVO)的内容,您最后的示例可能不会复制任何内容。 - Mat
    12
    此外,A = new MyClass();无法编译。 - Andy Prowl
    3
    这不是有效的C++代码。 - Lightness Races in Orbit
    3
    @BWG,在声明A之后才能进行赋值操作。例如:A a; ... a=b;。如果在声明时进行赋值操作,则A a=b等同于A a(b) - barak manos
    显示剩余7条评论
    7个回答

    37

    When an existing object is assigned an object of it own class

        B = A;
    
    不一定。这种类型的赋值被称为“拷贝赋值”,意味着类的赋值运算符将被调用以执行所有数据成员的逐个赋值。实际函数是MyClass& operator=(MyClass const&)这里并不会调用拷贝构造函数。这是因为赋值运算符取其对象的引用,因此不进行拷贝构造。
    拷贝赋值与拷贝初始化不同,因为拷贝初始化仅在对象初始化时完成。例如:
    T y = x;
      x = y;
    

    第一个表达式通过复制x来初始化y。它调用了复制构造函数MyClass(MyClass const&)。
    正如提到的,x = y是对赋值运算符的调用。
    (还有一种叫做copy-elison的东西,编译器将省略对复制构造函数的调用。你的编译器很可能使用这个功能)。

    If a functions receives as argument, passed by value, an object of a class

        void foo(MyClass a);
        foo(a);
    
    这是正确的。但是请注意,在C++11中,如果a是一个xvalue,并且如果MyClass有适当的构造函数MyClass(MyClass&&),则a可以被移动到参数中。
    (复制构造函数和移动构造函数是类的默认编译器生成成员函数之一。如果您没有自己提供它们,在特定情况下编译器会慷慨地为您提供)。

    When a function returns (by value) an object of the class

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

    正如一些答案中提到的返回值优化,编译器可以删除对拷贝构造函数的调用。通过使用编译器选项-fno-elide-constructors,您可以禁用复制省略并查看在这些情况下确实会调用拷贝构造函数。


    我不认为最后一个例子是正确的。 "return temp" 不会调用复制构造函数,但如果你添加 "MyClass & ref = temp;" 和 "return ref;",这时复制构造函数将被调用。 - chenlian
    3
    现在我回到这个答案,发现它有点不准确。如果没有启用-fno-elide-constructors,则实际上会首先调用移动构造函数(如果可用),如果不可用,则调用复制构造函数。之所以MyClass& ref=temp; return ref调用复制构造函数是因为返回值优化需要一个id表达式。在这种情况下,你需要显式使用std::move - David G
    -fno-elide-constructors 点赞。没有它,我的一些测试永远无法匹配我的假设。 - Rick

    25

    我可能对此有所错误,但这个类允许您查看什么被调用以及何时被调用:

    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
    

    这是因为当你赋值一个指针时,这并不会对实际的对象进行任何操作。


    11
    还有一个 a c = b; 语句也会调用拷贝构造函数。 - prajmus
    1
    不要忘记通过值传递对象作为参数,或者通过值返回对象。 - Some programmer dude
    2
    我的代码并不意味着展示所有可能的事件,它只是展示了一个可以用来查看事件的类。 - BWG
    1
    @Swapnil 我认为应该是复制赋值运算符,因为你正在使用 = 运算符。据我所知,如果你使用 = 运算符,它总是调用 operator=,除非它是第一次初始化。 - BWG
    1
    如果您需要测试向量行为,当您声明复制构造函数(和赋值运算符)时,编译器默认情况下不会定义移动构造函数(和赋值运算符)!因此,在某些情况下,可能更喜欢使用移动构造函数而不是复制构造函数,但您无法确定,因为这种方式始终会调用复制构造函数。 - Vassilis
    显示剩余2条评论

    12

    情况(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通过rvalue引用正式规范了这一点。


    6

    这基本上是正确的(除了你在#1中的拼写错误)。

    还有一个需要特别注意的具体场景是当您有容器中的元素时,这些元素可能会被复制多次(例如在向量中,当向量增长或一些元素被删除时)。这实际上只是#1的一个例子,但很容易忘记它。


    5

    复制构造函数被调用的三种情况:

    1. 当我们复制一个对象时。
    2. 当我们将一个对象按值传递给一个方法时。
    3. 当我们从一个方法中按值返回一个对象时。

    这些是唯一需要调用复制构造函数的情况...我想是这样的...


    4
    以下是调用复制构造函数的情况:
    1. 在实例化一个对象并使用另一个对象中的值进行初始化时。
    2. 当按值传递一个对象时。
    3. 当从函数中返回一个对象时,该对象是按值返回的。

    2
    你只是重复了问题的内容。答案应该是“不”。 - Lightness Races in Orbit

    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

    这里是在ideone上运行的完整代码。

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