来自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
一切似乎都很好。
所以在这一点上我无法理解作者的意思,所以如果有人能帮助我,我会很感激。
感谢。
嗜铬细胞。
答案 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
,它也不是虚拟的。虚拟方法调度不像您期望的那样工作。仅当方法签名完全匹配时才有效。