[TOC]

拷贝控制

拷贝控制操作(copy control)

  1. 拷贝构造函数
  2. 拷贝赋值运算符
  3. 移动构造函数
  4. 移动赋值运算符
  5. 析构函数

[!WARNING]

image-20240709175944611

1. 拷贝构造函数

本节要点

  • 拷贝/移动控制成员(拷贝构造、拷贝赋值、移动构造、移动赋值、析构)决定类的资源管理语义。
  • 若类管理资源(裸指针、文件句柄等),应显式管理这五类函数,遵循三/五/零法则。
  • 推荐使用移动语义和 noexcept 标注以提升容器重排性能和异常安全性。
  • 拷贝并交换(copy-and-swap)提供异常安全的赋值实现,适合多数自定义资源类型。

1. 拷贝构造函数

image-20240709180122499

1.1 拷贝初始化

image-20240709180459951

拷贝初始化发生的情况:

  1. = 定义变量; std::string a = "abc";
  2. 将一个对象作为实参传递给一个非引用类型的形参
  3. 从一个返回类型为非引用类型的函数返回一个对象
  4. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

[!IMPORTANT]

image-20240709180921455

[!IMPORTANT]

三/五法则

image-20240709192239216 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然。然而无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。

[!IMPORTANT]

image-20240709194301800

[!IMPORTANT]

image-20240709195055578

[!IMPORTANT]

image-20240710112408557

// 完整的 HasPtr 示例:展示拷贝、移动、拷贝并交换、swap
class HasPtr {
public:
    HasPtr(const std::string &s = std::string())
        : ps(new std::string(s)), i(0) {}

    // 拷贝构造
    HasPtr(const HasPtr &rhs)
        : ps(new std::string(*rhs.ps)), i(rhs.i) {}

    // 移动构造
    HasPtr(HasPtr &&rhs) noexcept
        : ps(rhs.ps), i(rhs.i) { rhs.ps = nullptr; rhs.i = 0; }

    // 析构
    ~HasPtr() { delete ps; }

    // 拷贝并交换赋值(同时支持拷贝和移动)
    HasPtr& operator=(HasPtr rhs) noexcept {
        swap(*this, rhs);
        return *this;
    }

    friend void swap(HasPtr &a, HasPtr &b) noexcept {
        using std::swap;
        swap(a.ps, b.ps);
        swap(a.i, b.i);
    }

private:
    std::string *ps;
    int i;
};

// 使用示例
void example() {
    HasPtr h1("hello");
    HasPtr h2("world");
    h1 = h2;               // copy-and-swap
    h1 = std::move(h2);    // move via parameter move-constructed
}

2. 对象移动

image-20240711175949972


2.1 右值引用

image-20240711180108941

右值引用代表着:

  1. 所引用的对象将要被销毁
  2. 该对象没有其他用户

这使得我们可以自由地接管右值引用对象的资源。

[!NOTE]

std::move

  1. 显式地将一个左值转换为对应的右值引用类型

  2. [!WARNING]

    使用 std::move 后的对象称为“移后源对象”,我们承诺了像右值一样使用它,所以除了对它进行赋值或销毁外,我们将不再使用它。

  3. [!WARNING]

    image-20240711181145980


2.2 移动构造函数和移动赋值运算符

如果我们希望在自己设计的类使用移动操作,那么就需要为其定义移动构造函数和移动赋值运算符。

移动构造函数应确保:

  1. 移后源对象必须可析构有效(可赋值、可以安全地使用而不依赖其当前值)

    同样地,我们的程序也不应该依赖移后源的值,因为它是不确定的。

  2. 一旦资源完成移动,源对象必须不再指向被移动的资源。即完成了所有权的转移

//移动构造函数
StrVec::StrVec(StrVec &&s) noexcept //不抛出任何异常
  //接管 s 中的资源
  : elements(s.elements), first_free(s.first_free), cap(s.cap)
  {
    //确保第一条:销毁无害
    s.elements = s.first_free = s.cap = nullptr;
  }
//移动赋值运算符
StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
  if (this != &rhs) {
    free();
    elements = rhs.elements;
    first_free = rhs.first_free;
    cap = rhs.cap;
    rhs.elements = rhs.first_free = rhs.cap = nullptr;
  }
  return *this;
}

[!NOTE]

为什么移动构造函数的参数没有const?

拷贝构造使用const的原因是它保证源对象的状态不变,而移动构造代表着所有权的转移,类似于"窃取",移后源将失去原有资源,并且不再使用它的值。

[!NOTE]

image-20240711210333379

前提是它真的不会抛出异常


2.3 合成的移动操作

image-20240712082917133

image-20240712083240162

[!NOTE]

image-20240712083431457

[!note]

示例:为自定义类型提供哈希与相等比较器,然后传入容器构造函数。

// 假设有一个 Sales_data 类型,提供 isbn() 成员
struct Sales_data { std::string isbn() const; /* ... */ };

auto hasher = [](const Sales_data &sd) {
  return std::hash<std::string>()(sd.isbn());
};

auto eqOp = [](const Sales_data &lhs, const Sales_data &rhs) {
  return lhs.isbn() == rhs.isbn();
};

// 使用 lambda 类型作为 Hash 和 KeyEqual
std::unordered_multiset<Sales_data, decltype(hasher), decltype(eqOp)>
  bookStore(42, hasher, eqOp);

image-20240712083922399


2.4 拷贝并交换赋值运算符和移动操作

class HasPtr {
public:
    // 添加的移动构造函数
  HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) { p.ps = nullptr; }
    // 赋值运算符既是移动赋值运算符,也是拷贝赋值运算符
  HasPtr& operator=(HasPtr rhs)
  { 
    swap(*this, rhs); 
    return *this; 
}
};

当函数的形参为非引用类型时,需要创建一个新的对象来保存传递的参数。这个新的对象可以通过拷贝构造函数或移动构造函数来创建。如果提供了移动构造函数且传递的参数是右值或临时对象,编译器将使用移动构造函数;否则,将使用拷贝构造函数。

因此,该参数的拷贝初始化表现为——左值被拷贝,右值被移动

hp = hp2; //拷贝构造,hp2不变
hp = std::move(hp2); //移动构造,hp2成为移后源

巧妙之处在于拷贝并交换(copy and swap)保证了两种情况都是安全且行为正确的。

[!IMPORTANT]

image-20240712090226487

image-20240712090234472


2.5 移动迭代器

std::make_move_iterator 将一个普通迭代器转换为一个移动迭代器。

[!CAUTION]

image-20240712091915801

[!CAUTION]

image-20240712094753171


2.6 右值引用和成员函数

[!NOTE]

image-20240712100511716

2.6.1 引用限定符

引用限定符用来限定调用这个函数的对象是左值还是右值。

引用限定符可以是&或&&,分别指出this可以指向一个左值或右值,只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。

class Foo {
public:
    Foo &operator=(const Foo&) &; // 只能向可修改的左值赋值
    // Foo 的其他参数
};

Foo &Foo::operator=(const Foo &rhs) & {
    // 执行将 rhs 赋予本对象所需的工作
    return *this;
}

[!NOTE]

一个函数可以同时使用 const 和引用限定。但是引用限定符必须跟随在 const 之后:

class Foo {
public:
  Foo someMem() const &; //先const再引用限定
}

2.7 重载和引用函数

引用限定符可以区分重载版本。

[!NOTE]

如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加

class Foo {
public:
    Foo sorted() &&;
    Foo sorted() const; // 错误:必须加上引用限定符
    // Comp 是函数类型的类型别名(参见6.7节,第222页)
    // 此函数类型可以用来比较 int 值
    using Comp = bool(const int&, const int&);
    Foo sorted(Comp*);
    Foo sorted(Comp*) const; // 正确:两个版本都没有引用限定符
};

2.8 图解

拷贝构造

copy

移动构造

move


2.9 总结

移动构造实际上是一个高效的抽象,在具体实现中涉及到指针的拷贝和一些静态内存的拷贝。移动构造的关键在于尽可能避免昂贵的深拷贝操作,通过转移资源所有权(如指针)来实现更高效的对象管理。

©OZY all right reserved该文件修订时间: 2025-09-20 05:42:10

评论区 - 04_拷贝控制

results matching ""

    No results matching ""