衡水市网站建设_网站建设公司_JavaScript_seo优化
2025/12/20 21:28:21 网站建设 项目流程

1. 编译器生成的版本会做什么?

对于您给出的 NamedObject 类:

template<typename T>
class NamedObject {
public:// ... 构造函数 ...
private:std::string nameValue;T objectValue;
};

如果你写了如下代码:

NamedObject<int> no1("Smallest Prime", 2);
NamedObject<int> no2(no1); // 调用 compiler-generated copy constructor

编译器生成的 copy constructor(拷贝构造函数) 会对 no1 的成员变量进行 member-wise copy(逐成员拷贝)

  1. 对于 nameValue:它是一个 std::string 类型。编译器会调用 std::string 自己的 copy constructor(拷贝构造函数),以 no1.nameValue 为参数来初始化 no2.nameValue
  2. 对于 objectValue:它是一个 int 类型(因为 Tint)。编译器会直接拷贝整数的 bits(位),就像拷贝内置类型一样。

同理,如果你使用了赋值操作:

NamedObject<int> no3("Third Prime", 5);
no3 = no1; // 调用 compiler-generated copy assignment operator

编译器生成的 copy assignment operator(拷贝赋值运算符) 会调用 std::string::operator= 来赋值 nameValue,并对 objectValue 进行整数赋值。


2. 关键陷阱:编译器何时会“罢工”?

这是 Item 5 中最重要的一点:编译器并不总是能为你生成 copy assignment operator(拷贝赋值运算符)。

如果生成的赋值操作是非法的或无意义的,编译器就会拒绝生成它,并会在你尝试赋值时报错。主要有以下两种情况:

情况 A:包含 Reference Members(引用成员)

假设我们将 NamedObject 修改如下,让 nameValue 变成一个引用:

template<typename T>
class NamedObject {
public:// name 现在通过引用传入,并且存储为引用NamedObject(std::string& name, const T& value): nameValue(name), objectValue(value) {}// ... 假设没有声明 operator= ...private:std::string& nameValue; // 这是一个引用!T objectValue;
};

现在考虑赋值:

std::string newDog("Persephone");
std::string oldDog("Satch");NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);p = s; // 这里会发生什么?

C++ 的引用在初始化后不能改变指向(不能 rebind)。

  • 编译器应该改变 p.nameValue 让它指向 s.nameValue 所指向的字符串吗?(这是非法的,引用不可变)。
  • 还是应该修改 p.nameValue 所指向的字符串的内容?(这可能不是你想要的,因为那会影响所有持有该 string 引用的对象)。

面对这种两难,C++ 的选择是:拒绝编译。如果你想支持含引用成员的类的赋值操作,你必须自己定义 operator=

情况 B:包含 const Members(常量成员)

如果 objectValueconst T

private:std::string nameValue;const T objectValue; // const 成员

在赋值 p = s 时,修改 const 成员是非法的。因此,编译器也会拒绝生成默认的赋值运算符。

情况 C:基类的赋值运算符是 Private

  • 逻辑:派生类自动生成的赋值运算符,按照惯例必须调用基类的赋值运算符来处理基类部分的数据(Base::operator=)。
  • 结果:如果基类的 operator=private 的,派生类无权调用。编译器为了避免生成无法编译通过的代码,选择直接拒绝生成派生类的 operator=

总结

  1. Empty Class 并不空:编译器会塞入默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。
  2. 默认行为:拷贝构造和拷贝赋值会执行 Member-wise copy(逐成员拷贝)
  3. 例外情况:如果类中包含 Reference members(引用成员)const members(常量成员),编译器将不会生成默认的 copy assignment operator(拷贝赋值运算符),你必须手动实现它。

补充C++11之后的规则

C++11 之后的规则更新(Item 5 的现代化补充)

在 C++11 及以后的现代 C++ 中,Item 5 的列表确实应该扩充。如果是一个“空类”或者满足特定条件的类,编译器实际上会试图生成 6 个 特殊成员函数,而不是 4 个:

  1. Default Constructor(默认构造函数)
  2. Destructor(析构函数)
  3. Copy Constructor(拷贝构造函数)
  4. Copy Assignment Operator(拷贝赋值运算符)
  5. Move Constructor(移动构造函数) —— C++11 新增
  6. Move Assignment Operator(移动赋值运算符) —— C++11 新增

⚠️ 关键区别:生成条件的“苛刻度”

虽然移动操作也被加入了“编译器自动生成”的豪华午餐,但它们的生成条件比拷贝操作要苛刻得多

1. Copy 操作(C++98 逻辑)

编译器非常“热心”。只要你自己没写 Copy 构造/赋值,编译器通常都会帮你写一个(除非遇到引用成员或 const 成员无法赋值的情况)。即使你写了析构函数,编译器依然会为你生成 Copy 操作。

2. Move 操作(C++11 逻辑)

编译器非常“害羞”和“谨慎”。只要你声明了以下任何一个函数,编译器就绝不会为你自动生成 Move 操作:

  • 你声明了 Destructor(析构函数)
  • 你声明了 Copy Constructor(拷贝构造函数)
  • 你声明了 Copy Assignment Operator(拷贝赋值运算符)
  • 你声明了 Move Constructor(移动构造函数)
  • 你声明了 Move Assignment Operator(移动赋值运算符)

这意味着,只要你稍微介入了对象的生命周期管理(比如写了个空的虚析构函数 virtual ~Base() {}),自动生成的移动语义就会立刻失效。此时,如果代码中发生了 std::move,编译器会回退(Fallback) 去调用拷贝操作(因为移动操作不存在,而拷贝操作通常接受 const T&,可以绑定到右值)。

默认生成的 Move 做什么?

如果编译器确实为你生成了移动操作,它们的行为与拷贝操作类似,只不过是 Member-wise Move(逐成员移动)

  • 对每个非静态成员变量,执行 std::move(即调用成员自己的移动构造/赋值)。
  • 对基类部分,执行基类的移动操作。

总结对比表

特性 Copy Constructor / Assignment Move Constructor / Assignment
引入时间 C++98 C++11
生成条件 宽松:只要用户没声明对应的 Copy 操作,通常就会生成(即使声明了析构函数也会生成)。 严格:仅当用户没有声明任何 Copy 操作、Move 操作或析构函数时,才会生成。
默认行为 Member-wise Copy (逐成员拷贝) Member-wise Move (逐成员移动)
回退机制 如果未生成 Move,右值会去调用 Copy

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询