Skip to content

拷贝控制

约 2093 个字 140 行代码 预计阅读时间 12 分钟

拷贝、赋值与销毁

拷贝构造函数

  • 第一个参数是自身类类型的引用,所有其他参数均有默认值

  • 逐个拷贝非static成员;内置类型成员直接内存拷贝;类类型成员,调用拷贝构造函数拷贝

C++
string dots(10, '.');   // 直接初始化
string s(dots);         // 直接初始化
string s2 = dots;       // 拷贝初始化
string null_book = "99" // 拷贝初始化 
  • 拷贝初始化
  • “=”
  • 一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类的成员
  • 容器调用insert或push操作时
  • 拷贝构造函数的形参必须是引用类型

  • 编译器可以绕过拷贝构造函数

C++
string null_book = "9-99";
// 改写为
string null_book("9-99"); // 编译器略过了拷贝构造函数

拷贝赋值运算符

  • 赋值运算符通常返回一个指向其左侧运算对象的引用

合成拷贝赋值运算符

  • 类未定义拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符
  • 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员

析构函数

  • 释放对象使用的资源,销毁对象的非static数据成员
  • 不接受参数,不能被重载

完成工作

  • 执行函数体;销毁成员,按初始化顺序的逆序销毁
  • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象

调用时机

  • 对象被销毁,自动调用其析构函数
  • 指向一个对象的引用或指针离开作用域,析构函数不会执行(delete 对象指针)
C++
bool fcn(const Sales_data *trans, Sales_data accum)
{
    Sales_data item(*trans), item2(accum);
    return item1.isbn() != item2.isbn();
}
// 调用3次析构函数
// item1, item2, accum
// trans是指针,不执行析构函数

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作
  • 需要拷贝的类也需要赋值操作

使用=default

  • 显示要求编译器生成合成版本的函数
C++
class Sales_data
{
public:
    Sales_data()=default;
    Sales_data(const Sales_data&)=default;
    Sales_data& operator=(const Sales_data&);
    ~Sales_data()=default;
};
Sales_data& Sales_data::operator=(const Sales_data&)=default;

阻止拷贝

  • 定义删除的函数:=delete;我们可以对任何函数指定=delete

  • 不能删除析构函数

合成的拷贝控制成员可能是删除的

  • 一个类有数据成员不能默认构造、拷贝、复制或销毁,对应的成员函数将被定义为删除的
  • 一个类有const成员,不能使用合成的拷贝赋值运算符;合成的试图赋值所有成员
  • 一个类有引用成员,不使用合成的拷贝赋值运算符;合成的拷贝赋值运算符只是修改引用指向的对象的值,没有改变引用对象;

private拷贝控制

  • 阻止拷贝:将拷贝赋值运算符,拷贝构造函数声明为private的;
  • 为了放置友元使用,只声明但不定义

拷贝控制和资源管理

  • 定义一个类的拷贝操作,类的行为像一个值或像一个指针
  • 类的行为像一个值:
  • 拥有自己的状态;副本和原对象是完全独立的,改变副本不会对原对象有任何影响
  • 类行为像指针:
  • 共享状态;副本和原对象使用相同的底层数据
  • string类和标准库容器类的行为像一个值;shared_ptr类提供类似指针的行为
  • IO类型和unique_ptr不允许拷贝和赋值,行为既不像值也不像指针

行为像值的类

  • 定义一个拷贝构造函数;完成string的拷贝;而不是指针的拷贝
  • 定义一个析构函数释放string
  • 定义一个拷贝赋值运算符释放当前string,并从右侧运算对象拷贝string
C++
class HasPtr
{
public:
    HasPtr(const std::string &s = std::string()): \
        ps(new std::string(s), i(0)){}
    HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i){}
    HasPtr& operator=(const HasPtr &);
    ~HasPtr(){delete ps;}
private:
    std::string *ps;
    int i;
};

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps);
    delete ps;
    ps = newp;
    i = rhs.i;
    return *this;
}
  • 赋值运算符:先将右侧运算对象拷贝到一个局部临时对象中;将临时对象中的成员拷贝到左侧运算对象成员中

定义行为上像指针的类

  • 最好方法是使用shared_ptr管理类中的资源
  • 希望直接管理资源,使用引用计数

引用计数

  • 初始化对象外,每个构造函数需要创建一个引用计数
  • 拷贝构造函数不分配新的计数器;递增共享的计数器
  • 析构函数递减计数器
  • 拷贝赋值运算符递增右侧对象计数器,递减左侧对象计数器
  • 将计数器保存在动态内存中;拷贝指向计数器的指针
C++
class HasPtr {
public:
    HasPtr(const string& s = string()) :
        ps(new string(s)), i(0), use(new size_t(1)){}
    HasPtr(const HasPtr& p): ps(p.ps), i(p.i), use(++*use);
    HasPtr& operator=(const HasPtr&);
    ~HasPtr;
private:
    string *ps;
    int i;
    size_t *use;
};

HasPtr::~HasPtr() {
    if(--*use == 0) {
        delete ps;
        delete use;
    }
}

HasPtr& HasPtr::operator=(const HasPtr& rhs) {
    ++*rhs.use;
    if(--*use == 0) {
        delete ps;
        delete use;
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use;
    return *this;
}

交换操作

  • 默认swap交换两个对象的非静态成员;v1两次被拷贝,这些拷贝不是必要的
C++
HasPtr tmp = v1;
v1 = v2;
v2 = tmp;
  • 我们有时希望swap交换指针,而不是分配新副本;可以编写我们自己的swap函数
C++
friend void swap(HasPtr&, HasPtr&);

inline void swap(HasPtr& lhs, HasPtr& rhs) {
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
}

赋值运算符中使用swap

  • 拷贝并交换:(参数按值传递,进行拷贝)
  • rhs是右侧对象的一个副本;使用swap交换*this和rhs的数据成员;是异常安全的,可以正确处理自赋值
C++
// rhs是按值传递的,HasPtr的拷贝构造函数
HasPtr& HasPtr::operator=(HasPtr rhs)
{
    swap(*this, rhs);
    return *this; // rhs被销毁,delete了rhs中的指针
}

拷贝控制(见书中代码)

动态内存管理类

  • 某些类需要自己进行内存分配;一般定义自己的拷贝控制成员来管理分配的内存
  • 重新分配内存的过程中移动而不是拷贝元素
  • 使用allocator获得原始内存
  • 三个指针成员指向元素使用的内存
  • elements:分配内存中的首元素
  • first_free:最后一个实际元素的位置
  • cap:分配内存末尾的位置

移动构造和std::move

  • 使用移动构造函数,string管理的内存不会被拷贝;会被构造的新的元素接管内存的所有权

对象移动

右值引用

  • 必须绑定到右值的引用;只能绑定到将要销毁的对象
  • const左值引用可以绑定到右值上
  • 所引用的对象将要被销毁;对象没有其他用户
  • 使用noexcept告知标准库我们的移动构造函数可以安全使用
  • 移动后源对象必须可析构:将移后源对象指针赋值nullptr

move函数

  • 将一个左值转换为相应的右值引用类型

  • 内部实现:使用引用折叠和static_cast强制转换成右值引用类型

  • 使用remove_reference

    C++
    template<typename _Tp>
    struct remove_reference
    { typedef _Tp   type; };
    
    // 特化版本
    template<typename _Tp>
    struct remove_reference<_Tp&>
    { typedef _Tp   type; };
    
    template<typename _Tp>
    struct remove_reference<_Tp&&>
    { typedef _Tp   type; };
    

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

  • 完成资源移动后,以哦对那个构造函数确保移动后源对象不再指向被移动的资源
C++
StrVec::StrVec(StrVec &&s) noexcept \
: 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;
}
  • 移动后源对象必须可析构;将源对象的指针置为空

合成移动操作

  • 只有一个类没有定义任何自己版本的拷贝控制成员;且每个非static成员都可以移动时才合成移动构造函数和移动赋值运算符
  • 如果用户=default,但是有成员不能移动,编译器将移动操作定义为=delete

合成移动操作定义为删除

  • 定义了移动构造或移动赋值运算符的类必须定义自己的拷贝操作,否则这些成员默认=delete
  • 类成员有const或引用,定义为delete
  • 析构=delete或不能访问,定义为delete
  • 移动构造或移动赋值运算符定义为删除或不可访问

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

C++
HasPtr(HasPtr&& p) noexcept:ps(p.ps), i(p.i) {p.ps = 0;}
// 既是移动赋值又是拷贝赋值
HasPtr& operator=(HasPtr rhs) {
    swap(*this, rhs);
    return *this;
}

移动迭代器

  • make_move_iteraotr:将一个普通迭代器转换为一个移动迭代器
  • 不要随意使用移动操作
  • 只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问,才能将移动迭代器传递

右值引用和成员函数

C++
void push_back(const X&); // 绑定到任意类型的X
void push_back(X&&);      // 绑定到类型X的可修改右值

右值和左值引用成员函数

  • 在参数列表后面放置一个引用限定符:

  • 限制某些方法只能左值对象调,或者只能右值对象调

C++
// 对象是右值,说明没有其他用户,可以改变对象
Foo Foo:sorted() && {
    sort(data.begin(), data.end());
    return *this;
}
  • 表示this指向一个左值或右值

  • 引用限定符必须跟随在const限定符之后

  • 如果一个成员函数有引用限定符,具有相同参数列表的所有版本必须有引用限定符

C++
Foo Foo::sorted() const & {
    Foo ret(*this);
    return ret.sorted();
    // 导致递归调用左值sorted();产生递归循环
}

左值OR右值

  • have identity and cannot be moved from are called lvalue expressions;
  • have identity and can be moved from are called xvalue expressions;
  • do not have identity and can be moved from are called prvalue ("pure rvalue") expressions;
  • do not have identity and cannot be moved from are not used[6].