Redis底层数据结构之String

news2024/10/5 20:27:06

文章目录

      • 1. 前提回顾
      • 2. RedisObject三大数据类型简介
      • 3. SDS字符串
      • 4. SDS字符串源码分析
      • 5. 总结

1. 前提回顾

前面我们说到redis的String数据结构在底层有多种编码方式。例如我们执行下面两条语句

set k1 v1
set age 17

我们查看类型,发现这类型都是String类型

在这里插入图片描述
我们现在查看底层的编码类型,发现编码类型一个是embstr一个是int类型,虽然都是String字符串,但redis根据value的不同类型设置了不同的编码方式

在这里插入图片描述

redisObject内部对应3大物理编码:intembstrraw

在这里插入图片描述

2. RedisObject三大数据类型简介

  • int

它保存long型(长整型)的64位(8个字节)有符号整数(9223372036854775807,最高19位)。

在这里插入图片描述
只有整数才会使用int类型去编码,如果是浮点数,redis内部其实先将浮点数转化为字符串值,然后再保存。

在这里插入图片描述

  • emstr

代表embstr格式的SDS(Simple Dynamic String 简单动态字符串),保存长度小于44字节的字符串,embstr即embedded String,表示嵌入式的String

  • raw

保存长度大于44字节的字符串

下面给演示一下各个类型的使用:

首先长度小于19位的数字,类型为int

在这里插入图片描述

浮点数底层为emstr

在这里插入图片描述

长度超过19的数字,类型为emstr

在这里插入图片描述

普通字符串是emstr

在这里插入图片描述

长度超过44的字符串,为raw类型

在这里插入图片描述

长度超过44的浮点数,为raw类型

在这里插入图片描述

3. SDS字符串

我们知道c语言底层表示字符串本质上是用的一个char数组。但是redis没有直接复用C语言的字符串的底层实现,而是新建了属于自己的结构—SDS,在redis数据库里,包含字符串值的键值对都是由SDS实现的(redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis所有的值对象中包含的字符串对象底层也是SDS实现的)

在这里插入图片描述
我们看一下sds结构体的源码:

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

可以看到redis底层工定义了五种sds字符串的结构,分别是:sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64。那么各个字段是什么意思:

  • len:代表存储的字符串的长度
  • alloc:存储了为字符串分配的内存空间的长度(即实际分配的空间大小,不包括结构体头部和字符串结束符 \0)
  • flags:代表SDS字符串的类型,上面介绍类型的五种之一
  • buf[]:真正存储字符串的char数组

那上面五种类型有什么不同?

sdshdr5:表示 2 5 2^5 25即共32个字节
sdshdr8:表示 2 8 2^8 28即256个字节
sdshdr16:表示 2 16 2^{16} 216即65536个字节,64KB
sdshdr32:表示 2 32 2^{32} 232即4GB
sdshdr64:表示 2 64 2^{64} 264即17179869184G个字节

len表示SDS实际的长度,使我们在获取字符串的时候能够在O(1)时间复杂度情况下拿到,而不需要遍历一遍char数组

alloc:可以用来计算free就是字符串已经分配的未使用空间,这个值就可以引入预分配空间算法了,而不用去考虑内存分配问题

buf:字符串数组,真正存数据的

alloc 字段的作用是提供了对字符串进行动态增长的能力。当字符串需要扩容时,Redis 可以根据需要重新分配更大的内存空间,并将原有的内容复制到新的内存中,然后释放原来的内存。这样做的好处是可以减少频繁地进行内存分配和释放操作,提高了性能。举个例子,假设有一个 SDS 字符串,其实际长度为 10,但为了防止频繁的内存分配和释放操作,alloc 被设置为了 20。当这个字符串需要扩容时,Redis 可以直接在内存中分配一个长度为 20 的新空间,然后将原字符串内容复制到新的空间中,同时更新 len 和 alloc 字段。这样即使字符串长度增长,也不需要每次都重新分配内存,减少了系统的开销。

现在思考一个问题:Redis为什么不直接使用char数组,而是自己重写设计来一个SDS数据结构?

这是因为C语言没有Java里面的String类型,只能是靠自己的char[]来实现,字符串在C语言中的存储方式,如果现在我们想通过strlen命令获取value的长度,我们就需要从头开始遍历,知道遇到\0即可,这样操作的时间复杂度是O(n),所以redis没有直接使用C语言传统的字符串标识,而是自己构建了一种名为简单动态字符串类型SDS,并作为redis的默认字符串类型。

在这里插入图片描述

4. SDS字符串源码分析

当我们输入set k1 v1 底层到底发生了什么?这里我们详细分析一下

首先看t_string.c文件

/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
 *     [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
void setCommand(client *c) {
	//key的过期时间
    robj *expire = NULL;
    //时间的单位是s
    int unit = UNIT_SECONDS;
    int flags = OBJ_NO_FLAGS;

    if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
        return;
    }
	//c->args[2]就是value,这里调用tryObjectEncoding就是尝试堆value值进行编码,编码后的结果再保存到c->args[2]中
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

当我们在cli写set k1 v1时,它底层本质上调用的是上面的setCommand命令

从上面代码代码可以看出字符串的编码和tryObjectEncoding这个函数时紧密相关的,我们来分析一下底层是怎么做的

  • int

当字符串键值的内存可以用一个64位有符号整数表示时,Redis会将键值转化为long型来存储,此时即对于OBJ_ENDOCDING_INT编码类型。内存内存结构如下:

在这里插入图片描述
OBJECT_ENCODING_INT这个时Redis定义的一个常量,我们可以在object.c文件中看到

char *strEncoding(int encoding) {
    switch(encoding) {
    case OBJ_ENCODING_RAW: return "raw";
    case OBJ_ENCODING_INT: return "int";
    case OBJ_ENCODING_HT: return "hashtable";
    case OBJ_ENCODING_QUICKLIST: return "quicklist";
    case OBJ_ENCODING_LISTPACK: return "listpack";
    case OBJ_ENCODING_INTSET: return "intset";
    case OBJ_ENCODING_SKIPLIST: return "skiplist";
    case OBJ_ENCODING_EMBSTR: return "embstr";
    case OBJ_ENCODING_STREAM: return "stream";
    default: return "unknown";
    }
}

Redis启动时会预先建立10000个分别存储0~9999的redisObject变量做为共享变量,这意味着如果set字符串的键值在0~10000之间的话,就可以直接指向功效变量,而不需要再建立新对象,此时键值不占内存。(这和java中有些包装类设置缓存的原理一样)。如下面两条命令:

set k1 100
set k2 100

在这里插入图片描述

上面的缓存可以再server.h中可以看到定义:

#define OBJ_SHARED_INTEGERS 10000 //缓存去大小

struct sharedObjectsStruct {
    robj *ok, *err, *emptybulk, *czero, *cone, *pong, *space,
    *queued, *null[4], *nullarray[4], *emptymap[4], *emptyset[4],
    *emptyarray, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,
    *outofrangeerr, *noscripterr, *loadingerr,
    *slowevalerr, *slowscripterr, *slowmoduleerr, *bgsaveerr,
    *masterdownerr, *roslaveerr, *execaborterr, *noautherr, *noreplicaserr,
    *busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,
    *unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink,
    *rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
    *emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,  
    *script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire, 
    *time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
    *lastid, *ping, *setid, *keepttl, *load, *createconsumer,
    *getack, *special_asterick, *special_equals, *default_username, *redacted,
    *ssubscribebulk,*sunsubscribebulk, *smessagebulk,
    *select[PROTO_SHARED_SELECT_CMDS],
    *integers[OBJ_SHARED_INTEGERS],
    *mbulkhdr[OBJ_SHARED_BULKHDR_LEN], /* "*<value>\r\n" */
    *bulkhdr[OBJ_SHARED_BULKHDR_LEN],  /* "$<value>\r\n" */
    *maphdr[OBJ_SHARED_BULKHDR_LEN],   /* "%<value>\r\n" */
    *sethdr[OBJ_SHARED_BULKHDR_LEN];   /* "~<value>\r\n" */
    sds minstring, maxstring;
};

下面我们看看redis时如何利用这些缓存起来的对象的

/* Try to encode a string object in order to save space */
robj *tryObjectEncodingEx(robj *o, int try_trim) {
    long value;
    sds s = o->ptr;
    size_t len;
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
    if (!sdsEncodedObject(o)) return o;
     if (o->refcount > 1) return o;
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }

    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. */
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    /* We can't encode the object...
     * Do the last try, and at least optimize the SDS string inside */
    if (try_trim)
        trimStringObjectIfNeeded(o, 0);

    /* Return the original object. */
    return o;
}

robj *tryObjectEncoding(robj *o) {
    return tryObjectEncodingEx(o, 1);
}

关键是下面这几句代码

if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }

value < OBJ_SHARED_INTEGERS判断当前值是否小于10000,如何小于shared.integers[value]中直接拿值,上面也是对INT类型编码的核心源码

  • embstr

上面我们分析了int类型是如何编码的,下面我们看看embstr类型是如何编码的

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
	//创建redisObject对象并分配内存,使用 zmalloc 函数分配了一块内存,用于存储字符串对象。这块内存的大小包括了字符串对象结构体 robj 的大小、sdshdr8 结构体的大小以及字符串的长度 len 加上一个字节用于字符串的结束符 \0。
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    //定义了一个 sdshdr8 结构体指针 sh,它指向了 robj 结构体之后的内存地址。这是因为 robj 结构体之后的内存空间被用来存储字符串数据的头信息。
    struct sdshdr8 *sh = (void*)(o+1);
	//类型表示为String类型
    o->type = OBJ_STRING;
    //编码为embstr编码
    o->encoding = OBJ_ENCODING_EMBSTR;
    //设置了 robj 结构体中的 ptr 字段为指向 sdshdr8 结构体之后的内存地址,即指向字符串的实际内容。
    o->ptr = sh+1;
    //引用计数为1
    o->refcount = 1;
    //使用次数0
    o->lru = 0;

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

首先len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT判断当前字符串的长度是否小于44,这前面也讲过为什么是44,如果小于则符合emstr类型,则调用createEmbeddedStringObject方法。我们看到ptr指针指向的位置为sh+1。我们从源码可以看出sh是robj 结构体位置,这就说明一个现象,即字符串SDS结构体和其对应的redisObject对象分配在同一连续内存空间,就像讲sds嵌入到redisObject中。

简单来说redisObject和对应的sdshdr8是在内存汇总紧挨着的,使用 embstr 编码时,字符串的内容直接存储在 robj 结构体之后的内存空间中,而不需要额外分配存储空间。这样做的好处是减少了内存分配和内存碎片,提高了内存使用效率。

  • raw

我们继续回到tryEncodingEx方法,如果我们对象的字节数大于44,我们此时就是创建raw类型

obj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;
    o->lru = 0;
    return o;
}

当字符串长度大于44的超长字符串时,Redis则将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR的编码方式不同,此时动态字符串sds的内存与其依赖的redisObject内存不再连续。

在这里插入图片描述
下面看一种特殊情况:
在这里插入图片描述

5. 总结

在这里插入图片描述
在这里插入图片描述
redis底层会根据用户给的键值使用不同的编码格式,自适应地选择较优化的内存编码格式,而这一切对用户完全透明!

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

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

相关文章

动态规划课堂5-----子序列问题(动态规划 + 哈希表)

目录 引言&#xff1a; 例题1&#xff1a;最长递增子序列 例题2&#xff1a;最长定差子序列 例题3&#xff1a;最长的斐波那契子序列的长度 例题4&#xff1a;最长等差数列 例题5&#xff1a;等差数列划分II-子序列 结语&#xff1a; 引言&#xff1a; 要想解决子序列问…

pc端vue2项目使用uniapp组件

项目示例下载 运行实例&#xff1a; 这是我在pc端做移动端底代码时的需求&#xff0c;只能在vue2使用&#xff0c;vue3暂时不知道怎么兼容。 安装依赖包时可能会报&#xff1a;npm install Failed to set up Chromium r756035! Set “PUPPETEER_SKIP_DOWNLOAD” env variable …

伪分布式Spark集群搭建

一、软件环境 软 件 版 本 安 装 包 VMware虚拟机 16 VMware-workstation-full-16.2.2-19200509.exe SSH连接工具 FinalShell Linux OS CentOS7.5 CentOS-7.5-x86_64-DVD-1804.iso JDK 1.8 jdk-8u161-linux-x64.tar.gz Spark 3.2.1 spark-3.2.1-bin-…

JVM的整体架构

JVM的整体架构 JVM的架构模型 基本上是基于栈的指令集架构 基于栈式架构的特点 设计和实现更简单&#xff0c;适用于资源受限的系统避开了寄存器的分配难题&#xff1a;使用零地址指令方式分配指令流中的指令大部分是零地址指令&#xff0c;其执行过程依赖于操作栈。指令集更…

AI写作一键生成原创文案,效率高!

AI写作一键生成原创文案&#xff0c;效率高&#xff01;当下时代&#xff0c;文案写作对于各个企业推广产品和服务显得隔外重要。优秀的文案能够吸引用户的注意力&#xff0c;激发购买欲望&#xff0c;从而为企业带来更多的销售机会。然而&#xff0c;对于许多人来说&#xff0…

【Python】新手入门学习:什么是工作目录?

【Python】新手入门学习&#xff1a;什么是工作目录&#xff1f; &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得…

【Python机器学习系列】自助法计算机器学习评价指标的置信区间(案例+源码)

这是我的第235篇原创文章。 一、引言 Bootstrap方法是非常有用的一种统计学上的估计方法&#xff0c;是一类非参数Monte Carlo方法&#xff0c;其实质是对观测信息进行再抽样&#xff0c;进而对总体的分布特性进行统计推断。 自助法计算分类模型的AUC、准确率、特异度和灵敏…

商业楼宇一卡通解决方案(1)

智能楼宇发展 智能楼宇也称智能建筑,又称智能大厦。智能楼宇是将建筑技术、通信技术、计算机技术和控制技术等各方面的先进科学技术相互融合、合理集成为最优化的整体,具有工程投资合理、设备高度自动化、信息管理科学、服务高效优质、使用灵活方便和环境安全舒适等特点,是…

Caffeine--实现进程缓存

本地进程缓存特点 缓存在日常开发中起着至关重要的作用, 由于存储在内存中, 数据的读取速度非常快,能大量减少对数据库的访问,减少数据库的压力. 缓存分为两类: 分布式缓存, 例如Redis: 优点: 存储容量大, 可靠性更好, 可以在集群间共享缺点: 访问缓存存在网络开销场景: 缓存数…

Python网站的搭建和html基础

1.Python网站代码及讲解 一般我们搭建小型的网站就用flask库就行了。 &#xff08;1&#xff09;安装flask库 安装完python后&#xff0c;按住windows徽标键和r,弹出“运行”&#xff0c;在里面输入cmd。 回车打开&#xff0c;输入“pip install flask”。 &#xff08;2&am…

4. C++ 类的大小

C 类的大小 ​ C类的大小&#xff0c;是一个比较经典的问题&#xff0c;学过C后&#xff0c;应该对类大小有清晰的认识&#xff0c;长话短说&#xff0c;本文精简凝练&#xff0c;我们进入正题&#xff01;&#xff01;&#xff01; 1.类的大小与什么有关系&#xff1f; 与类…

C#,数值计算,解微分方程的龙格-库塔四阶方法与源代码

Carl Runge Martin Wilhelm Kutta 1 龙格-库塔四阶方法 数值分析中,龙格-库塔法(Runge-Kutta)是用于模拟常微分方程的解的重要的一类隐式或显式迭代法。这些技术由数学家卡尔龙格和马丁威尔海姆库塔于1900年左右发明。 对于一阶精度的欧拉公式有: yi+1=yi+h*K1  K1=f(…

Portraiture2024中文版广泛应用于人像处理的磨皮美化插件

Portraiture插件是一款广泛应用于人像处理的磨皮美化插件&#xff0c;尤其在Photoshop和Lightroom等图像编辑软件中备受欢迎。这款插件能够帮助用户快速实现智能磨皮效果&#xff0c;使皮肤看起来更加平滑细腻&#xff0c;同时保留自然纹理和其他重要细节。 Portraiture for Ph…

基于JAVA实现五子棋游戏设计【附项目源码】分享

基于JAVA实现五子棋游戏设计&#xff1a; 项目源码地址&#xff1a;https://download.csdn.net/download/weixin_43894652/88842612 一、引言 五子棋&#xff0c;又称连珠、连五、五目、五目棋等&#xff0c;是一种传统的棋类游戏。本需求文档旨在详细阐述一个基于Java环境开…

LIGHTHOUSE Apex RBP应用案例|汽车涂装行业 电动汽车电池制造行业的颗粒物监测首选

Lighthouse ApexBP汽车制造中的颗粒物监测技术无疑是汽车制造领域的一项革命性发展。它不仅提供了全面、高精度的颗粒检测&#xff0c;而且能够轻松集成到现有的制造流程中&#xff0c;满足自动化需求&#xff0c;加强质量控制&#xff0c;确保电动汽车电池生产的安全性和效率。…

展览厅设计如何创新而独特

一、独特的建筑外观 展览厅的建筑外观是展览的第一印象&#xff0c;因此需要设计一个独特而有吸引力的外观。可以使用独特的建筑形态、创新的材料和结构&#xff0c;以及艺术化的立面设计。 二、灵活的展示空间 创新的展览厅设计应具备灵活的展示空间&#xff0c;以适应不同类型…

java中几种对象存储(文件存储)中间件的介绍

一、前言 在博主得到系统中使用的对象存储主要有OSS&#xff08;阿里云的对象存储&#xff09; COS&#xff08;腾讯云的对象存储&#xff09;OBS&#xff08;华为云的对象存储&#xff09;还有就是MinIO 这些玩意。其实这种东西大差不差&#xff0c;几乎实现方式都是一样&…

StringBuilder --java学习笔记

StringBuilder 代表可变字符串对象&#xff0c;相当于是一个容器&#xff0c;它里面装的字符串是可以改变的&#xff0c;就是用来操作字符串的StringBuilder比String更适合做字符串的修改操作&#xff0c;效率会更高&#xff0c;代码也会更简洁 StringBuilder的常用构造器和方…

单目标/多目标樽海鞘群优化算法——源码

目录 一、樽海鞘群优化算法&#xff1a; 二、多目标樽海鞘群优化算法&#xff1a; 三、代码运行结果&#xff1a; 四、代码下载&#xff1a; 一、樽海鞘群优化算法&#xff1a; 澳大利亚学者Seyedali Mirjalili等人于2017年提出了樽海鞘群算法&#xff0c;该算法源于对海底…

【C++】string类(介绍、常用接口)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 string类的常用接口说明 string类对象的常见构造 ​编辑 string字符串的遍历&#xff08;迭代器&#xf…