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

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

拷贝控制和资源管理

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

  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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值