文章目录
[一,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)(重点)
拷贝构造函数
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 个字符,此时通常不释放内存。
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;
}
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;
}
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;
}
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的正确用法是提前开辟好一块空间,避免后面频繁扩容.
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)都会立即失效 ,继续使用会导致程序崩溃!
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();
属于底层内存管理接口,普通开发中极少使用 ,仅供高级定制。
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;
}
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;
}
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),效率不高,后面的数据要往后挪
//实践中需求不高
//插入位置的下标必须合法
}
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头插类似,头删完要挪动数据
}
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返回的地址不是只读的,所以可以通过地址去修改元素。
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')。
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 停止读取
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 的结构
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 的结构
历史包袱:COW (写时复制)
在 C++11 标准之前,G++ 的 string 实现广泛采用了 COW(Copy-On-Write) 技术。
原理 :当用一个 string 拷贝构造另一个时,它们共享同一块堆内存 ,并通过一个**引用计数(Reference Count)**来追踪有多少个对象共享这块内存。
触发复制 :只有当其中一个对象尝试修改 内容时,才会真正复制一份新的内存出来,从而避免不必要的拷贝,提升赋值和拷贝构造的性能。
问题 :COW 在多线程环境下需要加锁来保护引用计数,带来了额外的同步开销;同时,标准库的某些接口(如 operator[])在 COW 下的行为也很难完全符合 C++11 的严格规定。因此,C++11 标准实际上已经禁止了 COW 的实现方式 。