Day14

news2024/10/23 11:18:11

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我也不太懂。但我能看懂,知道该怎么改代码这就完全足够了。由于这段时间没能学习,所以放上我之前做的一些笔记。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2211575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL SELECT 查询(三):查询常用函数大全

MySQL SELECT 查询&#xff08;三&#xff09;&#xff1a;查询常用函数大全 1. 单行函数 单行函数是 SQL 中一类重要的函数&#xff0c;它们可以对单行数据进行处理&#xff0c;并返回单个结果。单行函数可以嵌套使用&#xff0c;并提供灵活的数据处理能力。 1.1 定义 只对单…

微知-Mellanox网卡如何导出firmware中的config文件以及文件中有些什么?(ini配置文件,8个区)

背景 Mellanox网卡早期版本以及Engineer simple的DPU支持导出配置文件&#xff0c;该配置文件就是用来告诉firmware的行为。但不是mlxconfig真正设置的文件(mlxconfig -d xxx -e -q应该就是把这个文件读取出来&#xff0c;并且有3个文件&#xff0c;包括默认的&#xff0c;当前…

攻防世界2

forgot 发现是32位文件 fgets(s, 32, stdin)限制读入32位字符&#xff0c;无法利用 __isoc99_scanf("%s", v2) 典型的栈溢出 发现cat flag 覆盖v2-v3&#xff0c;覆盖为cat flag的函数地址 exp&#xff1a; from pwn import * context(oslinux,archamd64,log_lev…

芋道快速开发平台学习笔记

1.接口文档配置 基础知识:SpringDoc注解的使用,它是基于OpenAPI 3和Swagger 3的现代化解决方案,相较于旧版的Swagger2即SpringFox,SpringDoc提供了更简洁、更直观的注解方式。 详见springboot集成springdoc-openapi(模拟前端请求)_springdoc-openapi-ui-CSDN博客 doc文档配置…

c++面向对象三大特性——多态详解与虚函数,虚函数底层

目录 前言&#xff1a; 1. 多态的概念 1.1 概念 2. 多态的定义及实现 2.1多态的构成条件 2.2 虚函数 2.3虚函数的重写 2.4 C11 override 和 final 2.5 重载、覆盖(重写)、隐藏(重定义)的对比 3. 抽象类 3.1 概念 3.2 接口继承和实现继承 4.多态的原理 4.1虚函数表 …

7.1-I2C的中断

I2C的中断与DMA 回顾 HAL_I2C_MASTER_Transmit(&hi2c1,ADRESS,PDate,Size,Time);HAL_I2C_MASTER_Receive(&hi2c1,ADRESS,PDate,Size,Time);通信具体过程如下&#xff1a; 在I2C的轮询模式中 发送时&#xff1a;CPU将以主机0x70 发送 从机 ACK 回复 主机0xAC发送 A…

⽂件的操作

1. 为什么使⽤⽂件&#xff1f; 如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失了&#xff0c;等再次运⾏程序&#xff0c;是看不到上次程序的数据的&#xff0c;如果要将数据进⾏持久化…

深圳大学-Java程序设计-必实验2 类的高级应用

实验目的与要求&#xff1a; 实验目的&#xff1a;熟悉面向对象编程中类的编写。 实验要求&#xff1a; (1).请自行选择2023年成都大运会或2023杭州亚运会。大型运动会通常包括众多比赛项目。请通过分析&#xff0c;抽象它们所共有的性质&#xff0c;定义一个关于比赛项目的抽…

点评项目-6-缓存更新策略、缓存穿透、雪崩

缓存更新策略 使用 redis 缓存记录的信息&#xff0c;有可能在数据库被信息被修改导致信息不一致&#xff0c;使用缓存更新来解决这个问题 缓存更新策略主要有三种&#xff1a; 1.内存淘汰(redis默认开启) 2.超时剔除(给key添加TTL时间) 3.主动更新(编写业务逻辑) 主动更新策…

网络通信与并发编程(一)网络通信、osi五层模型、tcp协议的三次握手与四次挥手

网络通信、osi五层模型、tcp协议的三次握手与四次挥手 文章目录 网络通信、osi五层模型、tcp协议的三次握手与四次挥手一、网络通信二、osi五层模型1.物理层2.数据链路层3.网络层4.传输层5.应用层 三、tcp协议的三次握手与四次挥手 一、网络通信 网络通信是指在网络中的两个或…

Java ==> 数组(入门)

文章目录 前言一、一维数组1.入门操作2.何为null&#xff1f;3.数组可以作为函数的参数4.数组可以作为函数的返回值 二、二维数组1.基础操作2.不规则的二维数组 总结 前言 在Java语言当中&#xff0c;数组是一种基本的数据结构&#xff0c;它存储了固定大小的同类型元素的集合…

告别卡顿!五款Windows录屏工具,让每一帧都清晰流畅

小伙伴们&#xff0c;是不是在寻找一款好用、实用的Windows录屏工具呢&#xff1f;别担心&#xff0c;这次我给大家带来了一款热门录屏工具的详细评测和使用感受&#xff0c;包括福昕录屏、转转录屏、爱拍录屏、OBS录屏和EV录屏。快来看看哪款最适合你吧&#xff01; 一、福昕录…

反射的学习

1、什么是反射 反射允许对封装类的字段&#xff0c;方法和构造函数的信息进行编程访问。 也就是&#xff1a; 反射允许对成员变量&#xff0c;成员方法和构造方法的信息进行编程访问。 2、获取class对象 获取一个类的字节码文件对象&#xff1a; 方式1&#xff1a;Class.…

linux 环境运行 jenkins.war包,有可能会出现字体问题,jdk版本:11 jenkins 版本:2.420

jenkins的目录&#xff1a; /usr/jenkins 启动命令 java -Djava.awt.headlesstrue sudo timedatectl set-timezone Asia/Shanghai-Xmx1024m -jar jenkins.war --httpPort8090 任意目录启动&#xff1a; nohup java -Djava.awt.headlesstrue -Xms1024m -Xmx1024m -jar /usr/j…

基于opencv答题卡识别判卷

我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色&#xff1a;深度实…

Excel使用技巧:数据-》分列:处理log数据,把有用的信息提取出来;查找Ctrl+F,替换Ctrl+H;通配符

Excel的正确用法&#xff1a; Excel是个数据库&#xff0c;不要随意合并单元格。 数据输入的时候一定要按照行列输入&#xff0c;中间不要留空&#xff0c;不然就没有关联。 数据-》分列&#xff1a;处理log数据&#xff0c;把有用的信息提取出来。 按照向导一步一步操作。…

D36【python 接口自动化学习】- python基础之函数

day36 函数的定义 学习日期&#xff1a;20241013 学习目标&#xff1a;输入输出与文件操作&#xfe63;-49 函数定义&#xff1a;如何优雅地反复引用同一段代码&#xff1f; 学习笔记&#xff1a; 函数的用途 定义函数 调用函数 # 定义函数 def foo():print(foo)print(foo …

数据传送指令

文章目录 MOVXCHGPUSH和POPIN和OUTXLATLEA LDS LESLEALDSLES LAHF SAHFPUSHF POPF总结 MOV MOV dst, src ; dst <-- src可以进行8位或16位数据的传送源操作数可为立即数、寄存器、存储器操作数目的操作数不可为立即数&#xff0c;CS、IP寄存器两操作数必有一个寄…

Hi3244 应用指导

Hi3244 是一款DIP8封装高性能、多模式工作的原边控制功率开关。Hi3244内高精度的恒流、恒压控制机制结合完备的保护功能&#xff0c;使其适用于小功率离线式电源应用中。在恒压输出模式中&#xff0c;Hi3244 采用多模式工作方式&#xff0c;即调幅控制&#xff08;AM&#xff0…

LLM - 配置 ModelScope SWIFT 环境与 Qwen2-VL 模型推理 教程 (1)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142827217 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 SWIFT …