使用string类


  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页 吃透C语言专栏 数据结构 Gitee仓库 C++ Linux 算法

文章目录

  • [一,string 类](#一,string 类)
    • [1 c语言中的字符串](#1 c语言中的字符串)
    • [2 标准库中的 string 类](#2 标准库中的 string 类)
    • [3 编码表](#3 编码表)
    • [4 ASCLL码表](#4 ASCLL码表)
    • [5 Unicode(统一码)](#5 Unicode(统一码))
    • [6 UTF](#6 UTF)
    • [7 string类总结](#7 string类总结)
  • [二,string 类常用的接口说明](#二,string 类常用的接口说明)
    • [1 string 类对象的常见构造](#1 string 类对象的常见构造)
    • [2 string 类对象的容量操作](#2 string 类对象的容量操作)
    • [3 string 类对象的访问及遍历操作](#3 string 类对象的访问及遍历操作)
    • [4 string 类对象的修改操作](#4 string 类对象的修改操作)
    • [5 string 类非成员函数](#5 string 类非成员函数)
    • [6 vs 和 g++ 下 string 的结构说明](#6 vs 和 g++ 下 string 的结构说明)
      • [1 vs 下 string 的结构](#1 vs 下 string 的结构)
      • [2 g++ 下 string 的结构](#2 g++ 下 string 的结构)

一,string 类

1 c语言中的字符串

  • C语言中,没有字符串这个类型,只有字符类型char,而字符串是'\0'结尾的一些字符的集合,也就是只能用字符数组来表示字符串。C语言中提供的一些str类型的函数和字符串是分开的,str类型的函数想要操作字符串就必须手动接收字符数组作为参数,数据和操作是分离的,底层需要自己去管理,可能出现越界。

2 标准库中的 string 类

  • string在底层实际是:basic_string模板类的别名

类型 底层字符类型 占几个字节 对应编码 应用场景
std::string char 1 字节 UTF-8(或 ASCII) 99% 的日常开发、互联网、Linux/macOS
std::u16string char16_t 2 字节 UTF-16 极少直接用,用于对接 Windows/Java 数据
std::u32string char32_t 4 字节 UTF-32 内部算法处理(如文本排版引擎)
std::u8string char8_t 1 字节 UTF-8 C++20 新增,为了和 std::string 区分开
std::wstring wchar_t 看平台! 看平台! Windows 底层 API

  • 标准库中的string类本质上是一个类模板,而我们学习的string,其实是是string类型,通过同一个类模板实例化出来的不同的string类的区别在于这些string类中存储字符所需要的空间不一样,我们学习的string类中字符所占空间是一个字节,也就是8个比特位,而如u16string中一个字符所需要的空间是2个字节,16个比特位,而u32string中一个字符需要的空间是4字节,这些大于1字节空间所构成的字符,底层数据类型已经不是char了,而是char16_t类型,占2字节,char32_t类型占4字节,通过模板类实例化出不同数据类型的string,而string类本质上是存储字符的类,而不同数据类型的类,当然不能搞混了

3 编码表

  • 美国的符号包括字母,标点符号,数学符号之类的,这些符号我们可以叫做字符,而计算机中只能看懂,也只能执行的是二进制指令,而这些二进制指令,只能通过数字转换过来,由数字转换成二进制数字,那字符和二进制之间的桥梁就打通了,就是通过数字来进行映射,每一个字符都映射对应着一个数字,这些字符映射数字组成了一个表格,这个表格就叫做编码表。字符--映射-->数字--转换-->对应二进制-->计算机能识别

4 ASCLL码表

  • ASCLL码表就是编码表,而ASCLL是美国人弄出来的,所以ASCLL码表中字符基本都是字母,通过这些字符对应的编码,转换成对应的二进制,定位对应的字符

5 Unicode(统一码)

  • ASCLL码表中,字符都是字母,没有其他国家的文字,没有别的国家的字符,ASCLL针对的是美国的字符进行的编码,而Unicode,也叫做统一码和万国码,就是对全世界所有的字符进行编码,像一个世界字典一样,每一个字符都在Unicode中可以找到对应的编码,也就是对应的数字,而Unicode仅仅只是字典,负责编码,但是这些编码如果是按照1字节,char这种类型存储的话,汉字的数量映射的编码,1个字节,8个比特位是存不完的,8个比特位的排列组合的二进制,是无法映射出所有的汉字的,形象的说也就是一个字节所能代表的编码,也可以说是数字,是无法映射出所有的汉字的,我中国的字符有1000个,你一个字节只能存100个数字,无法映射完,而Unicode的作用仅仅是编码,字典,而这些编码该以多少字节的形式存进计算机中不是Unicode规定的。

项目 内容
名称 Unicode(统一码 / 万国码)
使命 给地球上所有语言的每一个字符,分配一个全球唯一的数字编号
专业术语 这个编号叫作「码点(Code Point)」
书写规范 通常写成 U+XXXX 的形式
示例 1:英文字母 'A' 码点是 U+0041(十进制 65)
示例 2:汉字 '中' 码点是 U+4E2D(十进制 20013)
示例 3:Emoji '😅' 码点是 U+1F602
关键认知 Unicode 不负责存储。它只是一个巨大的电子字典,里面写着:"中"这个字,它的唯一编号是 20013。至于这个 20013 在硬盘上到底占几个字节、变成什么样的 01 串,Unicode 不管。

6 UTF

  • UTF 全名 :Unicode Transformation Format。翻译: Unicode 转换格式。所以Unicode作用就是编码,而这些编码最终以多少字节,怎么存进计算机中,通过的就是UTF编码方案(Unicode转换格式方案)。统一码用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。

  • UTF的转换格式方案有三种:UTF-8,UTF-16,UTF-32,编码转换的格式方案不同
对比项 UTF-32(土豪版) UTF-16(半土豪版) UTF-8(变长版·现代主流)
规则 不管什么字符,统统固定 4 字节(32 位)存储 常用字符(英文、汉字)用 2 字节;生僻字、Emoji 用 4 字节 按需分配,1~4 字节变长存储
字节分配 全部 4 字节 常用 2 字节,生僻/Emoji 4 字节 ASCII英文字母/数字:1 字节;欧洲语言:2 字节;常用汉字(中日韩):3 字节;Emoji/生僻字:4 字节
优点 极其简单,找第 N 个字符直接偏移 4*N,速度飞快 折中方案,兼顾常用字符效率与生僻字扩展 完美兼容 ASCII(老英文软件零改动直接跑);省空间(互联网 90% 文本是英文/代码,全用 1 字节,省带宽)
缺点 极其浪费空间,纯英文 1MB 变 4MB 存纯英文仍比 UTF-8 多占一倍空间 变长导致随机访问不能直接偏移定位,需顺序扫描
使用场景 极少直接用于存储,主要用于内存中码点处理 Windows 系统底层(API)、Java 早期、C# 内部默认 现代绝对主流:网页、Linux、网络传输、数据库默认
C++ 对应 std::u32string(底层 char32_t) std::u16string(底层 char16_t) std::string(底层 char 组合)

  • UTF-8:不同字符代表的编码不同,所以如果表示所有的编码,那存储编码的字节大小要是4字节,如果是4字节,32比特位的空间大小,排列组合的二进制代表的数字就可以映射所有的字符编码,但是对于英文而言,一个字节就可以满足所有字母的编码。但是如果使用都使用四个字节来映射编码,对于ASCLL码这种只需要一个字节来存储的编码,就是浪费,但是如果都是按照1字节来存储编码,都无法覆盖到所有的字符,中文就需要3个字节才能把所有的编码都覆盖到。这种因为字符大类不同而导致的存储空间不均,UTF-8的规则则是按需分配,如果是英文用1字节去存储编码,如果是欧洲其他语言用2字节去存储编码,如果是汉字用3字节去存储,emoji或少数生僻字用做字节去存储,这样通过按需分配,不同大类的字符编码存储所需的字节不同,满足了每个大类字符编码存储所需不同字节的问题

UTF-8字节开头标识 含义说明
0xxxxxxx 1字节字符,一般是ASCII英文字符
110xxxxx 2字节字符的首字节,后面必须跟1个 10xxxxxx 后续字节
1110xxxx 3字节字符的首字节,汉字大多属于这一类
11110xxx 4字节字符的首字节,一般用于Emoji、生僻字
10xxxxxx 后续字节(小弟),不能作为一个字符的开头
  • 通过二进制中不同的开头标识,对不同大类字符存储字节数进行标记,这样识别到不同开头标识,计算机就能区分不同大类编码的长度,字节数,如果是0开头,则计算机就知道了是1字节长度的编码,如果是110开头就知道了是2字节长度的编码,以此类推

  • ❗❗❗注意,不同字节的意思就是代表存储编码所需要的内存空间的字节数不同,不要单纯得想着字符就一定要char去存,char是一种1字节的内置类型,如果编码所需要的内存是1字节,当然可以使用1字节大小的char去存储,但是如果是需要多字节存储的编码,多字节怎么实现呢,可以用多个char组合在一起,也可以使用char32_t,char 16_t这种类型,不是1字节的char,是2字节的char或者3字节的char,总之,存储空间的大小计量单位是字节,而不是char,char只是一个1字节存储空间的内置类型,不是单位,我们的目的是满足存储编码的字节数量。

7 string类总结

  • string类本质上是一个模板,因为字符在底层存储的字节数不同,string类模板需要接收各种字符,各种字符底层的编码存储大小不同,所以string类模板就需要接收不同类型的数据实例化出不同的string,而string类有着成员变量和成员函数,而成员变量就类似于顺序表中的ptr 和 size 和 capacity 来存储数据,和数据的大小,空间的容量,而对这些数据进行操作,则需要用到成员函数,成员函数就是各种不同的接口,调用各种接口对成员变量进行操作,也就是对我们存储的数据进行各种操作,增删查改,其实就是顺序表,只不过sting已经帮我们实现好了,我们是需要创建对象,然后对顺序表进行操作就可以了,而vector也是顺序表,但是和string不冲突,因为string是针对字符的顺序表,进行的操作是字符操作,所以对比起来,string中多了许多针对字符串的接口。


二,string 类常用的接口说明

1 string 类对象的常见构造

(constructor)函数名称 功能说明
string s { } (重点) 构造空的string类对象,即空字符串
string(const char* s) (重点) 用C-string来构造string类对象
string(size_t n, char c) string类对象中包含n个字符c
string(const string& s)(重点) 拷贝构造函数

c 复制代码
int main()
{
	std::string s1{};
	//隐式转换->构造常性临时对象->拷贝构造成s1->优化成直接构造
	std::string s2("hello world");
	//带参构造
	std::string s3(s2, 3);
	//初始化s2的下标3开始截取到末尾
	std::string s4(s2);
	//拷贝构造
	std::cout << s1 << std::endl;
	std::cout << s2 << std::endl;
	std::cout << s3 << std::endl;
	std::cout << s4 << std::endl;

	return 0;
}
  • string本身就是一个类,是类那就按照对类操作,string s1就是类实例化对象,实例化对象自动调用构造函数,而string s2("hello world")就是调用带参构造,string s3(s2,3)的语法拷贝构造,从s2中,第三个字符开始,长度为npos,npos 是 size_t类型的-1,无符号整型的-1代表有符号整型最大值(整型环绕),所以就可以理解,如果选好了拷贝哪个对象,从哪里开始拷贝,最后的长度不写,默认的npos代表拷贝到结尾,如果len的长度大于拷贝对象的长度没关系,只要拷贝完就停止。string s4就是拷贝构造

  • 上面就是调用构造函数初始化,初始化顺序表中的内容,不需要想的太复杂

2 string 类对象的容量操作

接口 (Interface) 功能说明 使用示例 重点解析与注意事项
size() (重点) 返回当前字符串的字符数(长度)。 str.size(); 返回类型为 size_t。时间复杂度为 O(1)。最常用的接口之一。
length() 返回当前字符串的字符数(长度)。 str.length(); 功能与 size() 完全等价 。之所以有两个,是历史遗留原因(为了兼容早期习惯),日常开发建议用 size()。
capacity() 返回当前分配给字符串的内存容量(在不重新分配内存的情况下,最多能存放多少字符)。 str.capacity(); capacity() >= size()。扩容机制通常是按倍数增长(如 1.5 或 2 倍),具体取决于编译器实现。
empty() (重点) 判断字符串是否为空(即 size() == 0)。 if(str.empty()) {...} 返回 bool 类型。语义比 size() == 0 更清晰,推荐使用。时间复杂度 O(1)。
clear() (重点) 清空字符串的内容。 str.clear(); 重点注意: clear() 仅将 size 置为 0,但通常不会释放底层内存 (capacity 保持不变)。后续添加字符不需要重新分配内存。
reserve() (重点) 预分配(请求)内存空间,将 capacity 至少设置为指定值。 str.reserve(100); 核心作用: 避免因频繁追加字符导致的多次扩容(扩容涉及重新申请内存和拷贝数据,非常耗时)。 注意: 它只改容量,不改大小。如果请求的值小于当前 capacity,通常不会做任何操作(不会缩减容量)。
resize() (重点) 调整字符串的字符数(size)。 str.resize(10); str.resize(10, 'a'); 重点注意: 1. 变大:多余的部分用指定字符(或默认的 \0)填充,此时可能会触发内存重新分配。 2. 变小:截断原字符串,保留前 n 个字符,此时通常不释放内存。

c 复制代码
void string1()
{
	std::string s1("first time");
	std::cout << s1.size() << std::endl;
	std::cout << s1.length() << std::endl;
	std::cout << s1.capacity() << std::endl;
	std::cout << s1.empty() << std::endl;
	s1.clear();
	std::cout << s1.size() << std::endl;
}

c 复制代码
void string2()
{
	std::string s1;
	std::cout << s1.capacity() << std::endl;
	s1.resize(10);
	std::cout << s1.capacity() << std::endl;
	s1.resize(100);
	std::cout << s1.capacity() << std::endl;
}

c 复制代码
void string3()
{
	std::string s1("what can i say");
	std::cout << s1.size() << std::endl;
	std::cout << s1.capacity() << std::endl;
	s1.resize(20);
	std::cout << s1.size() << std::endl;
	s1.resize(2);
	std::cout << s1.size() << std::endl;
	std::cout << s1 << std::endl;
	s1.resize(20, 'x');
	std::cout <<s1 << std::endl;
}

c 复制代码
void string4()
{
	std::string s1("what can i say");
	std::cout << s1.capacity() << std::endl;
	s1.reserve(100);
	std::cout << s1.capacity() << std::endl;
	s1.reserve(20);
	std::cout << s1.capacity() << std::endl;
	s1.reserve(10);
	std::cout << s1.capacity() << std::endl;
}

  • 1️⃣size()和length()的功能和底层原理一样,因为string顺序表主要是字符的操作所以计算有效元素个数的接口取名为length(),但是后来为了统一接口的叫法,别的计算有效元素个数的接口都size(),所以又在string中加了一个size()接口,统一接口,解决历史遗留问题

  • 2️⃣clear()只是清空有效字符并不会改变capacity空间容量

  • 3️⃣size()是计算有效元素个数,而resize()是指定有效元素个数,如果仅仅是指定个数的话,会用\0来填充不够的部分,如果指定的数量小于原本的数量,则会删除多余的有效元素。size的扩大需要扩容,所以会影响capacity,缩小不会影响capacity,size()指定元素个数的同时还可以指定元素**

  • 4️⃣reserve是指定空间容量大小(实际上最终的capacity并不一定是指定的大小,可能会多给一些,在windows中),也就是指定capacity,扩容不会影响size,但是扩容基本都是异地扩容,也就是原本空间后面的连续空间不足以扩容,需要释放原来的空间,开辟一块扩容后的空间,再把原来的数据拷贝到新的空间中。异地扩容的效率太低,消耗太大了,所以reserve的正确用法是提前开辟好一块空间,避免后面频繁扩容.

  • 补充:⬇️⬇️⬇️
c 复制代码
void string5()
{
	std::string s("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
	std::cout << "原本:"<<s.size() << std::endl;
	std::cout << "原本:"<< s.capacity() << std::endl;
	s.shrink_to_fit();
	std::cout << "现在:" << s.size() << std::endl;
	std::cout << "现在:" << s.capacity() << std::endl;
}
  • shrink_to_fit的作用是缩容,把capacity == size,但是这个和内联一样只是一个申请,编译器可以驳回申请,因为空间是不能分段释放的,只能整体释放,所以缩容是开辟一块新的空间,把旧空间的数据拷贝到新空间然后释放旧空间,所以效率是低下的

3 string 类对象的访问及遍历操作

函数名称 功能说明 核心使用示例 重点解析与注意事项
operator[] 重载了下标运算符,直接访问指定索引位置的字符。 char c = str[0]; str[1] = 'a'; 重点注意: 1. 访问极快,时间复杂度 O(1)。 2. 标准层面无越界保护 :越界属于未定义行为,Release 模式下无任何检查。 3. 在 VS 等 Debug 模式下,STL 可能会提供断言检查帮忙排错。 4. 若需绝对安全的越界检查,请使用 str.at(i)。
begin() + end() 返回正向迭代器。begin 指向首字符,end 指向尾字符的下一个位置(尾后)。 for(auto it = str.begin(); it != str.end(); ++it) 1. 传统 STL 遍历方式。 2. 绝对不能解引用 end() ,它是结束的哨兵标志。 3. 支持迭代器的泛型算法(如 std::find)都可作用于 string。
rbegin() + rend() 返回反向迭代器。rbegin 指向最后一个字符,rend 指向第一个字符的前一个位置。 for(auto rit = str.rbegin(); rit != str.rend(); ++rit) 1. 用于逆序 遍历字符串。 2. 注意:反向迭代器的 ++ 操作,实际上是让指针向字符串开头方向移动。 3. 同样绝对不能解引用 rend()。
常量迭代器 (C++11) cbegin / cend crbegin / crend 前缀 c 代表 const。强制返回只读迭代器。 for(auto it = str.cbegin(); it != str.cend(); ++it) 1. 无论原字符串是不是 const,返回的迭代器都只能读、不能改。 2. 意义:在泛型编程或只想遍历读取时,明确表达"只读"意图,提升代码安全性,防止意外修改数据。
范围 for 循环 (C++11) 语法糖,自动遍历容器内的所有元素。 只读:for(char c : str) 修改:for(char& c : str) 1. 语法最简洁,不易出错。 2. 重点 :如果需要在循环内修改字符的值,必须使用引用 &(如 char& c 或 auto& c),否则只是值拷贝,修改无效。 3. 底层编译器会自动将其转换为迭代器或下标遍历。

💡 避坑指南:迭代器失效

如果在使用迭代器遍历的过程中,对字符串进行了 push_back、insert、resize 等可能引发底层内存扩容 的操作,之前获取的所有迭代器(包括 begin/end)都会立即失效,继续使用会导致程序崩溃!


c 复制代码
void string6()
{
	std::string s("abcdefghi");
	std::cout << s[3] << std::endl;
	std::string::iterator it = s.begin();
	while (it != s.end())//begin()end()正向迭代器
	{
		std::cout << *it << "";
		++it;
	}

	std::cout << std::endl;
	
	std::string::const_iterator it1 = s.cbegin();
	while (it1 != s.cend())//cbegin()是const版本,只读迭代器
	{
		std::cout << *it1 << "";
		++it1;
	}

	std::cout << std::endl;

	std::string::reverse_iterator it2 = s.rbegin();
	while (it2 != s.rend())//反向迭代器
	{
		std::cout << *it2 << "";
		++it2;
	}

	std::cout << std::endl;

	std::string:: const_reverse_iterator it3 = s.crbegin();
	while (it3 != s.crend())//const方向迭代器
	{
		std::cout << *it3 << "";
		++it3;
	}
}
  • 1️⃣[ ]是operator[ ],运算符重载下标引用操作符,所以可以用下标引用操作符直接访问索引位置的字符,如果越界访问的话再debug版本下会断言,但是断言本身就是调试宏,所以在release版本下没有用,所以建议使用at[ ],at会抛异常

  • 2️⃣begin()和end()是正向迭代器,begin指向首字符,end指向最后一个字符的下一个位置,end没有指向有效数据,例如abcd的话,begin指向的就是a,end指向的就是\0,也就是最后一个有效元素d的下一个。所以迭代器是一个左闭右开的区间,而迭代器的作用就链接算法和容器。而begin 和 end 获取到的数据需要由一个iterator的类型变量来接收,而iterator是一个容器的类类型,可以理解为一个游走在数据间遍历的智能指针,接收了begin 和 end 传入的值,可以使用解引用*访问对应的元素,而正向迭代器的++是从左往后的。

  • 3️⃣cbegin()和cend()是const版本的正向迭代器,和begin end 的效果完全一致,但是对获取到的元素只读,不能修改,因为是const版本,所以类型不是iterator而是const_iterator

  • 4️⃣rbegin()和rend()是反向迭代器,rbegin指向的是最后一个元素,rend指向的是首元素的前一个位置,而++则是从后往前走,相当于和begin和end相比了过来,而迭代器获取的类型为reverse_iterator

  • 5️⃣crbegin()和crend()是const版本的反向迭代器,对获取的元素只读,不能修改,类型为const_reverse_iterator


  • ❗❗❗补充:begin迭代器和rbegin反向迭代器的作用就是从对象中拿到迭代器,而拿到的迭代器是什么类型的迭代器,调用哪个迭代器去拿,取决于对象的类型,如果对象是const只读类型,那从const类型对象中begin获取的迭代器,就是const版本的,调用的就是const版本,如果对象不是const只读类型,调用的就是非const版本,获取的就是非const版本的迭代器,但是如果获取的是非const版本的迭代器,可以用const版本的iterator来接收,因为权限可以缩小。所以使用begin和rbegin是通过对象是否只读来调用对应的迭代器,但是如果接收的话,权限可以缩小,但是不能放大。而cbegin 和 crbegin的对象无论是什么类型,接收的迭代器都是const类型的,cbegin和crbegin只有一种const版本的迭代器,对象是可读可改的,对象是只读的,都用const版本的来接收,而begin和rbegin有两个版本,编译器会根据对象的类型来调用最匹配的,但是如果都用const来接收呢,也都可以,因为权限缩小而已,但是没办法有两个版本,所以看匹配程度来调用。

4 string 类对象的修改操作

分类 函数名称 功能说明 核心使用示例 重点解析与注意事项
元素访问 operator[] 重载下标运算符,直接访问指定索引的字符。 char c = str[0]; 速度最快,标准层面无越界保护(越界属未定义行为)。
at 访问指定索引的字符,带越界检查。 char c = str.at(0); 如果越界,会抛出 std::out_of_range 异常,比 [] 安全。
front (C++11) 返回字符串第一个字符的引用。 char c = str.front(); 等价于 str[0],如果字符串为空,调用是未定义行为。
back (C++11) 返回字符串最后一个字符的引用。 char c = str.back(); 等价于 str[str.size() - 1],空字符串调用是未定义行为。
修改器 push_back 在字符串尾部追加单个字符。 str.push_back('a'); 如果触发扩容,会重新分配内存,导致原有迭代器失效。
pop_back (C++11) 删除字符串尾部的单个字符。 str.pop_back(); 字符串为空时调用是未定义行为,不会报错但可能崩溃。
append 在字符串尾部追加字符串或子串。 str.append("world"); 可追加整个字符串、字符数组或指定长度/范围的子串。
operator+= (重点) 重载追加运算符,可追加字符、字符串、字符数组。 str += "world"; 最常用、最直观的拼接方式,代码可读性极高。
assign 清空原内容,并重新赋值为新内容。 str.assign("new"); 类似于重新初始化,可赋字符串、字符数组或指定数量的字符。
insert 在指定位置(索引)插入字符或字符串。 str.insert(2, "abc"); 在下标 2 处插入 "abc"。插入操作可能会导致底层扩容和迭代器失效。如果只是插入1个字符,必须声明数量
erase 删除指定位置或范围的字符。 str.erase(2, 3); 从下标 2 开始删除 3 个字符。删除后迭代器也可能失效。删除数量默认值是npos
replace 替换指定范围内的字符。 str.replace(2, 3, "abc"); 将下标 2 开始的 3 个字符替换为 "abc"。
c_str(重点)* 返回指向 C 风格字符串(const char*)的指针。 const char* p = str.c_str(); 返回的指针指向底层数组,以 \0 结尾。常用于与 C 语言 API 交互。
data 返回指向底层字符数组的指针。 const char* p = str.data(); 在 C++11 及以后,功能和 c_str() 基本一致,也保证末尾有 \0。
copy 将子串拷贝到指定的字符数组中。 str.copy(buf, 5, 2); 从下标 2 开始拷贝 5 个字符到 buf。注意:它不会自动在末尾添加 \0。
find + npos (重点) 从前往后查找子串或字符,返回第一次出现的索引。 size_t pos = str.find("abc"); 找不到时返回 std::string::npos(通常是一个极大的无符号数)。
rfind 从后往前查找子串或字符,返回最后一次出现的索引。 size_t pos = str.rfind('a'); 反向查找,找不到同样返回 npos。
正向找集合中的任意一个出现的第一个位置的下标 find_first_of 查找任意一个目标字符集中字符第一次出现的位置。 str.find_first_of("aeiou"); 比如找第一个元音字母。只要匹配集合中任意一个字符即可。
从后往前找 find_last_of 查找任意一个目标字符集中字符最后一次出现的位置。 str.find_last_of("aeiou"); 从后往前找匹配集合中任意字符。
正向找不是集合中的第一个出现的位置的下标 find_first_not_of 查找第一个不包含在目标字符集中的字符。 str.find_first_not_of(" \t"); 常用于跳过前导空白字符。
从后往前找 find_last_not_of 查找最后一个不包含在目标字符集中的字符。 str.find_last_not_of(" \t"); 常用于去除尾部空白字符。
substr 从指定位置开始截取指定长度的子串。 std::string sub = str.substr(2, 5); 从下标 2 开始截取 5 个字符。如果省略长度,则截取到末尾。
compare 比较两个字符串的字典序。 int res = str.compare("abc"); 相等返回 0,大于返回正数,小于返回负数。
get_allocator 返回字符串使用的内存分配器(Allocator)的拷贝。 auto alloc = str.get_allocator(); 属于底层内存管理接口,普通开发中极少使用,仅供高级定制。

c 复制代码
void string7()
{
	std::string s("abcd");
	std::cout << s.front() << std::endl;//返回首字符的引用
	std::cout << s.back() << std::endl;//返回尾字符的引用
	s.push_back('e');//尾插1个字符
	std::cout << "尾插后:" << s << std::endl;
	s.pop_back();//尾删1个字符
	std::cout << "尾删后:" << s << std::endl;
	s.append("xxxx");//追加字符串
	std::cout << "追加字符串后:" << s << std::endl;
}

c 复制代码
void string8()
{
	std::string s1("ab");
	std::string s2("cd");
	std::cout << "s1是:" << s1 << std::endl<< "s2是:" << s2 << std::endl;
	s1 += s2;
	std::cout << "s1 += s2后:" << (s1 += s2) << std::endl;
	std::cout << "s1 += x 后:" << (s1 += 'x') << std::endl;
	std::cout << "s1 += xx 后:" << (s1 += 'xx') << std::endl;
	std::cout << "assign初始化s1后:" << (s1.assign("xxxxx")) << std::endl;
}

c 复制代码
void string9()
{
	std::string s1("bc");
	std::string s2("xxxxxx");
	std::cout << s1 << std::endl;
	s1.insert(s1.begin(), 'a');
	std::cout << s1 << std::endl;
	s1.insert(0, "abc");
	//插入多个字符只需要指定插入位置
	std::cout << s1 << std::endl;
	s1.insert(0, 1, 'x');
	//如果只插入1个字符,则必须中间声明数量
	std::cout << s1 << std::endl;
	s1.insert(s1.end(), s2.begin(), s2.end());
	//迭代器玩法
	std::cout << s1 << std::endl;
	//insert(头插)慎用,O(n),效率不高,后面的数据要往后挪
	//实践中需求不高
	//插入位置的下标必须合法
}

c 复制代码
void string10()
{
	std::string s1("abcdef");
	std::cout << s1 << std::endl;
	s1.erase(0, 1);//哪个位置开始,删几个
	std::cout << s1 << std::endl;
	s1.erase(0);//不指定删除数量则删除npos个,直接删到尾
	std::cout << s1 << std::endl;
	std::string s2("xx");
	std::cout << s2 << std::endl;
	s2.erase(0, 20);//删多了没事,删完就自动停下来
	std::cout << s2 << std::endl;
	//erase头删,效率不高和insert头插类似,头删完要挪动数据
}

c 复制代码
void string11()
{
	std::string s1("abcd");
	std::cout << s1 << std::endl;
	s1.replace(0, 2,"xxxx");//0下标开始,后面的两个字符替换成xxxx
	//哪个位置开始的几个字符替换成....
	//会挪动数据,效率不高
	std::cout << s1 << std::endl;
	std::string s2("utf");
	const char* p = s2.c_str();
	std::cout << *p << std::endl;
	const char* p1 = s2.data();
	std::cout << *p << std::endl;

	s2.copy(&s1[0], 4, 0);
	std::cout << s2 << std::endl;

}

C++ 标准版本 c_str() 的特性 data() 的特性 核心区别与总结
1. C++98/03 (旧标准) 保证返回的字符数组末尾必须有 \0。 不保证 末尾有 \0,仅返回底层数组起始地址。 你的理解完全正确。在旧标准中,data() 不能直接当作 C 风格字符串使用,容易导致越界读取。
2. C++11 及以后 (现代标准) 保证返回的字符数组末尾必须有 \0。 也保证 末尾有 \0 了。 两者完全等价 。从 C++11 开始,标准规定 string 底层必须自带 \0,因此两者都返回 const char*,功能一模一样。
3. C++17 (修改权限差异) 永远只返回 const char*(只读)。 新增了非 const 版本:返回 char*(可读可写,前提是 string 对象本身是非 const)。 这是目前两者最大的区别: 1. 传 C API 只读时用 c_str(); 2. 需要直接修改底层字符数组时用 data()。

  • 总结:c_str和data的功能都是返回地址,没有任何区别,无论是什么时候C和C++中标准的字符串就是以\0结尾的连续字符,如果没有\0就是字符数组,而c_str和data的作用都只能取出地址就行了,不会有任何检查,但是C语言中的字符串函数对字符串的操作是读取到字符串中的\0才代表读取完整个字符串,而字符数组是不一定有\0的,所以如果拿字符数组的地址去给C语言中的字符串函数操作就会产生越界访问,但是我们拿出来的地址是带有\0的字符串的地址,还是没有\0的字符数组的地址,只有程序员自己心里清楚,而c_str 和 data 的作用都只有取出地址,但是我们程序员自己心里清楚有没有\0,所以就写出c_str提醒我们自己取出来的地址是有\0的,而data则是提醒我们自己,取出来的地址指向的字符串不一定有\0,所以编译器根本就不会检测之类的,c_str和data也就是取地址,而C语言的字符串函数依然只按照规定读取到\0才算字符串读取完成,但是后来,规定了sting中的字符数组自动带\0,所以c_str 和 data取出来的地址,一定是带有\0的,所以这时候c_str 和 data就更没有区别了,唯一的区别是c_str 返回的地址是const类型的,是只读的,不可能通过地址去修改元素,而data返回的地址不是只读的,所以可以通过地址去修改元素。

c 复制代码
void string12()
{
	std::string s1("string:cpp.zip");
	std::string s2(":");
	size_t pos1 = s1.find("zip");
	size_t pos2 = s1.find(s2);
	size_t pos3 = s1.find("p");

	//在s1中去寻找,zip,s2,p,如果不指定位置,则从pos = 0开始找,从头开始,也可以指定pos
	//找到后返回第一次出现的下标,类型是size_t类型
	//所以只写,需要找什么的话,从头开始找,一直找到尾的意思
	std::string s3 = s1.substr(pos1, 3);
	std::cout << s3 << std::endl;
	//substr就是sub string的意思,截取字符串的意思,(从哪个位置开始,截多少个字符)
	//不写找多少个默认是npos也就是截到尾
	//不写从哪里开始截,默认从头开始截
	//所以什么都不写,直接调用的话,就是把字符串中的所有字符截取下来的意思
	//返回值是一个新的对象,返回值类型是std::string
	std::cout << s1.substr() << std::endl;

	size_t pos4 = s1.rfind("cpp", 10);
	//rfind就是反向查找,起始位置在最右边,从右往左找
	//10代表从下标为10的起始位置从右往左找
	std::cout << s1.substr(pos4, 3) << std::endl;

}

5 string 类非成员函数

接口名称 功能说明 核心使用示例 重点解析与注意事项
operator+ 字符串拼接。将两个字符串(或字符串与字符/字符数组)拼接成一个新的字符串。 std::string s3 = s1 + s2; 1. 返回的是一个全新的对象 ,原字符串不会被改变。 2. 会触发内存分配和深拷贝,效率低于 operator+=。不推荐在循环中频繁使用。
operator>> (重点) 输入流提取运算符。从输入流(如 cin)中读取字符串。 std::cin >> str; 遇到空白字符(空格、制表符、换行)就会停止读取! 只能读取单个"单词"。如果输入 "hello world",str 只会得到 "hello"。
operator<< (重点) 输出流插入运算符。将字符串输出到流(如 cout、文件流)。 std::cout << str; 最常用的输出方式,会完整输出字符串的内容,遇到 \0 停止。
getline (重点) 从输入流中读取一整行,直到遇到换行符。 std::getline(std::cin, str); 1. 可以读取包含空格的一整行 ,这是它与 cin >> 最大的区别。 2. 经典大坑 :如果先用 cin >> 再紧接着用 getline,缓冲区残留的换行符会导致 getline 直接读取到一个空字符串。需用 cin.ignore() 清理缓冲区。
relational operators (重点) 关系运算符(==、!=、<、>、<=、>=)。用于比较两个字符串。 if (s1 == s2) {...} 1. 比较规则是字典序(Lexicographical comparison) 。 2. 逐个字符按 ASCII 码值比较,一旦分出大小立刻返回。比如 "apple" < "banana"(因为 'a' < 'b')。 3. 比较时区分大小写('A' < 'a')。

c 复制代码
void string14()
{
	std::string s1("ab");
	std::string s2("cd");
	std::string s3 = s1 + s2;
	std::cout << "s1 = "<< s1 << std::endl;
	std::cout << "s2 = "<< s2 << std::endl;
	std::cout << "s1 + s2 = " << s3 << std::endl;
	std::string s4,s5;
	std::cin >> s4;
	std::cout << s4 << std::endl;
	std::getline(std::cin, s4);
	std::cout << s4 << std::endl;
	std::getline(std::cin, s5, 'i');
	std::cout << s5 << std::endl;
}
  • operator+是运算符重载,运算符重载参数中必须要有一个是自定义类型,所以不能"xxx" + "xxx",做成全局函数就是为了能"字符串"+自定义类型,因为如果是成员函数,第一个参数是this,''xxx'' + s1 就是错误的,因为运算符重载的形参的顺序和操作数的顺序是一致的,这种写法就是''xxxx''调用运算符重载函数,把s1作为参数,但是调用成员函数只能是对象,所以为了支持这种写法只能把operator +重载成全局函数了

  • cin 和 scanf 默认规定的是空格或者换行是多个值之间的分隔,所以cin 和 scanf 是不适合读取中间带空间的字符串的,因为遇到了空格就自动读取结束。但是为了能够读取到带空格的字符串,getline就可以做到从输入流中读取一整行,空间不再是多个值之间的分隔,只有换行才是。而cin读取what can i say 的时候遇到了what后面的空格所以直接结束读取,打印出了what,但是 can i say 还在缓冲区中,所以执行到getline(std::cin s4)的时候,发现缓冲区里面还有东西,直接当成你输入的了,直接进行了读取,然后打印了出来,而getline中最后一个参数可以指定遇到什么字符分割,指定了遇到 i 停止读取

c 复制代码
void string15()
{
	std::string s1("abcd");
	std::string s2("abcdef");
	if (s2 > s1)
	{
		std::cout << "s2 大于 s1" << std::endl;
	}
}
  • 按照字典序进行比较,也就是按照ASCLL码表进行比较大小,只要比较出来大小,直接返回结果,后面的不再进行比较

6 vs 和 g++ 下 string 的结构说明

1 vs 下 string 的结构

c 复制代码
union _Bxty
{ // storage for small buffer or pointer to larger one
 value_type _Buf[_BUF_SIZE];
 pointer _Ptr;
 char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
  • MSVC的实现主要围绕短字符串优化(SSO)设计,旨在避免为短字符串频繁分配堆内存

  • 对象大小 :在 32 位系统中,sizeof(std::string) 通常为 28 字节 ;在 64 位系统中,由于指针和对齐因素,通常为 32 字节

  • 内存布局(简化) :它的内部结构通常包含一个联合体 Union和几个管理字段:

    • 联合体 :这是实现 SSO 的关键。它同时定义了两种存储方式:
      • 一个16字节的字符数组 _Buf,用于存放短字符串。
      • 一个指向堆内存的字符指针 _Ptr,用于存放长字符串。
    • _Mysize:一个 size_t 字段,记录字符串的实际长度。
    • _Myres:一个 size_t 字段,记录容量(Capacity)。
  • SSO 机制 :当字符串长度小于16时,数据直接存放在内部 _Buf 数组中,不进行堆分配 ;超过此长度,才会在堆上分配内存,并用 _Ptr 指向它。这种方式对短字符串非常高效,因为数据就在栈上,访问速度快。


2 g++ 下 string 的结构

  • G++ 的实现更为复杂,经历了从 COW 到 SSO 的演进。

  • 现代实现:SSO

    • 自 GCC 5.1 版本起,libstdc++ 引入了全新的 ABI(_GLIBCXX_USE_CXX11_ABI=1),其 string 实现也转向了 SSO。
    • 对象大小 :在现代 64 位 G++ 中,sizeof(std::string) 通常是 32 字节。
    • 内存布局(简化) :其设计非常精妙,旨在极致地节省空间:
      • _M_dataplus :一个包含数据指针的"hider"对象。
      • _M_string_length:记录字符串长度。
      • 联合体(Union) :这是 SSO 的核心。
        • 一个15字节的本地字符缓冲区 _M_local_buf(用于短字符串)。
        • 一个 size_type 类型的 _M_allocated_capacity(用于长字符串时记录容量)。
    • SSO 机制 :当字符串长度不超过15 时,数据存放在 _M_local_buf 中,_M_dataplus 中的指针指向这个内部缓冲区;当长度超过15时,指针会指向堆内存

  • 历史包袱:COW (写时复制)
    • 在 C++11 标准之前,G++ 的 string 实现广泛采用了 COW(Copy-On-Write) 技术。
    • 原理 :当用一个 string 拷贝构造另一个时,它们共享同一块堆内存,并通过一个**引用计数(Reference Count)**来追踪有多少个对象共享这块内存。
    • 触发复制 :只有当其中一个对象尝试修改内容时,才会真正复制一份新的内存出来,从而避免不必要的拷贝,提升赋值和拷贝构造的性能。
    • 问题 :COW 在多线程环境下需要加锁来保护引用计数,带来了额外的同步开销;同时,标准库的某些接口(如 operator[])在 COW 下的行为也很难完全符合 C++11 的严格规定。因此,C++11 标准实际上已经禁止了 COW 的实现方式。

  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页 吃透C语言专栏 数据结构 Gitee仓库 C++ Linux 算法

相关推荐
xxxiugou1231 小时前
链表算法题精讲:从两数相加到K组翻转
数据结构·c++·算法·链表
HAPPY酷1 小时前
C++ 自定义内存池优化点云处理:从裸指针到 RAII 自动回收
开发语言·c++
飞鸟真人1 小时前
IM网关系列(九)单账号流量统计:滑动窗口桶方案设计与Go实现
后端·golang
武子康2 小时前
Embedding 替换怎么评测:把索引成本算进成功任务
人工智能·后端·agent
zhangdaliangmvp2 小时前
快速排序的学习理解:从基础分治到三路划分优化
c++·学习·算法
cu1432 小时前
细谈IT6563的具体功能与其应用
c语言·c++·单片机·嵌入式硬件
布莱克6053 小时前
C++ 内存泄漏全解析:常见场景、成因与规避实践
开发语言·c++·内存泄露
jimy13 小时前
lvalue、xvalue、prvalue(一):三者的分类
开发语言·c++
HAPPY酷4 小时前
C++ 内存泄漏排查:从裸 new 到 RAII 修复,附 ASan 实战
开发语言·c++