Day14

std::string的底层实现

三种方式:

深拷贝

写时复制

短字符串优化

深拷贝

无论什么情况,都是采用拷贝字符串内容的方式解决。不需要改变字符串内容时,对字符串进行频繁复制。

用一个string对象初始化另一个string对象时,源对象的内容会被完全复制到目标对象中,不是仅仅复制指针。即每一个string对象内部的指针都指向自己的内部字符存储区域。

string str = "hello";
string str1 = str;//拷贝构造的第二种形式
//重新申请空间复制内容。
​
//拷贝构造
//实现深拷贝,分配新内存并复制源对象的内容
string & (const string & res)
{
    //如果源对象的内容非空,将delete
    if(res.data)
    {
        size_t len = std::strlen(res.data);
        data = new char[len + 1];
        std::strcpy(data, res.data);
        //另一种拷贝方式
        //std::sprintf(data,"%s", res.data);
    }
    else
    {
        data = nullptr;
    }
}
​
//赋值操作符
//先释放当前对象的资源,再分配内存空间并复制源对象的内容
string & operator=(const string & res)
{
    //防止自赋值
    if(this != &res)
    {
        //释放原先指针的资源
        delete[] data;
        if(res.data)
        {
            size_t len = std::strlen(res.data);
            data = new char[len + 1];
            std::strcpy(data, res.data);
            //另一种拷贝方式
            //std::sprintf(data,"%s", res.data);
        }
        else
        {
            data = nullptr;
        }
    }
    return *this;
}
链接其它方面
1.内存布局

栈区:操作系统控制,由高地址向低地址生长,编译器做了优化,显示地址时栈区和其它区域保持一致的方向。

堆区:程序员分配,由低地址向高地址生长,堆区和栈区没有明确的界限。

全局/静态区:读写段(数据段), 存放全局变量、静态变量

文字常量区:只读段,存放程序中直接使用的常量

const char* p = "hello"
    hello这个内容就存放在文字常量区

程序代码区:只读段,存放函数体的二进制代码;

从上到下,地址由高到低。

std::string对象的控制块(指向堆内存的指针、长度、容量等)存储在栈上。

实际的字符串内容(字符数据)存储在堆上

字符常量本身,存储在文字常量区,但std::string会在堆上分配内存来存储它的副本。

static

static 全局变量/函数 只在本文件里面生效

static 局部变量 静态变量

static 类成员/类成员变量 指代和类相关的成员,可以用类作用域直接访问,而不限制于某个对象,所以没有this指针

写时复制

当字符串对象进行复制时,可以优化为指向同一个堆空间的字符串。回收堆空间时,引用计数refcount的-1,只有当引用计数refcount的值为0时才真正回收堆空间上的字符串。

 string str1("hello");
//单独创建对象没有优化空间
//在创建对象是不会遍历所有对象保存的内容
 string str2("hello");

用堆空间保存引用计数

将引用计数和字符串内容保存到一起

除了复制操作,赋值操作也可以确定两个string对象保存的字符串内容是相同的,也可以复用空间,引用计数随之改变。

相比于复制操作,还需要考虑string对象原本用来保存字符串的堆空间是否需要回收。

(1)原本空间的引用计数-1,引用计数减到0,才真正回收堆空间

(2)让自己的指针指向新的空间,并将新空间的引用计数+1

写时复制代码实现



//如果str1和str3共享一片空间存放字符串内容。
//读操作不需要进行复制,写操作应该让str1重新申请一片内存空间去修改,不应该改变str3的内容
cout << str1[0] << endl;//读操作,第一个参数os,第二个参数本对象
str1[0] = 'H';          //写操作,第一个参数本对象,第二个参数=
cout << str3[0] << endl;//str3的内容已经改变了
//要区分下标访问运算符到底是读操作还是写操作
//但是下标访问运算符都是string类对象,可以创建一个CowString类的内部类,让CowString的operator[]函数返回是这个类的对象,然后在这个新类型中对<<和=进行重载,让这两个运算符能够处理新类型对象,从而分开了处理逻辑
//综上所述str1[0]是一个对象,在这个类内部对=和<<运算符进行重载,就能够区分读操作和写操作了

#include <iostream>
#include <ostream>
#include <pstl/pstl_config.h>
#include <pthread.h>
using std::cout;
using std::endl;
using std::ostream;
#include <string.h>
#define PointCount 4
#define PointString 4
​
​
class String
{
private:
    char* _pstr;
​
public:
    class Temp
    {
    public:
        String& _str;
        int _count;
​
    public:
        Temp(String& str, int count)
            :_str(str), _count(count)
        {}
​
        //<< : 读操作
        friend ostream & operator<<(ostream & os, const Temp & rhs);
​
        //=: 写操作
        Temp & operator=(char c)
        {
            int index = _str.readCount();
            //深拷贝
            if(index > 1)
            {
                //开辟空间
                char * str = _str.New(_str._pstr);
                
                //初始化
                sprintf(str, "%s", _str._pstr);
                
                //原先的引用计数--
                _str.countDecrease();
                //修改数值
                _str._pstr = str;
                _str._pstr[_count] = c;
                //修改引用计数
                *((int *)(str - PointCount)) = 1;
                //置空,临时变量
                str = nullptr;
            }
            else if(index == 1)
            {
                //直接更改数据,不需要额外开辟空间
                _str._pstr[_count] = c;
            }
            return *this;
        }
    };
​
public:
    void UpdatePointString(int index, char c)
    {
        _pstr[index] = c;
    }
​
    char* New(const char* str)
    {
        //前四个字节是int,存放引用计数
        //后面是存储字符串内容的空间
        return new char[strlen(str) + 1 + PointCount]() + PointString;
    }
​
    void countadd()
    {
        ++(*(int*)(_pstr - PointString));  
    }
​
    void countDecrease()
    {
        --(*(int*)(_pstr - PointString));
        _pstr = nullptr;
    }
​
    int readCount()
    {
        return *((int*)(_pstr - PointString));
    }
​
    void StringInit(const char* str)
    {
        *((int*)(_pstr - PointString)) = 1;
        sprintf(_pstr, "%s", str);
    }
​
    void Destory()
    {
        //如果引用计数为1
        if(readCount() == 1)
        {
            delete[] (_pstr - PointCount);
            _pstr = nullptr;
        }
        else
        {
            countDecrease();
        }
    }
    char* getStrPoint()
    {
        return _pstr;
    }
​
public:
    //无参构造
    String()
    {}
    //有参构造
    String(const char* pstr)
        :_pstr(New(pstr))
    {
        //初始化
        StringInit(pstr);
    }
​
    //析构
    ~String()
    {
        Destory();
    }
​
    //浅拷贝
    //由于引用计数的出现只需要做浅拷贝即可,引用计数++
    String(const String & res)
    {
        _pstr = res._pstr;
        //引用计数++
        countadd();
    }
​
    //下标操作运算符
    String::Temp operator[](int count) 
    {
        //返回的是一个用于区分读和写操作的对象
        return String::Temp(*this, count);
    }  
};
ostream & operator<<(ostream & os, const String::Temp & rhs)
{
    //String对象
    os <<rhs._str.getStrPoint()[rhs._count];
    return os;
}
​
void test()
{
    String str1 = "hello";
    //拷贝
    String str2(str1);
​
    String str3(str1);
    cout << str1[0] << endl;
​
    str3[0] = 'H';
    cout << "str1[0] = " <<str1[0] << endl;
    cout << "str3[0] = " << str3[0] << endl;
    cout << "str1.readCount() = " << str1.readCount() << endl;
    cout << "str3.readCount() = " << str3.readCount() << endl;
}
​
int main()
{
    test();
    return 0;
}
//重点,str1[0]是Temp对象,在Temp对象中做重载读写操作
//重载[]运算符,间接的创建了临时对象temp,此时cout << str1[0] --> cout << Temp(*this, 0); 再调用<<运算符的读操作
//读操作<<
//写操作=
//用堆空间保存引用计数,指针始终指向char开始的位置

总结,str1[0] = 'H' 和 << str1[0] 都有一个共同点就是str[0],所以在String类中重载下标运算符[],在这个重载函数中创建一个新的对象,对这个新对象重载=和<<将读写操作进行分离

当运算符处理自定义类型对象时,出现模棱两可的情况,可以嵌套一个内部类进行进一步的区分,例如str[0] = 'h';将str[0]当作一个类嵌套在String中,通过=运算符进行区分。

嵌套类

Point类是定义在Line类中的内部类,无法直接创建Point对象,需要在Line类名作用域中才能创建。

Point pt(1,2);//error
Line::Point pt2(3,4);//ok

Point类是Line类的内部类,并不代表Point类的数据成员会占据Line类对象的内存空间,在存储关系上并不是嵌套的结构

只有当Line类有Point类类型的对象成员时,Line类对象的内存布局中才会包含Point类对象(成员子对象)。

(1)如果Line类中没有Point类的对象成员,sizeof(Line) = 8;

(2)如果Line类中有两个Point类的对象成员,sizeof(Line) = 24;

重载形式

友元函数的重载形式

不会修改操作数的值的运算符,倾向于采用友元函数的方式重载

具有对称性的运算符可能转换任意一端的运算对象,例如相等性、位运算符等,通常应该是友元形式重载

普通函数的重载形式

访问类中的私有成员,给这个类加共有的get系列函数。

成员函数的重载形式

第一个操作数实际上是this指针,也是运算符左操作数所指向的对象

与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员函数形式重载

会修改操作数的值的运算符,倾向于采用成员函数的方式重载

赋值=、下标[ ]、调用()、成员访问->、成员指针访问->* 运算符必须是成员函数形式重载

短字符串优化

当字符串的字符数小于等于15时, buffer直接存放整个字符串;当字符串的字符数大于15时, buffer 存放的就是一个指针,指向堆空间的区域。这样做的好处是,当字符串较小时,直接拷贝字符串,放在 string内部,不用获取堆空间,开销小。

union表示共用体,允许在同一内存空间中存储不同类型的数据。共用体的所有成员共享一块内存,但是每次只能使用一个成员。

class string {
    union Buffer{
        char * _pointer;
        char _local[16];
    };
    
    size_t _size;
    size_t _capacity;
    Buffer _buffer;
};

总结:最佳策略

  1. 很短的(0~22)字符串用SSO,23字节表示字符串(包括'\0'),1字节表示长度

  2. 中等长度的(23~255)字符串用eager copy,8字节字符串指针,8字节size,8字节capacity.

  3. 很长的(大于255)字符串用COW, 8字节指针(字符串和引用计数),8字节size,8字节capacity.

一些感言

找到工作了,这些天一直在搞图像捕获用来选取角色和血量匹配,大概有五天的时间没有静下心来学习。老板给了我一个月的时间,但是我现在基本上已经搞出来了,而且打包生成了可执行程序。就等美术方面了。但是我明天才入职,老板还不知道我的进度,目前是一个初创公司。我需要进去稳定一个礼拜,才能慢慢的出货,而且代码是我一个人写的,打包的地方也有坑,整个公司就三个会程序的,其它全是美术。就我一个既会c++又会python,其实python我也不太懂。但我能看懂,知道该怎么改代码这就完全足够了。由于这段时间没能学习,所以放上我之前做的一些笔记。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值