手把手教你实现buffer(二)——内存管理及移动语义


在webrtc中有一个 Buffer类,它是一个非常典型的基础buffer封装,我们来通过分析它的实现,来学习要如何实现一个buffer。这篇文件介绍 Buffer的构造函数及移动语义。

webrtc中的Buffer类

在前面的文章中,提到了buffer的几个基本功能:

  1. 内存动态分配
  2. 自动管理内存
  3. 自动扩容
  4. 提供使用方便的接口

我们从这4个方面来分析Buffer类的实现

class Buffer:public noncopyable {
public:
    Buffer():_size(0),_capacity(0),_data(nullptr){

    }
    
    Buffer(size_t size,size_t capacity):
    _size(size),
    _capacity(std::max(size,capacity)),
    _data(_capacity>0?new uint8_t[_capacity]:nullptr) {

    }

    Buffer(size_t size):Buffer(size,size) {}
    
    Buffer(const uint8_t* data, size_t size, size_t capacity):Buffer(size,capacity) {
        std::memcpy(_data.get(),data, size);
    }

    Buffer(const uint8_t* data,size_t size):Buffer(data,size,size){}
    
    Buffer(Buffer&& buf):
    _size(buf.size()),
    _capacity(buf.capacity()),
    _data(std::move(buf._data)) {
        assert(IsConsistent());
        buf._size = 0;
        buf._capacity = 0;
    }

    Buffer& operator=(Buffer&& buf) {
        assert(IsConsistent());
        _size = buf._size;
        _capacity = buf._capacity;
        std::swap(_data,buf._data);
        
        buf._data.reset();
        buf._size = 0;
        buf._capacity = 0;
        return *this;
    }

    bool empty() const {
        return _size == 0;
    }

    uint8_t* data() {
        return _data.get();
    }

    uint8_t* data() const {
        return _data.get();
    }

    size_t size() const {
        return _size;
    }

    size_t capacity() const {
        return _capacity;
    }

    uint8_t& operator[](size_t index) {
        return data()[index];
    }

    uint8_t operator[](size_t index) const {
        return data()[index];
    }

    void SetData(const uint8_t* data,size_t size);
    void AppendData(const uint8_t* data,size_t size);
    void SetSize(size_t size);
private:
    void ZeroTrailingData(size_t count);
    void EnsureCapacityWithHeadroom(size_t capacity, bool extra_headroom);
    bool IsConsistent() const;
private:
    size_t _size;
    size_t _capacity;
    std::unique_ptr<uint8_t[]> _data;
};

}

我做了点小的改良,Buffer类原本是个模版类,这里我将的类型固化为uint8_t便于分析。

管理内存

  1. Buffer内部通过unique_ptr来管理内存空间,这里是uint8_t[],因为是管理的一段内存。
std::unique_ptr<uint8_t[]> _data;
  1. 在构造函数中分配内存。
//传入size和capacit来分配一段内存
Buffer(size_t size,size_t capacity):
    _size(size),
    _capacity(std::max(size,capacity)),
    _data(_capacity>0?new uint8_t[_capacity]:nullptr) {
}

//只传入一个size参数时,size合capacity大小一直
Buffer(size_t size):Buffer(size,size) {}

也可以通过一段内存构造Buffer

//分配制定size和capacity的内存并将data的内存copy到buffer中
Buffer(const uint8_t* data, size_t size, size_t capacity):Buffer(size,capacity) {
        std::memcpy(_data.get(),data, size);
 }

Buffer(const uint8_t* data,size_t size):Buffer(data,size,size){}
  1. 在析构函数中释放内存

因为使用的是unique_ptr,所以直接使用默认析构函数即可,在析构_data时,内存会自动释放。

给Buffer赋予语意

在C++中的对象有三种语意:值语义,引用语义,移动语义

  1. 值语义,支持拷贝,指对象的拷贝与原对象无关,就像内置类型一样,将一个int型变量赋值给另外一个变量,两者间是无关联的。
  2. 引用语义,支持拷贝,指对象的拷贝与原对象相关联,比如在内部有指针的对象,如果对象赋值时对指针是浅拷贝,这里两个对象是相互关联的,因为两个对象的指针都指向同一块内存。
  3. 移动语义,不支持拷贝,两个对象间不能相互赋值,只能将内部资源移动到另外一个对象,原对象则变为无效对象。

Buffer在C++中最直观的语义就是引用语义,因为它代表一块内存空间。多个裸指针可以指向同一块内存,理论上Buffer也应该支持引用。

但是这种引用语义,很难区分所有权,即是谁需要对这块内存负责(负责分配,负责释放),这种语义的Buffer多引用几次,引用关系就会混乱了,很容易造成问题,比如内存无法释放;内存在释放后还会被访问造成段错误等。

移动语意刚刚与引用语义相反,它不支持拷贝,不能相互引用,只存在移动操作。对**Buffer**来说的移动语义,它所代表的资源就是内部的内存空间,将一个buffer1对象移动到另外一个buffer2对象,就是代表在buffer1对象不再拥有这段内存空间,而是将其转移给了buffer2对象。

这样严格区分所有权,可以转让出所有权,但是不能共享所有权。这样就能保证内存空间能被正常使用和安全的释放。

禁止拷贝

禁止拷贝,通过是构造函数,复制构造函数,赋值操作符号来实现。要禁用复制语义,需要即不定义这些函数也不能让编译器自动生成。如下是Buffer类的实现方法,继承noncopyable

class Buffer:public noncopyable

nocopyable的实现

class noncopyable {
public:
    noncopyable(const noncopyable&) = delete;
    void operator = (const noncopyable&) = delete;
protected:
    noncopyable() = default;
    ~noncopyable() = default;
};

nocopyable拷贝构造函数和赋值函数声明为delete,构造函数和析构函数声明为private,就无法产生**nocoyable**对象。而**Buffer**继承**nocopyable**也就禁用了拷贝,这是实现禁止拷贝的通用手法。

实现移动语义

C++11中自动类型具有移动语义,需要实现移动构造函数和移动赋值操作符,通过它们来定义移动的行为。

移动构造函数
//移动构造函数
Buffer(Buffer&& buf):
    _size(buf.size()),
    _capacity(buf.capacity()),
    _data(std::move(buf._data)) {
        assert(IsConsistent());
        buf._size = 0;
        buf._capacity = 0;
 }

将形参的_data指向的内存空间通过std::move给到了所构造的对象,而原对象的_data被置为无效,_size_capacity也被置为0。

移动赋值操作符
//移动赋值运算符
Buffer& operator=(Buffer&& buf) {
        assert(IsConsistent());
        _size = buf._size;
        _capacity = buf._capacity;
        std::swap(_data,buf._data);
        
        buf._data.reset();
        buf._size = 0;
        buf._capacity = 0;
        return *this;
    }

移动赋值运算符的逻辑跟移动构造函数的相同,将资源转移,将原对象置为无效。

使用Buffer

结合构造函数及移动语义,那么Buffer就可以这样用

Buffer tmpF() {
    uint8_t tmp[5] = {1,2,3,4,5};
    Buffer tmpB(tmp,5);
    return tmpB;
}

int main() {
    Buffer buffer1;//1
    uint8_t b[3] = {1,2,3};
    Buffer buffer2(b,3);
    std::cout<<"buffer1 size "<<buffer1.size()<<",buffer2 size "<<buffer2.size()<<std::endl;

    Buffer buffer3(std::move(buffer2));//2
    std::cout<<"buffer3 size "<<buffer3.size()<<",buffer2 size "<<buffer2.size()<<std::endl;

    Buffer buffer4 = tmpF();
    std::cout<<"buffer4 size "<<buffer4.size()<<std::endl;
}
  1. 首先构造了两个Buffer对象buffer1和buffer2,buffer1的size为0,buffer2的size为3。
  2. 通过移动构造函数使用buffer2构造了对象buffer3,此时buffer2的资源转移至buffer3,buffer3的size为3,buffer2的size为0。
  3. 函数tmpF()返回一个临时对象通过移动构造函数来构造buffer4,临时对象的资源被转移到buffer4,buffer4的size为5。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mo4776

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值