• 手把手教你实现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;
    };
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

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

    管理内存

    1. Buffer内部通过unique_ptr来管理内存空间,这里是uint8_t[],因为是管理的一段内存。
    std::unique_ptr<uint8_t[]> _data;
    
    • 1
    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) {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    也可以通过一段内存构造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
    • 2
    • 3
    • 4
    • 5
    • 6
    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
    
    • 1

    nocopyable的实现

    class noncopyable {
    public:
        noncopyable(const noncopyable&) = delete;
        void operator = (const noncopyable&) = delete;
    protected:
        noncopyable() = default;
        ~noncopyable() = default;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    将形参的_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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    使用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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    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。
  • 相关阅读:
    springboot中自定义JavaBean返回的json对象属性名称大写变小写问题
    华为政企路由器产品集
    如何使用ChatGPT辅助设计工作
    直接缓存访问DCA
    自定义类型(结构体、位段、联合体、枚举)
    Redis内存淘汰机制
    牛客[NOIP2016]蚯蚓
    12. 转义字符及print函数的参数
    安卓玩机教程---全机型安卓4----安卓12 框架xp edx lsp安装方法
    【备忘】ChromeDriver 官方下载地址 Selenium,pyppetter依赖
  • 原文地址:https://blog.csdn.net/mo4776/article/details/126107657