C ++为什么要压缩默认的复制构造函数?

时间:2014-03-18 00:20:24

标签: c++ pointers copy-constructor

来自Bjarne Stroustrup的The C ++ Programming Language第4版:

3.3.4。抑制操作

使用层次结构中的类的默认副本或移动通常是一种灾难: 只给出一个指向基数的指针,我们根本不知道派生的成员是什么 class有(§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

这对我来说有点意外,作为一个菜鸟,但随后意识到由于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-&gt; 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的副本分配,但不是女士的副本分配。很奇怪,年龄得到了更新,但热度的价值保持不变,我不明白为什么。

再试一次:

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

一切似乎都很好。

所以在这一点上我无法理解作者的意思,所以如果有人能帮助我,我会很感激。

感谢。

嗜铬细胞。

3 个答案:

答案 0 :(得分:6)

Woman w(24, 10);

Person p = w;
p.Print();
  

24

     

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

正确

  

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

不,它没有。

  

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

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


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

20
  复制作业
  24

     

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

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


Woman::Print
  

20和7
  复制作业
  24和7

     

所以我想因为Person* p = new Woman(20, 7); p->Print(); *p = w; p->Print(); p,所以调用了人物的副本分配,而不是女人的副本分配。很奇怪,年龄得到了更新,但热度的价值保持不变,我不明白为什么。

此处,虽然Person*确实指向p Woman的额外数据成员,但该副本仍然使用hotness完成,因此它不知道复制额外的字段。有趣的是,它会将内部指针复制到虚拟调度表,因此当您使用Person::operator=时,它会调度到p->Print()


Woman::Print
  

20和7
  复制作业
  复印件分配给女人   24和10

     

现在数字似乎是正确的。

是的,因为编译器知道分配和复制Woman* p = new Woman(20, 7); p->Print(); *p = w; p->Print(); 的所有数据成员,其中包括指向虚拟调度表和Woman的指针。


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


  

所以在这一点上我无法理解作者的意思,所以如果有人能帮助我,我会很感激。

他所说的是,如果有人认为他们正在获取指针或对hotness的引用并将其复制(如在之前的尝试中),那么他们通常会意外删除派生类({{ 1}})相关成员,最后是一个简单的Person对象,在应用程序逻辑级别Woman是有意义的。通过删除这些运算符,编译器将防止这种意外切片构造。正确的做法是提供一个Person函数,它创建一个动态对象类型的新对象,允许一种“虚拟副本”。如果你搜索“克隆”,你会发现很多解释和例子。

答案 1 :(得分:3)

一次一个例子。

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

    Person p = w;
    p.Print();

    return 0;
}

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

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

age

语句int main() { Woman w(24, 10); Person* p = new Person(20); p->Print(); p = &w; p->Print(); return 0; } 丢弃(泄漏)p = &w;的旧值,然后使p成为指向原始对象p的指针,就像您刚刚完成的一样w。因此,在这种情况下,Person* p = &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*p,因此使用的分配是Person,而不是Person::operator=(const Person&);。因此Woman::operator=(const Woman&);成员age被重新分配,但*p的派生程度最高的类型无法更改且仍为*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&);成员,但不能更改对象age的{​​{1}}成员!

hotness

这次表达式*p的类型为int main() { Woman w(24, 10); Woman* p = new Woman(20, 7); p->Print(); *p = w; p->Print(); return 0; } ,因此*p会调用Woman并执行您可能期望的事情。

当您开始删除*p = w;函数的定义时,请注意,与Stroustrup建议的删除函数不同。如果未为类声明赋值运算符,则编译器会自动生成自己的赋值运算符。因此,除了输出较少的事实之外,删除这些声明对您的程序没有任何影响。

Woman::operator=(const Woman&);operator=(其中Person p = w;*p = w;)的意外行为被称为“对象切片”。 Stroustrup建议删除复制和赋值函数,以避免意外编写试图执行此操作的代码。如果这些声明被定义为已删除,则这两个语句都不会编译。

答案 2 :(得分:1)

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

相关问题