拷贝控制2(拷贝控制和资源管理/交换操作/动态内存管理)

为了定义拷贝构造函数和拷贝赋值运算符,我们首先必须确认此类型对象的拷贝语义。通常可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针(即所谓的深拷贝和浅拷贝)

类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然

行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然

在我们使用过的标准库类中,标准库容器和 string 类的行为像一个值。shared_ptr 提供类似指针的行为。IO 类型和 unique_ptr 不允许拷贝或赋值,因此它们的行为既不像值也不像指针

行为像值

#include<bits/stdc++.h>
using namespace std;
struct Node{
public:
    Node(const string &s=string()):
        ps(new string(s)),i(0){};//使行为像值,就在构造函数里定一个默认的值
    Node(const Node&p):ps(new string(*p.ps)),i(p.i){};//拷贝构造函数
    Node& operator=(const Node&);//三五法则
    ~Node(){ delete ps;}//还要定义自己的析构函数
    ostream& print(ostream &os){
        os<<i<<" "<<*ps<<" "<<ps<<endl;
    }
private :
    string *ps;
    int i;
};
Node& Node::operator=(const Node &rhs){//定义自己的赋值运算
    auto newps=new string(*rhs.ps);//为了避免两个指针指向同一块内存
    delete ps;//释放自己旧的内存
    ps=newps;
    i = rhs.i;
    return *this;
}
int main(){
    Node s1("hello");
    Node s2=s1;
    Node s3=s2;
    s1.print(cout);
    s2.print(cout);
    s3.print(cout);
}

但是我们要注意:在重新定义赋值操作的时候,要防范自赋值的情况

Node& Node::operator=(const Node &rhs){//定义自己的赋值运算
    delete rhs.ps;//如果rhs是自身,那么ps就会成为一个空悬指针
    ps=rhs.ps;//不指向任何对象。
    i = rhs.i;
    return *this;
}

行为像指针的类:

#include<bits/stdc++.h>
using namespace std;
class Node{
public :
    Node(const string &s=string())://分配一个新的string
        ps(new string(s)),
        i(0),
        use(new std::size_t(1)) {}
//        use(new std::size_t(1)){}//直接初始化
    Node(const Node &p):ps(p.ps),i(p.i),use(p.use){//拷贝构造函数
        ++*use;
    }
    Node& operator=(const Node&);
    ~Node();
    ostream&  print(ostream &os){
        os<< *use <<" "<<i<<" "<< *ps <<" "<<ps<<endl;
        return os;
    }
private :
    string *ps;//一个指向string的指针
    int i;
    std::size_t *use;//定义一个计数器
};
Node& Node::operator=(const Node &rhs){
    ++*rhs.use;
    if(--*use==0){//如果左值的引用计数器变为0,那么就讲左边的指针指向的对象给清空
        delete ps;
        delete use;
    }
    ps=rhs.ps;
    i=rhs.i;
    use=rhs.use;
    return *this;
}
Node::~Node(){
    if(--*use==0){
        delete ps;
        delete use;
    }
}
int main(){
    Node s1("Hello");
    Node s2=s1;
    Node s3=s1;
    Node s4("word");
    s1.print(cout);
    s2.print(cout);
    s3.print(cout);
    s4.print(cout);
/*3 0 Hello 0xac14d0
3 0 Hello 0xac14d0
3 0 Hello 0xac14d0
1 0 word 0xac18c0*/
}

注意:为了实现类似于 shared_ptr 的引用计数功能,我们可以将计数器保持到动态内存中,指向相同 ps 对象的 HasPtr 也指向相同的 use 对象。 这里我们不能使用 static 来实现引用计数,因为它是属于类本身的,这意味着所有 HasPtr 类的对象中 use 值都是相等的,并且我们将无法做到给赋值运算符右侧对象 use 加一,左侧对象 use 减一:

交换操作:

库函数 swap 的实现依赖于类的拷贝构造函数和赋值运算符。如,对值语义的 HasPtr 类对象使用库函数 swap:

swap(s1, s2);的实现流程是:
HasPtr cmp = s1;
s1 = s2;
s2 = cmp;
这个过程中分配了 3 次内存,效率及其低下。理论上这些内存分配都是不必要的。我们可以只交换指针而不需要分配 string 的新副本(对于管理资源的类来讲)。

因此,除了定义拷贝控制成员,管理资源的类通常还需要定义一个名为 swap 的函数。尤其对于那些与重排元素顺序的算法一起使用的类,定义 swap 是非常重要的。这类算法在需要交换两个元素时会调用 swap。

在行为像值的类中,因为swap的实现依赖拷贝赋值函数,会多次分配内存(因为你自定义的拷贝赋值操作是要重新分配内存的)。所以使用库函数的swap就会降低性能,这是我们就需要自定义swap函数

对于行为像值的类的swap:

#include <iostream>
using namespace std;

class HasPtr{
friend void swap(HasPtr&, HasPtr&);

public:
    HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {}
    HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
    HasPtr& operator=(const HasPtr&);
    ~HasPtr(){
        delete ps;
    }

    ostream& print(ostream &os){
        os << i << " " << *ps << " " << ps;
        return os;
    }

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

HasPtr& HasPtr::operator=(const HasPtr &rhs){
    auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中
    delete ps;//释放旧内存
    ps = newp;
    i = rhs.i;
    return *this;
}

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

int main(void){
    HasPtr s1("hello");
    HasPtr s2("word", 1);

    s1.print(cout) << endl;
    s2.print(cout) << endl;

    swap(s1, s2);
    // auto cmp = s1.ps;
    // s1.ps = s2.ps;
    // s2.ps = cmp;
    // auto cnt = s1.i;
    // s1.i = s2.i;
    // s2.i = cnt;

    s1.print(cout) << endl;
    s2.print(cout) << endl;

// 输出:
// 0 hello 0x2bb1208
// 1 word 0x2bb1128
// 1 word 0x2bb1128
// 0 hello 0x2bb1208
    return 0;
}

注意:如果存在类型特定的 swap 版本,其匹配程度会优于 std 中定义的版本。如果不存在类型特定的版本,则会使用 std 中的版本(假定作用域中有 using 声明)

对于行为像指针的类来讲,自定义swap并不能优化性能。

在赋值运算符中使用 swap:(行为像值的类的例子)

定义了 swap 的类中通常用 swap 来定义它们的赋值运算符。这些运算符使用了一种名为 拷贝并交换(copy and swap) 的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

#include<bits/stdc++.h>
using namespace std;
class Node{//行为像值的类
friend void swap(Node&,Node&);
public :
    Node(const string s=string(),int a=0):
        ps(new string(s)),i(a){}
    Node(const Node &p):
        ps(new string(*p.ps)),i(p.i){};//拷贝复制函数
    Node& operator=(Node);//注意:使用拷贝并交换技术,参数不能是引用
    ~Node(){
        delete ps;//delete 删除ps所指向的对象。
    }
    ostream& print(ostream& os){
        os<<i<<" "<<*ps<<" "<<ps<<endl;
        return os;
    }
private:
    string *ps;
    int i;
};
Node& Node::operator=(Node rhs){//注意:使用交换并且赋值的技术
    swap(*this,rhs);//这个函数将rhs赋值给this,函数结束的时候,rhs(赋值前的this)被销毁
    return *this;
}
inline void swap(Node &lhs,Node&rhs){
    swap(lhs.ps,rhs.ps);
    swap(lhs.i,rhs.i);
}
int main(){
    Node s1("hello");
    Node s2("world",1);
    s1.print(cout);
    s2.print(cout);
    cout<<"----------------"<<endl;
    swap(s1,s2);//交换的是string的指针,并不是值
    s1.print(cout);
    s2.print(cout);
    cout<<"---------------"<<endl;
    s1=s2;
    s1.print(cout);//string重新分配了一次内存,并不是普通swap的三次、
    s2.print(cout);
}

注意:这个版本赋值运算符中,参数并不能是引用
使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值

#pragma once//表示在编译过程中,这个文件只会被include一次
#include<iostream>
#include<string>
#include<memory>
#include<utility>


class StrVec{
public:
    StrVec():elements(nullptr),frist_free(nullptrs),cap(nullptr);
    //三五法则
    StrVec(const StrVec&);//拷贝构造函数
    StrVec& operator=(const StrVec&);//重载运算符
    ~StrVec();//析构函数、

    void push_back(const string&)//拷贝元素
    size_t size()const {return frist_free-elements;}
    size_t capacity()const {return cap-elements;}//容量的大小
    string *begin()const {return elements;}
    string *end()const {return frist_free;}


private:
    string *elements;//指向分配的内存中的首元素
    string *frist_free;//指向最后一个实际元素之后的位置
    string *cap;//指向分配的内存之后的位置

    static allocator<string> alloc;//??为什么静态的?
    void chk_n_alloc(){  if(size()==capacity()reallocate;)}//如果没有空余位置,就分配新的空间
    pair<string*,string *> alloc_n_copy(const string *,const string *);//分配内存,并且拷贝范围元素
    void free();//释放内存
    void reallocate();//重新分配内存
};

void StrVec::push_back(const string&s){
    chk_n_alloc();//检查内存是否足够
    alloc.construct(frist_free++,s);//分配一个元素,记得递增这个元素
}
//分配空间保存给定空间的元素
pair<string*,string*> StrVec::alloc_n_copy(const string *s1,const string *s2){
    auto data = a.alloc(s2-s1);//
    return {data,uninitialized_copy(s1,s2,data)};//返回目的(递增后的)位置迭代器
}
//copy和uninitialized_copy的不同,是依次调用拷贝构造函数
void StrVec::free(){//释放空间
    if(elements){//不能给destroy一个空的
        for(auto p=frist_free;p!=elements;){
            alloc.destory(--p);//对p所指向的对象执行析构函数
        }
        alloc.deallocate(elements,cap-elements);
        elements=frist_free=cap=nullptr;
    }
}
//调用调用分配空间复制的函数,
StrVec::StrVec(const StrVec&s){//拷贝构造函数,
    auto newdata=alloc_n_copy(s.begin(),s.end());
    elements=newdata.frist;
    frist_free=cap=newdata.second;
}
StrVec::~StrVec(){
    free();//先释放空间,析构函数再delete指针。
}
StrVec& StrVec::operator=(const StrVec& s){//赋值构造函数
    auto data=alloc_n_copy(s.begin(),s.end());
    free();
    elements=data.frist;
    cap=frist_free=data.second;
    return *this;
}
//释放旧的内存
void StrVec::reallocate(){
    auto newcapacity()=size()?size()*2:1;
    auto newdata=alloc.allocate(newcapacity);//构造空间
    auto dest=newdata;      //指向新内存
    auto elem=elements;         //指向旧内存
    for(size_t i=;i!=size();i++) alloc.construct(dest++,std::move(*elem++));//construct的第二个参数决定了使用哪个构造函数
    free();
    elements=newdata;
    frist_free=dest;
    cap=elements+newcapacity;//空间的末尾
}


/*
总结:
    资源管理类,有三个指针成员,元素前,元素后,以及内存后
    添加元素的函数要每次检查内存是否充足,内存充足就直接添加,不足就重新释放空间,移动所有元素,
    要定义自己的析构函数(释放allocate)分类的空间
    释放空间函数
    拷贝构造函数:通过allocate直接开辟空间,然后进行拷贝。
    移动函数:切记要使用std::move而不是move
*/

StrVec.h:

#pragma once

#include <iostream>
#include <memory>
#include <utility>
#include <initializer_list>

class StrVec{
public:
    //默认构造函数
    StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}//allocator成员进行默认初始化
    StrVec(const std::initializer_list<std::string>&);
    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;
    }

    void reserve(const size_t&);//分配指定大小的空间并将原来的元素拷贝到新空间
    void resize(const size_t&, const std::string &s = "");//使得容器为指定大小但不减小容量

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();//获得更多内存并拷贝已有元素
    void reallocate(const size_t&);

    std::string *elements;//指向数组首元素的指针
    std::string *first_free;//指向数组第一个空闲元素的指针
    std::string *cap;//指向数组尾后位置的指针
    
};

StrVec.cpp:

#include "StrVec.h"
#include <iostream>
using namespace std;

allocator<std::string> StrVec:: alloc;

void StrVec::push_back(const string &s){
    chk_n_alloc();//确保有空间容纳新元素
    alloc.construct(first_free++, s);//在原先first_free位置构造一个值为s的新元素
}

pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e){
    auto data = alloc.allocate(e - b);//分配大小等于给定范围元素数目
    //data指向分配的内存的开始位置
    return {data, uninitialized_copy(b, e, data)};//uninitialzed_copy返回最后一个构造元素之后的位置
}

void StrVec::free(){
    if(elements){//不能传递一个空指针给deallocate
        for(auto p = first_free; p != elements;){
            alloc.destroy(--p);//销毁对象
        }
        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(const std::initializer_list<std::string> &il){
    auto newdata = alloc_n_copy(il.begin(), il.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());//为了避免自赋值时出错先开辟内存并拷贝rhs
    free();//释放原有内存
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

void StrVec::reallocate(){
    auto newcapacity = size() ? 2 * size() : 1;
    auto newdata = alloc.allocate(newcapacity);//分配新内存

    //将旧的数据移动到新内存中
    auto dest = newdata;//指向新数组中下一个空闲位置
    auto elem = elements;//z指向旧数组中下一个位置
    for(size_t i = 0; i !=size(); ++i){
        alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string
    }
    free();//释放旧内存
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

void StrVec::reallocate(const size_t &newcapacity){
    auto newdata = alloc.allocate(newcapacity);//分配新内存

    //将旧的数据移动到新内存中
    auto dest = newdata;//指向新数组中下一个空闲位置
    auto elem = elements;//z指向旧数组中下一个位置
    for(size_t i = 0; i !=size(); ++i){
        alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string
    }
    free();//释放旧内存
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

void StrVec::reserve(const size_t &newcapacity){//分配不小于newcapacity的空间
    if(newcapacity > size()) reallocate(newcapacity);
}

//使得容器为指定大小但不减小容量
void StrVec::resize(const size_t &newcapacity, const std::string &s){
    if(newcapacity > size()){
        for(int i = size(); i < newcapacity; i++){
            push_back(s);
        }
    }
    while(newcapacity < size()){
        --first_free;
    }
}

main.cpp

#include <iostream>
#include "StrVec.h"
using namespace std;
    
int main(void){
    StrVec s({"gg", "yy"});
    s.push_back("hello");
    s.push_back("word");
    for(const auto &indx : s){
        cout << indx << " ";
    }
    cout << endl;

    s.reserve(100);//给s分配能容纳100个元素的空间
    s.resize(10, "jf");

    for(const auto &indx : s){
        cout << indx << " ";
    }
    cout << endl;

    s.resize(2);
    for(const auto &indx : s){
        cout << indx << " ";
    }
    cout << endl;

// 输出:
// gg yy hello word
// gg yy hello word jf jf jf jf jf jf
// gg yy

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值