C++基础的不能再基础的学习笔记——拷贝控制示例(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fancynece/article/details/79374820

拷贝控制示例

我们在之前的博客上了解到了,需要管理资源的类通常要定义拷贝控制操作、swap操作。http://blog.csdn.net/fancynece/article/details/79346314

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

一些类需要拷贝控制成员的帮助来进行②簿记工作或其他操作

我们通过一个例子来说明,簿记工作需要拷贝控制成员。

定义两个类Message和Folder,分别表示电子邮件消息和消息目录。

  • 一个Message可以出现在多个Folder中
  • 每个Message只有一个副本

我们在每个Message中,用set记录下包含它的Folder的指针;在每个Folder中,用set记录下包含的所有Message的指针。

  • Message提供save和remove操作,用来将对象保存到Folder中或从Folder删除。
  • 当我们拷贝一个Message时,需要拷贝set,并且在所有包含它的Folder的set中添加该Message的指针
  • 当我们销毁一个Message时,需要在所有包含它的Folder的set中删除该Message的指针
  • 当我们进行赋值操作时,左侧Message的内容被销毁,右侧Message的内容拷贝到左侧。
  • Folder类提供addMsg和remMsg操作,用来添加或删除Message对象
class Message {

    friend class Folder;
    friend void swap(Message &lm, Message &rm);

public:

    explicit Message(const string &str = "") :contents(str) {}
    Message(const Message&);
    Message& operator=(const Message&);
    ~Message();

    void save(Folder &f)
    {
        folders.insert(&f);
        f.addMsg(this);
    }
    void remove(Folder &f)
    {
        folders.erase(&f);
        f.remMsg(this);
    }

private:

    string contents;        //内容
    set<Folder*> folders;

    void add_to_Folders(const Message&);
    void remove_from_Folders();
};


Message::Message(const Message &m):contents(m.contents),folders(m.folders)
{
    add_to_Folders(m);
}

Message & Message::operator=(const Message &m)
{
    remove_from_Folders();

    contents = m.contents;
    folders = m.folders;

    add_to_Folders(m);

    return *this;
}

Message::~Message()
{
    remove_from_Folders();
}

void Message::add_to_Folders(const Message &m)
{
    for (auto c : m.folders)
        c->addMsg(this);
}

void Message::remove_from_Folders()
{
    for (auto c : folders)
        c->remMsg(this);
}


void swap(Message & lm, Message & rm)
{
    using std::swap;

    for (auto c : lm.folders)
        c->remMsg(&lm);
    for (auto c : rm.folders)
        c->remMsg(&rm);

    swap(lm.contents, rm.contents);
    swap(lm.folders, rm.folders);

    for (auto c : lm.folders)
        c->addMsg(&lm);
    for (auto c : rm.folders)
        c->addMsg(&rm);
}

动态内存管理类

某些类需要在运行时分配可变大小的内存空间。

这种类通常可以一般也都会使用标准库容器来保存它们的数据。但是某些类需要自己进行内存分配,这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存

我们将实现一个标准库vector类的简化版本StrVec,不适用模板,并且只用于string。

StrVec类的设计

我们已经知道,vector类将元素保存在连续内存中,添加元素时,若有空间则加入,若没有空间则重新分配内存,并将原有元素移动到新内存中,释放旧空间并添加新元素。

因此我们的StrVec类也要如此。

我们将使用一个allocator(一个类模板,将内存分配和对象构造分离开来)来获得原始内存。添加对象时,由于allocator分配的内存是未构造的,我们将用allocator的construct成员在原始内存中创建对象。删除对象时,使用destroy成员来销毁元素。

StrVec有三个指针成员指向其元素所使用的内存,有一个静态成员来分配内存。

  • elements : 指向分配的内存中的首元素
  • first_free : 指向最后一个实际元素之后的位置
  • cap : 指向分配的内存末尾之后的位置
  • alloc : 分配内存的静态成员,allocator < string >类型

StrVec还有4个工具函数。

  • alloc_n_copy : 分配内存,拷贝指定范围中的元素
  • free : 销毁构造的元素并释放内存
  • chk_n_alloc : 保证StrVec至少有容纳一个新元素的空间
  • reallocate : 在内存用完时为StrVec分配新内存
class StrVec {

public:

    StrVec() :elements(nullptr), first_free(nullptr), cap(nullptr) {}
    StrVec(const StrVec&);
    StrVec& operator = (const StrVec &);
    string& operator[](const size_t);
    ~StrVec();

    //插入
    void push_back(const string&);
    string* insert(string*,const string&);
    string* insert(string*,const string*,const string*);

    //删除
    void pop_back();
    string* erase(string *);

    size_t size() const { return first_free - elements; }  //获取当前元素个数
    size_t capacity() const { return cap - elements; }     //获取内存大小

    string* begin() const { return elements; }
    string* end() const { return first_free; }


private:

    allocator<string> alloc;  //分配内存
    string *elements;
    string *first_free;
    string *cap;

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

    pair<string*, string*> alloc_n_copy(const string *, const string *);
    void free();
    void reallocate();



};



StrVec::StrVec(const StrVec &s)
{
    pair<string*,string*> p;
    p = alloc_n_copy(s.begin(), s.end());
    elements = p.first;
    first_free = p.second;
    cap = p.second;



}

StrVec & StrVec::operator=(const StrVec &s)
{
    auto p = alloc_n_copy(s.begin(), s.end());

    free();

    elements = p.first;
    first_free = p.second;
    cap = p.second;

    return *this;

}

string& StrVec::operator[](const size_t loc)
{
    return *(elements + loc);
}

StrVec::~StrVec()
{
    free();
    cout << "析构函数被调用辣" << endl;
}

void StrVec::push_back(const string &s)
{
    //添加元素到容器末尾

    //确保有位置
    chk_n_alloc();
    //加入元素
    alloc.construct(first_free++,s);

}

string* StrVec::insert(string *p, const string &s)
{

    assert(p >= begin() && p <= end());

    size_t loc = p - elements;
    chk_n_alloc();
    auto e = elements + loc;

    alloc.construct(end());

    for (auto b = end(); b > e; --b)    
        *b = *(b - 1);

    *e = s;

    first_free++;

    cout << "插入了一个元素" << endl;

    return e;
}

string * StrVec::insert(string *, const string *, const string *)
{
    return nullptr;
}

void StrVec::pop_back()
{
    assert(elements != first_free);
    alloc.destroy(--first_free);
}

string * StrVec::erase(string *p)
{
    assert(p >= begin() && p < end());

    for (auto b = p; b < end() - 1; ++b)
        *b = *(b + 1);

    alloc.destroy(end() - 1);

    return ++p;
}


pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e)
{
    //分配空间保存给定范围中的元素,返回首元素指针和尾元素下一位置的指针

    //分配空间
    auto data = alloc.allocate(e - b);

    //初始化并返回一个pair,将[b,e)赋值给迭代器data指定的未构造的内存中
    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);
    }
}

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)
        alloc.construct(dest++, std::move(*elem++));  //移动而不是拷贝

    //释放旧空间
    free();

    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;

    cout << "重新分配内存辣 当前内存为" << cap - elements << "元素个数为" << first_free - elements << endl;


}

现在,我们只有一个函数reallocate没有实现。这个函数需要完成如下功能。

  • 为容器分配一个更大的内存
  • 在内存的前一部分构造对象,保存现有元素
  • 销毁原内存空间中的元素,释放内存

我们知道,拷贝一个string就必须拷贝数据,它就会有两个用户。但是,如果是reallocate拷贝StrVec中的string,则在拷贝之后,只有一个用户(旧内存被释放了)。

因此,拷贝string是多余的,分配和释放string会造成很多的额外开销,所以我们想要移动它们。

移动构造函数和std::move

通过使用新标准库引入的两种机制,我们就可以避免string库的拷贝。

  • 移动构造函数:移动构造函数通常将对象移动而不是拷贝到正在创建的对象中。对于string,我们可以想象每个string都有一个指向char数组的指针,可以假定string的移动构造函数进行了指针的拷贝。
  • sd::move : 定义在utility头文件中。① 当我们希望用string的移动构造函数时,必须调用move来表示这个希望 ② 我们使用move时,直接调用std::move而不是move。
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)
        alloc.construct(dest++, std::move(*elem++));  //移动而不是拷贝

    //释放旧空间
    free();

    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

猜你喜欢

转载自blog.csdn.net/fancynece/article/details/79374820
今日推荐