C++中含有其他对象的类的隐式复制构造函数

60

我知道如果你没有自己实现拷贝构造函数,编译器有时会提供一个默认的拷贝构造函数。但是我不确定这个构造函数具体做了什么。如果我有一个包含其他对象的类,其中没有一个声明了拷贝构造函数,那么行为会是什么样子?例如,像这样的一个类:

class Foo {
  Bar bar;
};

class Bar {
  int i;
  Baz baz;
};

class Baz {
  int j;
};

现在如果我这样做:

Foo f1;
Foo f2(f1);

默认复制构造函数会做什么?在Foo中编译器生成的复制构造函数会调用Bar中生成的构造函数来复制bar,随后Bar又会调用Baz中编译器生成的复制构造函数吗?

5个回答

84
Foo f1;
Foo f2(f1);

是的,这段代码会实现你所期望的功能:
Foo 的 f2 拷贝构造函数 Foo::Foo(Foo const&) 会被调用。
该拷贝构造函数会递归地对其基类和每个成员进行拷贝构造。

如果你定义了一个如下的类:

class X: public Y
{
    private:
        int     m_a;
        char*   m_b;
        Z       m_c;
};

以下方法将由您的编译器定义。

  • 构造函数(默认)(2个版本)
  • 构造函数(复制)
  • 析构函数(默认)
  • 赋值运算符

构造函数:默认:

实际上有两个默认构造函数。
其中一个用于“零初始化”,而另一个用于“值初始化”。使用哪个取决于您在初始化期间是否使用了()

// Zero-Initialization compiler generated constructor
X::X()
    :Y()                // Calls the base constructor
                        //     If this is compiler generated use 
                        //     the `Zero-Initialization version'
    ,m_a(0)             // Default construction of basic PODS zeros them
    ,m_b(0)             // 
    m_c()               // Calls the default constructor of Z
                        //     If this is compiler generated use 
                        //     the `Zero-Initialization version'
{
}

// Value-Initialization compiler generated constructor
X::X()
    :Y()                // Calls the base constructor
                        //     If this is compiler generated use 
                        //     the `Value-Initialization version'
    //,m_a()            // Default construction of basic PODS does nothing
    //,m_b()            // The values are un-initialized.
    m_c()               // Calls the default constructor of Z
                        //     If this is compiler generated use 
                        //     the `Value-Initialization version'
{
}

注意:如果基类或任何成员没有有效的可见默认构造函数,则无法生成默认构造函数。除非您的代码尝试使用默认构造函数(然后只有编译时错误),否则这不是一个错误。

构造函数(拷贝)

X::X(X const& copy)
    :Y(copy)            // Calls the base copy constructor
    ,m_a(copy.m_a)      // Calls each members copy constructor
    ,m_b(copy.m_b)
    ,m_c(copy.m_c)
{}

注意:如果基类或任何成员没有有效的可见复制构造函数,则无法生成复制构造函数。这不是一个错误,除非您的代码尝试使用复制构造函数(然后只会出现编译时错误)。

赋值运算符

X& operator=(X const& copy)
{
    Y::operator=(copy); // Calls the base assignment operator
    m_a = copy.m_a;     // Calls each members assignment operator
    m_b = copy.m_b;
    m_c = copy.m_c;

    return *this;
}

注意:如果基类或任何成员没有有效的可行赋值运算符,那么赋值运算符将无法生成。除非代码尝试使用赋值运算符(这时只会出现编译时错误),否则这不是一个错误。

析构函数

X::~X()
{
                        // First runs the destructor code
}
    // This is psudo code.
    // But the equiv of this code happens in every destructor
    m_c.~Z();           // Calls the destructor for each member
    // m_b              // PODs and pointers destructors do nothing
    // m_a          
    ~Y();               // Call the base class destructor
  • 如果声明任何构造函数(包括复制构造函数),则编译器不会实现默认构造函数。
  • 如果声明了复制构造函数,则编译器不会生成一个。
  • 如果声明了赋值运算符,那么编译器将不会生成一个。
  • 如果声明了析构函数,那么编译器将不会生成一个。

查看您的代码后,以下复制构造函数会被生成:

Foo::Foo(Foo const& copy)
    :bar(copy.bar)
{}

Bar::Bar(Bar const& copy)
    :i(copy.i)
    ,baz(copy.baz)
{}

Baz::Baz(Baz const& copy)
    :j(copy.j)
{}

1
m_am_bm_c不是非常具有信息量的名称。这本来不是问题,但你最初将它们定义为m_am_c(对于char*),以及m_d(对于Z类型)。我猜更具有信息量的名称可以避免这个小错误。无论如何,因为你的好帖子还是给你点赞。 - Chris Lutz
固定类型:名称是故意这样命名的,以便可以显示顺序。我本来会使用m_1、m_2、m_3,但我不喜欢在标识符中使用数字。 - Martin York

14
编译器会提供一个复制构造函数,除非你自己声明(注意:不是定义)一个。编译器生成的复制构造函数只调用类的每个成员(和每个基类)的复制构造函数。
另外,赋值运算符和析构函数也是如此。但对于默认构造函数则不同:只有在你没有声明任何其他构造函数时,编译器才会提供它。

2

是的,编译器生成的拷贝构造函数会按照包含类中成员声明的顺序执行成员逐一拷贝。如果任何一个成员类型本身没有提供拷贝构造函数,则包含类的拷贝构造函数无法生成。如果你可以决定某些适当的方式来初始化不能被拷贝构造的成员的值,可能仍然可以手动编写拷贝构造函数——例如使用其它构造函数之一。


1

C++中的默认复制构造函数会创建一个浅拷贝。浅拷贝不会为原始对象引用的对象创建新的副本;旧对象和新对象只是包含指向相同内存位置的不同指针。


我知道它创建了一个浅拷贝,指向的对象不会被复制,但是像我的例子中那样仅仅包含的对象呢? - Jamison Dance
抱歉,我已经在 Java 领域生活太久了,忘记了在 C++ 中对象可以放在堆栈上。 - Phil
2
我更倾向于说它是按值复制,指针本身就是一个值,所以只有指针本身作为值被复制。指针所指向的对象并没有被复制。这样做会创建一个具有新地址的新对象,这将需要在结果指针中使用不同的值,这绝对不像是“复制的指针”。 - seh

0

编译器将为您生成所需的构造函数。

然而,一旦您自己定义了拷贝构造函数,编译器将放弃为该类生成任何内容,并且如果您没有定义适当的构造函数,编译器将报错。

使用您的示例:

class Baz {
    Baz(const Baz& b) {}
    int j;
};
class Bar {
    int i;
    Baz baz;
};
class Foo {
    Bar bar;
};

尝试默认实例化或复制构造Foo将会抛出错误,因为Baz不可复制构造,编译器无法为Foo生成默认和复制构造函数。

这适用于任何构造函数吗?如果我定义了一个无参构造函数,编译器是否仍会生成任何构造函数? - Jamison Dance
我的错,你是对的,默认不会防止复制,正好相反。 - Coincoin
1
小心使用“Throw”这个词,它暗示了运行时错误。复制构造函数的问题(无法进行复制构造)在编译时被检测到。 - Martin York

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