【问题标题】:C++ Why should I suppress the default copy constructor?C++ 为什么要禁止默认的复制构造函数?
【发布时间】:2014-04-23 11:12:30
【问题描述】:

来自 Bjarne Stroustrup 的 C++ 编程语言第 4 版:

3.3.4。抑制操作

对层次结构中的类使用默认复制或移动通常是一场灾难: 只给出一个指向基址的指针,我们根本不知道派生的成员是什么 类有(§3.2.2),所以我们不知道如何复制它们。所以,最好的办法 do 通常是删除默认的复制和移动操作,也就是 消除这两个操作的默认定义:

class Shape {
    public:
        Shape(const Shape&) =delete; // no copy operations
        Shape& operator=(const Shape&) =delete;
        Shape(Shape&&) =delete; // no move operations
        Shape& operator=(Shape&&) =delete;
        ~Shape();
        // ...
};

为了理解他的意思,我创建了以下示例:

#include <iostream>

using namespace std;

class Person {
    private:
            int age;
    public:
            Person(const int& Age) : age {Age} {};
            Person(const Person& from) : age {from.Age()} { cout << "copy constructor" << endl; };
            Person& operator=(const Person& from) { cout << "copy assignment" << endl; age = from.Age(); return *this; }
            virtual void Print() { cout << age << endl; };
            void Age(const int& Age) { age = Age; };
            int Age() const { return age; };
};

class Woman : public Person {
    private:
            int hotness;
public:
            Woman(const int& Age, const int& Hotness) : Person(Age), hotness {Hotness} {};
            Woman(const Woman& from) : Person(from), hotness {from.Hotness()} { cout << "copy constructor of woman" << endl; };
            Woman& operator=(const Woman& from) { Person::operator=(from); cout << "copy assignment of woman" << endl; hotness = from.Hotness(); return *this; };
            void Print() override { cout << Age() << " and " << hotness << endl; };
            int Hotness() const { return hotness; };
};

int main() {
    Woman w(24, 10);

    Person p = w;
    p.Print();

    return 0;
}

这个版本的程序的输出是:

copy constructor
24

作为一个菜鸟,这对我来说有点意外,但后来 a 意识到由于 p 不是指针,因此不使用虚拟表,并且由于它是 Person,因此调用了 Person::Print() .所以我知道 Person 的复制构造函数被调用了,但我不知道 Woman 的复制构造函数是否被调用,但这并不重要,因为 p 是一个 Person,通过它我永远无法访问对女人::性感,即使我尝试了演员表。

所以我认为他可能只是在谈论指针,所以我尝试了这个:

int main() {
    Woman w(24, 10);

    Person* p = new Person(20);
    p->Print();
    p = &w;
    p->Print();

    return 0;
}

新的输出是:

20
24 and 10

现在 p 是一个指针,因为它是一个指针,所以不会进行复制或移动,只需更改引用。

然后我想我可以尝试取消引用 p 并将 w 分配给它:

int main() {
    Woman w(24, 10);

    Person* p = new Person(20);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

输出是这样的:

20
copy assignment
24

我认为第二次调用 p->Print() 会调用 Woman::Print(),因为 p 指向的是一个女人,但事实并非如此。知道为什么吗?来自 Person 的复制分配被调用,我认为是因为 p 是 Person*。

然后我尝试了这个:

int main() {
    Woman w(24, 10);

    Person* p = new Woman(20, 7);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

新的输出是这样的:

20 and 7
copy assignment
24 and 7

所以我猜是因为 p 是 Person*,所以调用了 Person 的副本分配,而不是 Woman 的副本分配。奇怪的是,年龄更新了,但热度值保持不变,我不知道为什么。

再试一次:

int main() {
    Woman w(24, 10);

    Woman* p = new Woman(20, 7);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

输出:

20 and 7
copy assignment
copy assignment of woman
24 and 10

现在数字似乎是对的。

我的下一步是删除 Person 的复制分配的实现,看看是否会调用默认值:

//Person& operator=(const Person& from) { cout << "copy assignment" << endl; age = from.Age(); return *this; }

输出:

20 and 7
copy assignment of woman
24 and 10

请注意,年龄是复制的,所以不用担心。

下一个明显的举措是删除女人的复制分配的实现,看看会发生什么:

//Woman& operator=(const Woman& from) { Person::operator=(from); cout << "copy assignment of woman" << endl; hotness = from.Hotness(); return *this; };

输出:

20 and 7
24 and 10

一切似乎都很好。

所以现在我不太明白作者的意思,所以如果有人能帮助我,我将不胜感激。

谢谢。

密送。

【问题讨论】:

  • 作者建议复制构造函数的默认行为是浅拷贝,这通常不是您想要的(特别是如果您的类包含指针)。从本质上讲,他是说除非您明确希望您的类是可复制的并定义自己的复制构造函数,否则您应该删除它以防止人们使用默认生成的浅拷贝并期望它生成正确的副本。
  • 一个大问题是,如果你的类使用指针,一个默认副本将具有相同的指针值,所以你的两个类实例将指向内存中的相同空间,这意味着更改值一个会改变另一个。另外,你的帖子有点长,你应该看看你能不能把它剪掉一点或者做一个 tl;dr 部分。
  • @Namfuak 这将是浅拷贝和深拷贝之间的区别。换句话说,浅拷贝基本上是for arg in class.args: new_class.arg = class.arg,没有比这更聪明的了。
  • @aruisdante 我知道如果你的类是一个资源句柄,你应该要么实现深拷贝,要么删除默认的拷贝构造函数。但是作者所说的与基类上的默认复制构造函数有关,基类是层次结构的一部分。我只是不太明白他在担心什么,因为在我的测试之后我注意到一个基类的默认复制构造函数是自动调用的。
  • 因为Base(Inherited) // Base = Inherited 是一种有效的做法,并且可能不会产生预期的结果。在vector&lt;Base&gt; 等容器中使用它时,最常发生这种情况。 IE。 vector&lt;Base&gt;[0] = Inherited 会起作用,但不会做你期望它做的事情,即在向量中有一个 Inherited 的实例。它会简单地创建一个Base,其中包含来自Inherited 浅层复制的公共字段。

标签: c++ pointers copy-constructor


【解决方案1】:
Woman w(24, 10);

Person p = w;
p.Print();

24

作为一个菜鸟,这对我来说有点意外,但后来 a 意识到由于 p 不是指针,因此不使用虚拟表,并且由于它是 Person,因此调用了 Person::Print() .

正确

所以我知道 Person 的复制构造函数被调用了,但我不知道 Woman 的复制构造函数是否被调用,...

不,它没有。

...但这并不重要,因为 p 是一个人,通过它我永远无法访问 Woman::Hotness,即使我尝试了演员表。

考虑Person p = 行创建了一个新变量p,它具有足够的内存字节来存储Person 的数据。如果您调用复制构造函数Person::Person(const Person&amp;);,代码只知道Person 的数据成员——而不是任何派生类型的数据成员——因此“切片”Woman 对象以仅复制构成Person 的数据成员。没有空间放hotness,也没有复制。


Person* p = new Person(20);
p->Print();
*p = w;
p->Print();

20
复制作业
24

我认为第二次调用 p->Print() 会调用 Woman::Print(),因为 p 指向的是一个女人,但事实并非如此。知道为什么吗?来自 Person 的复制分配被调用,我认为是因为 p 是 Person*。

*p 指的是您刚刚分配的Person 对象。 new 只被告知 Person - 它无法知道您可能想要/期望/希望的额外空间,稍后可以将 Woman 的额外字段复制到其中,因此它只是为Person。当您编写*p = w; 时,它使用Person::operator=(const Person&amp;) 函数仅复制了属于Person 的字段。这不会将指向虚拟调度表的指针设置为地址Woman 的表...同样不知道Woman...这就是为什么即使像Print 这样的virtual 函数也不会稍后解析为Woman::Print


Person* p = new Woman(20, 7);
p->Print();
*p = w;
p->Print();

20 和 7
复制作业
24和7

所以我猜是因为pPerson*,所以调用了 Person 的复制分配,但没有调用 Woman 的复制分配。奇怪的是,年龄更新了,但热度值保持不变,我不知道为什么。

这里,虽然p 确实指向带有hotness 的额外数据成员的Woman,但复制仍然使用Person::operator= 完成,因此它不知道复制额外的字段。有趣的是,它确实将内部指针复制到了虚拟分派表,所以当您使用p-&gt;Print() 时,它会分派到Woman::Print


Woman* p = new Woman(20, 7);
p->Print();
*p = w;
p->Print();

20 和 7
复制作业
女人的复制作业
24 和 10

现在数字似乎是对的。

是的,因为编译器知道分配和复制Woman 的所有数据成员,其中包括指向虚拟调度表的指针和hotness


您的其余实验(删除显式定义的赋值运算符)表明,复制哪些成员以及是否/如何更新虚拟调度表指针的问题是所涉及的静态类型的基础,因此这些问题是有或没有你的实现。


所以现在我不太明白作者的意思,所以如果有人能帮助我,我将不胜感激。

他的意思是,如果有人认为他们获得了指向 Person 的指针或引用并将其复制(就像您之前的尝试一样),他们通常会不小心删除派生类 (Woman)相关成员并以一个简单的Person 对象结束,在应用程序逻辑级别,Woman 将是有意义的。通过删除这些运算符,编译器将防止这种意外的切片构造。正确的做法是提供一个clone() 函数,该函数创建一个无论动态对象类型是什么的新对象,从而允许一种“虚拟副本”。如果您搜索“克隆”,您会找到很多解释和示例。

【讨论】:

  • 赋值从不复制虚拟表指针。在分配后*pWoman 的示例中,这是因为它首先始终是Woman
【解决方案2】:

这是因为在Woman 中,您的赋值运算符采用Woman,而不是Person,而且它也不是虚拟的。虚拟方法分派不像您期望的那样工作。仅当方法签名完全匹配时才有效。

【讨论】:

    【解决方案3】:

    一次一个例子。

    int main() {
        Woman w(24, 10);
    
        Person p = w;
        p.Print();
    
        return 0;
    }
    

    对象p 不是Woman,它只是一个Person 对象。它使用复制构造函数构造并复制wPerson 基类子对象,因此具有相同的age

    虚拟覆盖是否生效并不取决于您是否有指针、引用或两者都没有。它基于对象在创建时派生最多的类型,该类型可能与指向该对象的引用或指针的类型不同。

    int main() {
        Woman w(24, 10);
    
        Person* p = new Person(20);
        p->Print();
        p = &w;
        p->Print();
    
        return 0;
    }
    

    语句p = &amp;w; 丢弃(泄漏)p 的旧值,然后使p 成为指向原始对象w 的指针,就好像您只是完成了Person* p = &amp;w;。所以在这种情况下*p 是一个Woman,同一个对象w

    int main() {
        Woman w(24, 10);
    
        Person* p = new Person(20);
        p->Print();
        *p = w;
        p->Print();
    
        return 0;
    }
    

    语句*p = w; 调用*p 的赋值运算符。但是由于*pPerson,所以使用的赋值是Person::operator=(const Person&amp;);,而不是Woman::operator=(const Woman&amp;);。因此*page 成员被重新分配,但*p 的最派生类型无法更改,仍然是Person

    int main() {
        Woman w(24, 10);
    
        Person* p = new Woman(20, 7);
        p->Print();
        *p = w;
        p->Print();
    
        return 0;
    }
    

    这一次 *p 被创建为 Woman 开始。所以对象*p 派生最多的类型是Woman,尽管表达式*p 的类型是Person。那么当你调用虚函数Print时,无论是赋值前后,都会使用最派生类型的函数覆盖,所以调用的是Woman::Print(),而不是Person::Print()

    在语句*p = w; 中,左侧的类型为Person,右侧的类型为Woman。由于(对于这些类)operator= 不是虚函数,因此调用的函数仅取决于表达式类型,因此使用的函数是Person::operator=(const Person&amp;);。如您所见,这具有更改 age 成员的效果,但不会更改对象 *photness 成员!

    int main() {
        Woman w(24, 10);
    
        Woman* p = new Woman(20, 7);
        p->Print();
        *p = w;
        p->Print();
    
        return 0;
    }
    

    这次*p 的表达式类型是Woman,所以*p = w; 调用Woman::operator=(const Woman&amp;); 并执行您可能期望的操作。

    当您开始删除 operator= 函数的定义时,请注意这与 Stroustrup 建议的删除函数不同。如果没有为类声明赋值运算符,编译器会自动生成自己的。所以删除这些声明对你的程序没有影响,除了你得到更少的输出。

    Person p = w;*p = w;(其中pPerson*)可能出乎意料的行为被称为“对象切片”。 Stroustrup 建议删除复制和赋值函数是为了避免意外编写尝试这样做的代码。如果这些声明被定义为删除,那么这两个语句都不会编译。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-17
      • 1970-01-01
      • 2020-05-14
      • 1970-01-01
      • 2019-01-25
      • 1970-01-01
      • 2022-01-09
      • 2015-10-07
      相关资源
      最近更新 更多