《C++Primer》学习笔记(13、拷贝控制)

拷贝构造函数:

如果一个构造函数的第一个参数是:自身类类型的引用,(且任何额外参数都有默认值)。

没有拷贝构造函数,或者定义了其他构造函数,则编译器,合成拷贝构造函数。

可使用合成拷贝构造函数,阻止拷贝某类类型的对象。

在合成拷贝构造函数中,类中每个成员的类型决定了它如何拷贝,该调用拷贝构造函数就调用。

直接初始化:要求编译器使用普通的函数匹配。

拷贝初始化:要求编译器,将右侧运算对象,拷贝到正在创建的对象中。

可依靠:拷贝或移动构造函数来完成。

发生情况:

1、将对象传递给非引用形参;2、函数返回一个对象,函数返回类型为非引用。3、用花括号列表初始化一个数组

 

vector接受单一大小参数的构造函数是,explicit:

虽然编译器可略过,拷贝/移动构造函数,但他们必须是存在且可访问的,也不能是private。

拷贝赋值运算符:

与处理拷贝构造函数一样,如果一个类未定义,则编译器自己合成拷贝赋值运算符。

合成的拷贝赋值运算符,可以用来禁止该类型对象的赋值。

它返回一个指向其左侧运算对象的引用。

析构函数:

不接受参数,不能被重载,给定类,只有唯一一个析构函数。

作用:释放对象在生存期分配的所有资源。在该函数中,首先执行函数体,然后销毁成员,按初始化顺序逆序销毁。

与普通指针不同,智能指针成员,在析构阶段会被自动销毁。

调用时机:变量离开作用域;对象(容器)被销毁,成员(元素)也被销毁;应用delete;临时对象;

析构函数自动运行,无须担心何时释放资源。

未定义,则编译器回定义一个,合成析构函数(可用于,阻止该类型的对象被销毁)。

成员是在,析构函数体之后,隐含的析构阶段中,被销毁的。

三/五法则:

1、若一个类需要析构函数,则几乎肯定,也需要拷贝构造函数和拷贝赋值运算符。

若使用合成的拷贝构造函数和拷贝赋值运算符,有可能析构函数会释放同个指针两次:

2、若一个类需要拷贝构造函数,则几乎,也需要一个拷贝赋值运算符

使用=default:

显示地要求,编译器生成合成的版本。

若在类内使用=default,合成的函数,将隐式地声明为内联的。

阻止拷贝:

iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。

可定义删除的函数,来阻止拷贝。

=delete必须,出现在函数第一次声明的时候。并可对任何函数指定=delete

析构函数不能是删除的成员。

若析构函数是删除了的,则不能定义该类型变量,但可动态分配这种类型的对象。

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

类的某个成员的类中会被定义为,删除的函数
析构函数(删除或不可访问)合成的析构函数、合成的拷贝构造函数、默认构造函数
拷贝构造函数(删除或不可访问)合成的拷贝构造函数
拷贝赋值运算符(删除)。或有一个const\引用成员合成的拷贝赋值运算符
有引用\const成员(类内没初始化器,且其类型未显式定义)默认构造函数

 

拷贝控制和资源管理:

管理类外资源的类,必须定义拷贝控制成员

拷贝一个像值的对象时,副本和原对象是完全独立的。(如标准库容器和string类)

拷贝一个像指针的类对象时,副本和原对象使用相同的底层数据。(如shared_ptr)

定义一个行为像的类:

一个对象赋予它自身,赋值运算符也能正确工作:

HasPtr& HasPtr::operator=(const HasPtr &rhs){
    auto newp = new string(*rhs.ps);
    delete ps;
    ps = newp;
    i = rhs.i;
    return *this;
}

定义行为像类指针的类:

可使用shared_ptr来管理类中的资源。

若想直接管理资源,则使用引用计数。可保存在动态内存中。

析构函数的定义:

赋值运算符定义:

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

 

交换操作:

void swap(Foo &lhs,Foo &rhs){
    using std::swap;
    swap(lhs.h,rhs.h);
}

如果存在类型特定的swap版本,swap调用会与之匹配。

自己编写的swap函数:

ps:swap的存在就是为了优化代码,因此要将函数声明为inline函数。

ps:参数不是一个副本,因此传参时会发生拷贝。拷贝HasPtr操作,会分配一个该对象的string副本。函数销毁时,rhs会释放掉交换出来的string的内存。这个函数也天然地确保,自赋值的安全。

 

拷贝控制示例:

资源管理,并不是一个类需要定义自己的,拷贝控制成员的唯一原因。

设计示例思路:

1、析构函数和拷贝赋值运算符,都必须从包含一条Message的所有Folder中删除它。

2、拷贝构造函数和拷贝赋值运算符,都要将一个Message添加到给定的一组Folder中。

 

动态内存管理类:

某些类需要自己进行内存分配,也就是必须定义自己的拷贝控制成员来管理所分配的内存。

class StrVec {
public:
    StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}

    StrVec(const StrVec &);

    StrVec &operator=(const StrVec &);

    ~StrVec();

    void push_back(const std::string &);//拷贝元素

    size_t size() const { return first_free - elements; }

    size_t capacity() const { return cap - elements; }

    std::string *begin() const { return elements; }

    std::string *end() const { return first_free; }

private:
    static std::allocator<std::string> alloc;

    void chk_n_alloc() {
        if (size() == capacity())
            reallocate();
    }

    std::pair<std::string *, std::string *> alloc_n_copy(const std::string *, const std::string *);

    void free();//销毁元素并释放内存

    void reallocate();//获得更多内存,并拷贝已有内存

    std::string *elements;//指向数组首元素的指针

    std::string *first_free;//指向数组第一个空闲元素的指针

    std::string *cap;//指向数组尾后位置的指针

};

void StrVec::push_back(const std::string &s) {
    chk_n_alloc();
    alloc.construct(first_free++, s);
}

//两个指针分别指向新空间的,开始位置和拷贝的尾后的位置
std::pair<std::string *, std::string *> StrVec::alloc_n_copy(const std::string *b, const std::string *e) {
    auto data = alloc.allocate(e - b);
    //first:指向分配的内存的开始位置; second:指向最后一个构造元素之后的位置
    return {data, uninitialized_copy(b, e, data)};
}

void StrVec::free() {
    if (elements) {
        for (auto p = first_free; p != elements;)
            alloc.destroy(--p);//运行string的析构函数
        alloc.deallocate(elements, cap - elements);
    }
}

StrVec::StrVec(const StrVec &s) {
    auto newdata = alloc_n_copy(s.begin(), s.end());
    elements = newdata.first;
    first_free = cap = newdata.second;
}

StrVec::~StrVec() {
    free();
}

StrVec &StrVec::operator=(const StrVec &rhs) {
    auto data = alloc_n_copy(rhs.begin(), rhs.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

//元素移动完后,string成员不再管理它们曾经指向的内存
void StrVec::reallocate() {
    //将分配当前大小两倍的内存空间
    auto newcapacity = size() ? 2 * size() : 1;
    auto newdata = alloc.allocate(newcapacity);
    auto dest = newdata;//指向新数组中,下一个空闲位置
    auto elem = elements;//指向旧数组中,下一个元素
    for (size_t i = 0; i != size(); ++i) {
        //使用标准库函数move,且提供一个using声明
        alloc.construct(dest++, std::move(*elem++));
    }
    free();
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

ps:string具有类值行为,每个string对构成它的所有字符都会保存自己的一份副本。

可以想象,string的移动构造函数,进行了指针的拷贝,而不是字符分配内存空间,然后拷贝字符。

 

对象移动:

IO类或unique_ptr,都不包含能被共享的资源。这些对象不能拷贝但可移动。

如果对象较大,进行不必要的拷贝代价非常高。

右值引用:必须绑定到右值的引用。

重要特性:只能绑定到一个将要销毁的对象。

对于常规引用,则是左值引用,可将其绑定到,要求转换的表达式、字面常量、返回右值的表达式。

标准库move函数:

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

move:获得绑定到左值上的右值引用。

可以销毁rr1或给它重新赋值,但不能再使用它的值。

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

ps:移后原对象会被销毁。

需显式地,告诉标准库,移动构造函数可以安全使用,将函数标记为弄except来做到这一点。

移动赋值运算符:

StrVec &StrVec::operator=(StrVec &&rhs) noexcept{
    //直接检测自赋值
    if(this != &rhs){
        free();//释放已有元素
        elements = rhs.elements;//从rhs接管资源
        first_free = rhs.first_free;
        cap = rhs.cap;
        //将rhs置于可析构状态
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

ps:先释放左侧运算对象所使用的内存,并接管给定对象的内存。

 

需主要的细节:

1、在移动操作之后,移后源对象必须保持有效的可析构的状态,但用户不能对其值进行任何假设。StrVec的移动操作,是通过,将移后源对象,的指针成员,置为nullptr来实现。

2、只当类定义了自己的拷贝成员,且它的所有数据成员都有移动成员,编译器才会为这个类,合成移动成员。

3、合成的移动操作,被定义为删除的函数:

一个Y类型的类成员,它定义了,自己的拷贝构造函数,但未定义自己的移动构造函数。某个类包含Y类型数据成员,则这个类的合成移动操作,被定义为删除的函数。

4、定义了移动操作的成员的类,必须要定义自己的拷贝操作,否则,合成的拷贝操作,将被定义为删除的。

5、如果一个类有,拷贝控制成员,而没有移动控制成员,则其对象是,通过拷贝成员来"移动"的。

6、单一的赋值运算符,实现了拷贝和移动赋值运算符两种功能:

上述,赋值运算符,有一个非引用参数,因此,会发生拷贝初始化。而拷贝初始化,要么使用拷贝构造函数,要么使用移动构造函数。

第一个赋值,拷贝初始化时,使用拷贝构造函数。第二个赋值,拷贝初始化时,则调用移动构造函数。

7、一个类要是定义了,任何一个拷贝操作,则就应该定义所有五个操作。

 

Message类的移动操作:

ps:从m的folders中,删除掉m,然后在folders中加入,本msg。

移动构造函数:

ps:contents直接调用string的移动构造函数,folders就直接删除m,然后加入,在新地址的Message

移动赋值构造函数:

ps:remove_from_Folders,要销毁左侧运算对象,就得从左侧对象的folders中,移除掉指向本Message的指针。

 

移动迭代器:

通过改变,给定迭代器的解引用运算符,来适配此迭代器。解引用运算符,会生成一个右值引用。

可调用标准库的make_move_iterator函数,将一个普通迭代器,转换为一个移动迭代器。

ps:对于转换后的输入序列,当解引用时,会生成一个右值引用,意味着,construct将使用,移动构造函数来构造元素。

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值