《C++ Primer》读书笔记第十三章-2-拷贝控制、交换、动态内存管理类

来源:互联网 发布:bootstrap input控件js 编辑:程序博客网 时间:2024/05/22 01:49

笔记会持续更新,有错误的地方欢迎指正,谢谢!

拷贝控制和资源管理

通过定义不同的拷贝操作,使自定义的类的行为看起来像一个值或指针。

  1. 类的行为像一个,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响,反之亦然。
  2. 类的行为像指针(共享状态),当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。

标准库容器和string类的行为像一个值;shared_ptr类的行为像指针;IO和unique_ptr不允许拷贝或赋值,值和指针都不像。

举例:

实现一个类HasPtr,让它的行为像一个值;然后重新实现它,再使其像一个指针:
我们的HasPtr有两个成员,一个int和一个string指针,对于内置类型int,直接拷贝值,改变它也不符合常理;我们把重心放到string指针,它的行为决定了该类是像值还是像指针。

行为像值的类

为了像值,每个string指针指向的那个string对象,都得有自己的一份拷贝,为了实现这个目的,我们需要做以下三个工作:

  1. 定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
  2. 定义析构函数来释放string
  3. 定义一个拷贝赋值运算符来释放对象当前的string,并从右侧运算对象拷贝string
    我们来抛代码:
class HasPtr{public:    HasPtr(const string &s = string()) : ps(new string(s)), i(0){} //默认实参,列表初始化/*补充:const修饰s为字符串常量(只读),此处s恒为空字符串;&的作用是引用,避免再复制一个string,若是const string s的话还要再复制一次,岂不是很浪费。所以,既然已是只读,为啥不直接用引用。*/    HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i){} //拷贝构造函数    HasPtr& operator=(const HasPtr &); //赋值运算符声明    ~HasPtr(){delete ps;} //析构函数private:    string *ps;    int i;};

主要在于拷贝构造函数,它是有副本的,会拷贝string对象,所以析构函数要delete来释放内存。这个类写得很优雅~

类值拷贝赋值运算符
举例:a = b;
类值拷贝赋值:自赋值也是安全的,发生异常后左侧对象状态仍有意义;先拷贝构造右侧对象到临时变量,再销毁左侧成员并赋予新值。

所以,赋值运算符定义如下:

HasPtr& HasPtr::operator=(const HasPtr &rhs){ auto newp = new string(*rhs.ps); //拷贝底层string delete ps; //释放对象指向的string,指针无析构函数,所以指针还在 ps = newp; //从右侧对象拷贝数据到本对象 i = rhs.i; return *this; //返回本对象}

定义行为像指针的类

拷贝指针就行了?没那么简单,还是要释放内存。而且这个释放内存的时机很重要:只有当最后一个指向string的HasPtr销毁时,才能释放内存,所以,我们可以用shared_ptr,但这里,不用智能指针,弄麻烦些,让大家看看底层如何实现引用计数

class HasPtr{public:    HasPtr(const string &s = string()) : ps(new string(s)), i(0),    use(size_t(1)){} //默认实参,列表初始化    HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use){++(*use)} //拷贝构造函数,要递增计数器    HasPtr& operator=(const HasPtr &); //赋值运算符声明    ~HasPtr() //析构函数    {        if(--(*use) == 0) //没人引用了才释放        {            delete ps;            delete use;        }    }private:    string *ps;    int i;    size_t *use; //引用计数    //补充:size_t是目标平台能够使用的最大的类型,考虑到了跨平台的效率问题。};

类指针拷贝赋值运算符:

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

拷贝并交换:在赋值运算符中,若参数不是引用,此时结合拷贝和交换的赋值运算符是安全的。

比如我们来交换一下前面写的HasPtr(值拷贝版本):

HasPtr temp = v1;v1 = v2;v2 = temp;

这是很自然的一种写法,我们借助中间量temp来交换v1和v2,但是,有时候对象很大,你跑去拷贝交换对象很浪费,不如去交换指针更合算:

string *temp = v1.ps;v1.ps = v2.ps;v2.ps = temp;

我们定义交换指针的swap函数后,就可以利用它写出更简洁的赋值运算符啦~

这样做的好处在哪?
在于你不用管内存拷贝释放等事,把类写好后,保证你调用的方式是最经济有效的。

拷贝控制示例

我们将建立两个类用于邮件处理,两个类命名为Message和Folder,分别表示邮件消息和消息目录。每个Message对象可以出现在多个Folder中,但是任意给定的Message的内容只有一个副本,这样的话,一条Message的内容改变,则我们从任意Folder来浏览它时看到的都是更新的内容。

书上写得挺优雅地,分析和代码请见P460

动态内存管理类

某些类需要在运行时分配可变大小的内存空间,这种类最好在底层使用标准容器库,例如vector。但是,有些类需要自己进行内存分配,它基本就要定义自己的拷贝控制成员来管理所分配的内存

这一节,要实现标准库vector类的一个简化版,它只能用于string,命名为StrVec。

StrVec类的设计

主要参照vector<string>源码来实现,vector源码请见我的另一篇博客:
http://blog.csdn.net/billcyj/article/details/78801834

我们将使用allocator来获得原始内存,由于它分配的内存是未构造的,我们将需要在添加新元素时,用allocator的construct成员在原始内存中创建对象;同样的,我们在删除元素时就使用destroy成员来销毁函数。
每个StrVec有三个指针成员指向其元素所使用的内存:

  1. elements 指向首元素
  2. first_free 尾后元素
  3. cap 指向分配的内存末尾的下一个位置

StrVec还有一个名为alloc的静态成员,其类型为allocator<string>。alloc成员会分配StrVec使用的内存。我们的类还有4个工具函数:

  1. alloc_n_copy会分配内存,并拷贝一个给定范围内的元素
  2. free会销毁构造的元素并释放内存
  3. chk_n_alloc保证StrVec至少有容纳一个新元素的空间,如果空间不够的话,它会调用reallocate来分配更多内存
  4. reallocate在内存用完时为StrVec分配新内存

分析设计好了之后,我们就可以动手写类了:

class StrVec{public:    StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr){} //默认构造函数    StrVec(const StrVec&); //拷贝构造函数声明    StrVec &operator=(const StrVec&); //拷贝赋值运算符声明    ~StrVec(); //析构函数    //一些常用成员函数    void push_back(const 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:    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();    //数据成员    string *elements;    string *first_free;    string *cap;};

该说明的都已经在注释中说明了。

接下来分别实现已经声明的函数定义:

push_back

void StrVec::push_back(const string& s){    chk_n_alloc(); //确保有空间    alloc.construct(first_free++, s); //调用allocator成员construct来插入    //至于construct怎么搞得咱们就不了解了,有兴趣自己去查吧}

alloc_n_copy
分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中:

pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e){    auto data = alloc.allocate(e-b); //分配正好的空间    return {data, uninitialized_copy(b, e, data)};}

在返回语句中完成拷贝工作:
返回语句中对返回值进行了列表初始化,返回的pair中,first指向分配内存的开始位置(因为data作为名字来用就是首元素地址),second是uninitialized_copy的返回值,这个值是一个指向尾后元素的指针。

free
free要干两件事:

  1. destroy元素,是指确实有内容的元素
  2. 释放分配的内存空间,包括没有元素的内存
void StrVec::free(){if(elements) //确保不是空指针,就是要有元素{    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 = 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;}

reallocate
我们会用到一些之后要学的函数:

void StrVec::reallocate(){    //空就分配一个,不空就变为2倍,好好看看,这个写法很装逼    auto newcapacity = size() ? 2*size() : 1;    auto newdata = alloc.allocate(newcapacity); //分配新内存    auto dest = newdata; //指向新数组的下一个空闲位置    auto elem = elemments; //指向旧数组的下一个元素    for(size_t i=0; i != size(); ++i)    {        alloc.construct(dest++, move(*elem++));        //这里的move函数你就理解为把旧数组元素移动到新数组中,不需要拷贝了    }    free(); //移动好元素就释放旧内存空间    //更新数据成员    elements = newdata;    first_free = dest;    cap = elements + newcapacity;}
阅读全文
1 0