# 拷贝控制与移动语义

> [!info] 学习导航
> 返回：[[C++ 学习指南]]
> 前置知识：[[计算机系/C++/06 类与对象基础|类与对象基础]]

对象传递设计对象的复制、复制、转移和销毁，通过类拷贝控制函数

拷贝控制函数由类的拷贝构造、拷贝复制、移动构造、移动赋值和析构函数组成

C++传递对象的地址时，形参本质上时实参的别名，实参是对象名，实参和形参是同一个对象。

```cpp
Cset Union(const Cset &rhs)
C=A.Union(B)
```

C++函数传递对象的地址，形参是类指针类型，实参是对象低地址，形参指向实参代表的uix

```cpp
Cset Uion (const Cset * pSet)
C=A.Union(&B)
```

C++传递对象数组时，形参实际是实参掉膘数组的其实地址

C++函数出纳你的对象的复制副本，实参是对象名，形参是在运行栈上根据实参对象新复制构造的对象，构造完成后，形参和实参是两个独立的对象，形参变化时，实参不变

```cpp
Void dosomething (Csample obj);
Dosomething(obj);
```

函数的返回值也有返回对象引用、对象指针和对象值三种类型。对于局部对象，由于函数执行完毕后局部对象会析构，因此不可返回局部对象的引用或指针

当函数返回对象值类型，避难一起会通过复制构造一个匿名对象（副本对象）返回，因此函数返回值具有对象类型时，函数可以返回局部对象

然后根据这个副本对象再次（复制构造）一个新对象

可能还需要显示根据对象A复制构造一个新对象B

Csample B（A）

```cpp
Csample B{A}
```

Csample B=A

成员子对象也会复制构造

系统会提供默认的拷贝构造函数和拷贝赋值函数

合成复制构造函数实际效果时逐个成员数据的复制


类似也需要队对象赋值

CSAmple A.B

```cpp
B=A
```

对象的赋值运算符是通过赋值运算符完成的

如果一个类所有数据类型成员都是内置了数据类型、固定大小数组或类类型，无需特殊处理


## 拷贝构造函数

是一种特殊的构造函数，它在创建对象时，是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于：

- 通过使用另一个同类型的对象来初始化新创建的对象。
- 复制对象把它作为参数传递给函数。
- 复制对象，并从函数返回这个对象。

如果在类中没有定义拷贝构造函数，编译器会自行定义一个。如果类带有指针变量，并有动态内存分配，则它必须有一个拷贝构造函数。

- 确保 **display** 内的 **obj** 和 **main** 中的 **line** 的 **ptr** 指向**不同的内存地址**（避免浅拷贝问题）。

在这里，**obj** 是一个对象引用，该对象是用于初始化另一个对象的。


拷贝构造函数的最常见形式如下：

```cpp
classname (const classname &obj) { // 构造函数的主体 }
Line::Line(const Line &obj)
{
  cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
  ptr = new int;
  *ptr = *obj.ptr; // 拷贝值
}
void display(Line obj)
{
  cout << "line 大小 : " << obj.getLength() <<endl;
}
// 程序的主函数
int main( )
{
  Line line(10);
  display(line);
  return 0;
}
```

因为obj是通过值传递的，因此直接通过拷贝构造函数初始化形参 obj。

```cpp
// 程序的主函数
int main( )
{
  Line line1(10);
  Line line2 = line1; // 这里也调用了拷贝构造函数
  display(line1);
  display(line2);
  return 0;
}
```

默认的拷贝构造函数实现的只能是浅拷贝，即直接将原对象的数据成员值依次复制给新对象中对应的数据成员，并没有为新对象另外分配内存资源。

这样，如果对象的数据成员是指针，两个指针对象实际上指向的是同一块内存空间。

在某些情况下，浅拷贝回带来数据安全方面的隐患。

**什么情况使用拷贝构造函数：**

类的对象需要拷贝时，拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数：

- （1）一个对象以值传递的方式传入函数体
- （2）一个对象以值传递的方式从函数返回
- （3）一个对象需要通过另外一个对象进行初始化。

函数参数为 对象的引用时，不会调用拷贝构造函数:

## 拷贝赋值

就像其他运算符一样，您可以重载赋值运算符（ = ），用于创建一个对象。

下面的实例演示了如何重载赋值运算符。

Cset & operator =（const Cset &rhs）

赋值运算符完成后返回对象的引用，使我们可以连续赋值

```cpp
   void operator=(const Distance &D )
   {
     feet = D.feet;
     inches = D.inches;
   }
```

D1=D2等同于D1.operator=(D2)

```cpp
  Colour& operator =(const Colour& right)
  {
    if (this == &right)
    {
      return *this;
    }
    this->r = right.r;
    this->g = right.g;
    this->b = right.b;
    return *this;
  }
```

## 移动赋值


A事后无需使用，先释放B对象的链表，再将A对象转移给B对象，A对象指针置nullptr

消失时析构函数不再释放链表

C++右值引用&&与原赋值引用&左值引用尽心区分 无const修饰


```cpp
// 移动赋值运算符
  String& operator=(String&& other) noexcept {
    if (this != &other) {
      delete[] data; // 释放当前资源
```

​

```cpp
// 窃取资源
      data = other.data;
      length = other.length;
```

​

```cpp
// 将被移动对象置为空状态
      other.data = nullptr;
      other.length = 0;
    }
    return *this;
  }
```

-  std::move将左值转换为右值引用
-  **移动构造函数不能接受const左值引用参数**
-  移动操作可以避免不必要的深拷贝
-  std::unique_ptr支持移动语义

具有普通名字的普通对象，采用拷贝构造或拷贝复制


如果需要移动构造或者移动复制

可用cstd::Move

```cpp
Cset tmp=std::move(A)
A=STD::move()B
B=std;:Move(A)
```

关于std::forward的正确使用场景是

-  将左值强制转换为右值 错误
-  **完美转发函数参数时保留值类别**
-  替代std::move用于移动语义 错误
-  用于基本类型的类型转换 错误


## 移动构造

Cset C=A.Unio(B)

Cset temp=A；

```cpp
A=B;
B=temp；
```

拷贝构造

拷贝赋值

拷贝复制

极大地降低了执行效率

A对象指针置nullptr

消失时析构函数不再是放链表

C++11用右值引用&&与原引用&（左值引用）进行区分

无const修饰

后面又noexcept声明，用来不抛出异常

移动构造函数的实现代码，注意移动构造后必须保证被移动的对象将来的正确析构，一般也应该保证被移动对象将将来可重新赋值

```cpp
class MyClass {
public:
// 移动构造函数
  MyClass(MyClass&& other) noexcept {
// 窃取other的资源
    this->data = other.data;
    this->size = other.size;
```

​

```cpp
// 将other置为有效但空的状态
    other.data = nullptr;
    other.size = 0;
  }
private:
  int* data;
  size_t size;
};
```

**被移动对象的状态要求**

1. **被移动对象**：参数**other**就是被移动的对象
2. **必须保证被移动对象能正确析构**：
   - 将被移动对象的指针成员置为**nullptr**
   - 将被移动对象的基本类型成员置为默认值(如0)
3. **应该保证被移动对象可重新赋值**：
   - 被移动后对象应该处于"空但有效"的状态
   - 这样后续可以对其赋值或调用其他成员函数而不会崩溃

**为什么复制赋值（Copy Assignment）通常比移动赋值（Move Assignment）多一个 Cset tmp(rhs)？**

在 C++ 中，**复制赋值运算符（operator=）** 和 **移动赋值运算符（operator= 带 &&）** 的实现方式不同，其中一个关键区别是：

- **复制赋值** 通常需要先创建一个临时副本（**tmp**），再交换资源，以确保异常安全（**strong exception safety**）。
- **移动赋值** 直接“窃取”资源，不需要临时副本，因为被移动的对象（**rhs**）之后不再需要其数据。


**1. 复制赋值（Copy Assignment）的标准实现**

**为什么需要 Cset tmp(rhs)？**

```cpp
Cset& operator=(const Cset& rhs) {
  if (this != &rhs) {
Cset tmp(rhs); // (1) 先构造一个临时副本
swap(tmp); // (2) 交换当前对象和副本的资源
  }
  return *this; // (3) tmp 析构时自动清理旧资源
}
```

**作用**：

1. **异常安全**：如果 **rhs** 的拷贝构造可能抛出异常（如内存分配失败），**tmp** 的构造失败不会影响当前对象的状态。
2. **资源管理**：**swap(tmp)** 保证当前对象获得新资源，而旧资源由 **tmp** 在析构时自动释放（RAII 机制）。
3. **避免自赋值问题**：即使 **this == &rhs**，也能正确工作（因为先拷贝再交换）。


**2. 移动赋值（Move Assignment）的标准实现**

**为什么不需要 tmp？**

```cpp
Cset& operator=(Cset&& rhs) noexcept {
  if (this != &rhs) {
    delete[] data; // (1) 直接释放当前资源
    data = rhs.data; // (2) 窃取 rhs 的资源
    size = rhs.size;
rhs.data = nullptr; // (3) 置空 rhs，防止重复释放
    rhs.size = 0;
  }
  return *this;
}
```

**区别**：

1. **直接窃取资源**：移动赋值无需拷贝，直接接管 **rhs** 的资源（高效）。
2. **无需异常安全**：移动操作通常标记为 **noexcept**，因为不分配新资源（仅指针交换）。
3. **被移动对象置空**：**rhs** 的资源被“掏空”，但仍需保持可析构状态（如 **nullptr**）。


**3. 关键对比**

| **特性**             | **复制赋值 (operator=(const Cset&))** | **移动赋值 (operator=(Cset&&))**   |
| -------------------- | ------------------------------------- | ---------------------------------- |
| **是否需要临时副本** | 是 (**Cset tmp(rhs)**)                | 否（直接窃取资源）                 |
| **异常安全性**       | 强异常安全（拷贝失败不影响原对象）    | 通常 **noexcept**（无分配操作）    |
| **资源管理**         | 通过 **swap** 自动清理旧资源          | 直接接管 **rhs** 的资源            |
| **自赋值处理**       | 自动处理（**tmp** 隔离）              | 需显式检查 (**if (this != &rhs)**) |

## 相关内容
- [[计算机系/C++/13 异常处理与智能指针|异常处理与智能指针]]

---

## 学习导航
- 上一篇：[[计算机系/C++/06 类与对象基础|类与对象基础]]
- 返回目录：[[C++ 学习指南]]
- 下一篇：[[计算机系/C++/08 类的高级成员与静态成员|类的高级成员与静态成员]]
