欢迎拜访 :Madison-No7个人主页
文章主题:string类的常见接口的使用
隶属专栏 :我的C++成长日志
写作日期:2025年9月27号
目录
[2.1 string的构造函数](#2.1 string的构造函数)
[2.2 string 的析构函数](#2.2 string 的析构函数)
[2.3 string的常见容量接口](#2.3 string的常见容量接口)
[2.4 遍历string中的元素](#2.4 遍历string中的元素)
[(2)Iterator 迭代器](#(2)Iterator 迭代器)
[2.5 与对象修改有关的操作](#2.5 与对象修改有关的操作)
[2.6 与查找有关的接口](#2.6 与查找有关的接口)
[2.7 string类的非成员函数](#2.7 string类的非成员函数)
一、初识string
- string是表示字符串的字符串类,可以理解为字符顺序表
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
- 不能操作多字节或者变长字符的序列。
注意:在使用string类时,必须包含#include<string>以及using namespace std**;
二、string类的常见接口的使用
2.1 string的构造函数

📖默认构造string()
**说明:**构造一个长度为0个字符的空字符串。
📖用字符串构造string:string (const char* s)
说明:用C-string来构造string类对象。
📖拷贝构造string (const string& str)
说明:使用一个string对象拷贝构造一个新的string对象
📖string(const string & str, size_t pos, size_t len = npos)
说明:拷贝str 中从字符位置pos 开始并跨越len 个字符的部分(如果str太短 或len为string::npos, 则复制到str的末尾)。
注意:string对象的第一个有效字符的下标是0。

其中string::npos是string类中的静态成员变量,默认为-1,因为它是size_t类型,实际上就是整型的最大值了,大概是42亿多字节,4G左右,一个string对象不会有4G那么大,所以npos完全够用。如果在调用时,不显示设置len的值,那就从pos开始复制到str的末尾。
不显示设置len:

显示设置len:
📖初始化字符串s前n个字符:string (const char* s, size_t n)

📖初始化n个字符C:string(size_t n, char c)

2.2 string 的析构函数

销毁字符串对象。
2.3 string的常见容量接口
📖size()/length()**
返回字符串有效字符的长度(不包含'\0'),以字节为单位。
cpp
string s1("hello world");
cout << s1.size() << endl;//推荐使用size
cout << s1.length() << endl;//length也可以求字符串长度,为了更好的兼容C语言

📖capacity()**
返回当前为字符串分配的存储空间( 不包含'\0'**)**的大小,表示能存多少个有效字符不算\0,以字节表示。
cpp
string s1("hello world");
cout << s1.capacity() << endl;

为什么s1的容量是15呢?就需要来看看capacity的扩容机制了。
capacity扩容机制:
写一个测试容量变化的代码,向一个对象中循环插入字符,只要容量变了就打印来看看。
在VS2022下运行:
cpp
void testcapacitygrow()
{
string s;
size_t sc = s.capacity();
cout << "0个字符的string容量:" << sc<<endl;
cout << "容量变化的过程:" << endl;
for (int i=0;i<100;i++)
{
s.push_back('a');//向s对象中插入字符a
if (sc!=s.capacity())
{
sc = s.capacity();
cout << "目前容量:" << sc << endl;
}
}
}

实际上开的空间要多一个字节,多的一个字节是'\0','\0'不算作有效的空间。
所以我们不难发现,第一次是2倍扩 ,后续就是1.5倍扩了。
VS在底层做了特殊的处理,当所需要的空间小于16字节时,会把字符串存到栈上buf的数组里。大于16字节时,此时buf数组废弃不用了,会去堆上开辟一块空间,存到堆上。
Vs下,string的结构:
cpp
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;
string这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建
好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。
把同样的代码放在linux 下用g++编译并运行:

可见在Linux下,是标准的2倍扩容。
小Tips: 在不同的编译器下,扩容机制是不一样的。
📖reserve(size_t n=0)
为了避免频繁的扩容,用reserve()可以提前开辟空间,也就是在内存中预留n个空间,避免后续的频繁扩容,提高效率。
也就是我们中午去学校食堂吃饭,人很多,你去的时候,占下一个位置,别人看来就是这个位置有人了,当你打完饭,就直接可以到你占的位置就餐,就不需要找位置了,节约了一些时间,也就是提高了效率。

Vs下提前开100个字节的空间,但是开了比100大的空间。

g++下就是要100就给100,但是有些时候,为了内存对齐,会多一些空间。


***小Tips:***所以在不同的编译器下,所预留的空间大小是不确定的,但是一定大于你想要预留空间的大小n。
注意:返回string开辟或预留的空间的大小都不包含\0,实际在底层开的空间大小都要大一个字节,以存储'\0'。也就是说在Vs下预留空间111,实际上在底层是112字节。
那reserve()会不会缩容呢?


我们假设给一个字符串长度为20,容量为31,当reserve(n)中n<20时,是不会缩容的,因为这个函数不能影响string的长度和内容。当20<n<31,是不确定的,不同的编译器情况是不同的,当n>31时,会扩容。
在Vs2022编译器下调试缩容情况:以下是测试代码
cpp
void test_reserve()
{
string s("12345678919876543219");
cout << "size:" << s.size() << endl;
cout <<"capacity:" << s.capacity() << endl;
s.reserve(15);
cout << "n<20时,容量为:" << s.capacity() << endl;
s.reserve(25);
cout << "20<n<31时,容量为:" << s.capacity() << endl;
s.reserve(35);
cout << "31<n时,容量为:" << s.capacity() << endl;
}

我们假设给一个字符串长度为20,容量为31,测试预留不同的空间(n),缩容的情况;
可见VS2022的策略是:预留空间(n)小于capacity时,容量就不变化,大于预留空间(n)时,就扩容。
来看看在Linux操作系统的g++编辑器下的情况:一样的测试代码


可见在g++编辑器下,reserve()不会缩容。
**小Tips:**所以在VS和g++下,都不会缩容。
2.4 遍历string中的元素
有三种方法:
(1)operator[]
表示获取字符串的字符;


s1[0]会去调用operator[]函数,参数为字符串pos位置的下标,返回的是 pos位置字符的引用,因为string中的字符是存在堆上的,出函数不会销毁。operator[]能获取pos位置的字符,对于普通对象,指定字符串的下标能修改该位置的值。该函数的越界检查是断言检测。
既然operator[]能获取pos位置的字符,我们就能像用数组一样,遍历string中的内容。

(2)Iterator 迭代器

迭代器(Iterator) 是一种用于遍历容器(如**
vector
、list
、map
** 等)中元素的对象,它提供了统一的访问接口,使得开发者可以不依赖容器的具体实现来操作元素。迭代器的作用类似于指针,但比指针更通用 ------ 它可以适配不同的数据结构(数组、链表、树等),让遍历操作变得一致。
迭代器体现了封装的思想,因为它屏蔽了底层的实现细节,提供了统一的类似访问容器的方式,不需要关心容器底层结构和实现的细节。
***小Tips:***迭代器用于遍历和访问容器的。掌握了迭代器,就可以访问所有的容器。
📖begin():

表示返回一个指向字符串第一个字符的迭代器。
📖end():

表示返回一个指向字符串末尾的下一个字符的迭代器。
📖rbegin():
表示返回一个指向字符串最后一个字符的反向迭代器(即它的反向开头)。
📖rend():
返回一个反向迭代器,指向字符串第一个字符前面的理论元素(被认为是字符串的反向结束)。
📖cbegin():
表示返回指向字符串第一个字符的const_iterator。
📖cend():
表示返回一个const_iterator ,指向字符串的后结束字符。
📖crbegin():
表示返回一个const_reverse_iterator,指向字符串的最后一个字符。
📖crend():
表示返回一个const_reverse_iterator,指向字符串第一个字符前面的理论字符。
📖正向迭代器访问string:可读可写
cpp
string s1("hello world");
//it类似于指针
string::iterator it = s1.begin();
while (it!=s1.end())
{
cout << *it << " ";
it++;
}


it接收的是指向s1对象的字符串首字符的迭代器。*it类似于指针的解引用,it++类似于指针的移动,这里的*和++都做了运算符的重载,关于如何重载的,后续再做讲解。
此外,普通迭代器可以修改。但是const修饰的迭代器就只能读,不能写了。

📖反向迭代器访问string:(倒着遍历)可读可写
cpp
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
rit++;//注意这里还是++,反向即倒着走
}


📖const正向迭代器访问string:只能读,不能写
用于遍历常量字符串
cpp
const string s2(s1);
string::const_iterator cit = s2.cbegin();//s2.cbegin()返回const迭代器
while (cit!= s2.cend())
{
cout << *cit << " ";
cit++;
}

类似于const int * a,自己的指向能修改,指向的内容不能修改。
***注意:***cbegin()和cend()得匹配使用。
📖const反向迭代器访问string:只能读,不能写
用于遍历常量字符串
cpp
string::const_reverse_iterator crit = s2.crbegin();
while (crit != s2.crend())
{
cout << *crit << " ";
crit++;//注意这里还是++,反向即倒着走
}
cout << endl;

注意:const_reverse_iterator 是一个类型,他们之间使用**_**连接。
(3)范围for(C++11支持):
cpp
//自动复制,自动迭代,自动判断结束
string s1("hello world");
for (auto ch : s1)
{
cout << ch << " ";
}
auto ch : s1 表示:从字符串s1中依次取出每个字符,赋值给变量ch(auto会自动推导出ch的类型为char),auto下面会具体讲解。
for 循环后的括号由冒号 " : " 分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围。
📖**范围for的特点:自动复制,自动迭代,自动判断结束
- 自动迭代:无需手动获取迭代器或控制索引,自动从容器第一个元素遍历到最后一个元素。
- **自动复制:**自动将遍历到的字符拷贝给迭代变量(上面是ch)。
- 自动判断结束:当遍历字符串的\0时,自动结束。
若需要修改原字符串中的字符,应使用引用类型。
cpp
string s1("hello world");
for (auto& ch : s1)
{
cout << ch << " ";
}
若只需读取元素(不修改),建议用 const 引用:for (const int& num : nums) ,避免不必要的拷贝,提高效率。
cpp
string s1("hello world");
for (const auto& ch : s1)
{
cout << ch << " ";
}
📖范围for与迭代器的关系:
范围 for 循环的底层实现依赖容器的迭代器(调用 begin() 和 end() 获取范围),因此自定义容器若要支持范围 for,需实现 begin() 和 end() 方法并返回合法迭代器。
📖建议使用情景:
范围 for 循环特别适合需要完整遍历容器 且无需手动控制索引 / 迭代器的场景,能显著简化代码,是 C++ 中推荐的遍历方式之一。
补充知识: auto关键字
auto
是一个类型说明符,主要作用是自动推导变量的类型 ,让编译器根据初始化表达式的类型 来确定变量的具体类型,从而简化代码书写并提高灵活性。
cpp
int main()
{
auto b = 10;
auto c = 'a';
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}

编译器在编译时期, 根据等号右侧的值推断变量类型。其中**typeid().name()
** 是用于获取类型名称的机制。
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加**&**

- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际****只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
cpp
//编译器报错,变量q和w类型不同,"auto"必须推倒同一类型
//auto q = 20, w = 20.0;
- auto****不能作为函数的参数,可以做返回值,但是建议谨慎使用
- auto****不能直接用来声明数组
auto用武之地:简化代码
cpp
const string s2(s1);
//传统写法,类型冗长
//string::const_iterator cit = s2.cbegin();
// 使用auto:自动推导为迭代器类型
auto cit = s2.cbegin();
while (cit!= s2.cend())
{
cout << *cit << " ";
cit++;
}
2.5 与对象修改有关的操作
特别说明: **operator+=**在以后用的比较多,其他的要用的时候,查查文档即可。
(1)operator =

表示为当前字符串赋一个新值,替换其当前内容。
cpp
int main()
{
string s1("hello world");
string s2("你好,世界");
//将一个string对象赋值给另一个对象
s2 = s1;
cout << s2 << endl;
//将一个字符串赋值给已存在的string对象
s2 = "你好,C++";
cout << s2 << endl;
//将一个字符赋值给已存在的string对象
s2 = 'c';
cout << s2 << endl;
return 0;
}

(2)void push_back(char c)
表示将字符c追加到字符串的末尾,使其长度增加1。

(3)append
表示追加一个字符串的拷贝到当前string对象

cpp
void test07()
{
string s1("hello ");
string s2("world!");
cout << "追加前的string->" << s1 << endl;
s1 = "hello";
s1.append(s2);
cout << "追加一个string对象的拷贝->" << s1 << endl;
s1 = "hello";
s1.append(s2,1,3);
cout << "追加一个string对象的拷贝,从下标为1处开始,跨越3个字符->" << s1 << endl;
s1 = "hello";
s1.append("C++");
cout << "追加一个字符串C++->" << s1 << endl;
s1 = "hello";
s1.append("C++",1);
cout << "追加一个字符串C++的前1个字符->" << s1 << endl;
s1 = "hello";
s1.append(5, 's');
cout << "追加5个字符s->" << s1 << endl;
}

(4)operator+=
表示在当前对象的末尾追加字符串来扩展字符串。

cpp
void test08()
{
string s1 = ("hello ");
string s2 = ("world");
cout << "追加前->" << s1 << endl;
s1 = "hello ";
s1 += s2;
cout << "追加string对象的拷贝->" << s1 << endl;
s1 = "hello ";
s1 += "world";
cout << "追加字符串s->" << s1 << endl;
s1 = "hello ";
s1 += 'c';
cout << "追加字符c->" << s1 << endl;
}

📖小Tips:
append()和operator+=都是追加字符串的,说明C++在这里设计的有点冗余了,追加字符使用operator+=相对来说更方便一些。
(5)erase
表示删除字符串的一部分,减少其长度。可间接实现头删,尾删,任意位置的删除。

cpp
void test09()
{
string s1("hello");
cout << "删除前->" << s1<<endl;
s1.erase(0,4);
cout << "删除从位置0开始,跨越4字符的部分->" << s1 << endl;
s1 = "hello";
s1.erase(s1.begin());
cout << "删除第一个位置的字符->" << s1 << endl;
s1 = "hello";
s1.erase(++s1.begin(),--s1.end());
cout << "删除[1,最后一个元素)范围的字符串->" << s1 << endl;
}

📖小Tips:
频繁的插入和删除会影响效率,因为string就相当于动态顺序表,会涉及到挪动数据。
(6)push_back
void push_back (char c);
表示在字符串后尾插字符C;此外,与之相对的是pop_back,表示删除字符串的最后一个字符。
除了上面介绍的一些常用的字符串修改接口外,还有一些不太常用的,例如:assign(内容替换)、insert(指定位置插入)、erase(删除)、replace(部分替换)、swap(交换两个字符串),它们的使用方法都大同小异。
值得注意的是insert、erase、replace要谨慎使用,它们都是性能杀手。因为如果你频繁调用它们,可能会频繁扩容和挪动数据。
2.6 与查找有关的接口
(1)find
从字符串的pos位置开始往后查找字符或字符串,返回其在当前字符串中的位置

cpp
void test10()
{
string s1("The secret of success is constancy to purpose.");
string s2("success");
size_t a=s1.find(s2,0);
cout <<"从0位置开始找,第一次找到s2对象的内容的下标是:"<< a << endl;
size_t b = s1.find("constancy",14);
cout << "从下标为14位置开始找,第一次找到"constancy"的下标是:" << b << endl;
size_t c = s1.find("constancy",14,5);
cout << "从下标为14位置开始找,第一次找到"constancy"的前5个字符的下标是:" << c << endl;
size_t d = s1.find('z',14);
cout << "从下标为14位置开始找,第一次找到'z'的下标是:" << d << endl;
}

***注意:***该函数的返回值是第一个匹配的第一个字符的位置。如果没有找到匹配项,函数返回string::npos。npos表示整型最大值。
(2)c_str
const char* c_str() const;
返回一个指向字符串对象的指针,该字符串对象包含一个以空结尾的字符序列(即C-string),返回值类型是const char*。
cpp
void test11()
{
string s1("hello C++");
const char* str=s1.c_str();
cout << str << endl;
cout << (const void*)str << endl;
printf("%p",str);
}

c_str的返回值是const char*指针类型,那str打印应该是地址呀?为什么是字符串呢?
原因是: C++ 的输出流(std::cout)对 char* 或 const char* 类型的指针做了重载处理, 对于其他类型指针(如 int*、void* 等),默认输出地址。当检测到指针指向的是字符类型时,它会默认将其视为C 风格字符串 (以 \0 结尾的字符序列),并从指针指向的位置开始,依次输出字符,直到遇到字符串结束符 \0 为止。这是为了方便字符串的输出,符合日常使用习惯。
如果想打印指针的地址: 需要通过强制类型转换 ,将 const char* 转为无类型指针(const void*) ,cout 对 void* 类型会输出地址。
c_str接口的作用:
许多 C 语言库函数(如
strlen
、strcmp
、printf
等)或遵循 C 风格的 API 要求传入以\0
结尾的const char*
类型字符串。c_str()
提供了从 C++string
到 C 风格字符串的转换,实现了两种字符串类型的兼容。
cpp
#include<stdio.h>
#include<string>
using namespace std;
int main()
{
string s1 = "hello C++";
//使用C语言的printf输出,需要C风格字符串
printf("%s\n",s1.c_str());//hello C++
return 0;
}
(3)substr
string substr (size_t pos = 0, size_t len = npos) const;
表示在str中从pos位置开始,截取n个字符,然后将其返回。len不指定时,默认为整型最大值。
该接口常与find和rfind接口使用。
(4)rfind
表示从字符串pos位置开始往前查找字符或字符串,返回该字符或字符串在调用的字符串对象中的位置。

比如我们要在"C:\Users\Administrator\Desktop"这样一个路径中取出Desktop,就可以用到find。
但是得注意路径中的反斜杠,需要使用双反斜杠 \\
(第一个 \
用于转义第二个 \
,使其被视为普通字符)。
cpp
void test12()
{
string s1="C:\\Users\\Administrator\\Desktop";
size_t pos=s1.rfind('\\');
string SubStr = s1.substr(pos);
cout << SubStr << endl;
}

📖小Tips:
除了上面介绍的一些常用接口,还有一些不常用的,比如:find_first_of(在字符串中搜索与其参数中指定的任何字符匹配的第一个字符)、find_last_of(查找最后一个匹配的)、find_first_not_of(查找第一个不匹配的)、find_last_not_of(查找最后一个不匹配的)。
个人认为把find_first_of改成find_any_of,find_last_of改为rfind_any_of这样好理解一点。
2.7 string类的非成员函数

返回一个新构造的字符串对象,其值是lhs中的字符与rhs中的字符的连接,即实现字符串加字符串。
此函数被重载成了全局函数,这样就能实现对象加字符串之间的顺序任意性。
(2)operator>>(string) operator<< (string)
istream& operator>> (istream& is, string& str);
重载了>>和<<才能用cin和cout对string类的输入和输出。
(3)relational operators (string)
大小比较:


从输入流的当前位置开始,持续读取字符,直到遇到换行符 \n
为止。
能读取完整的一行文本(包括空格),适合处理带空格的输入(如句子、地址等)。
📖补充知识:
cin>>读取数据的机制:
当用户从键盘输入数据时,输入的字符会先存入缓冲区,cin>>是从缓冲区读取数据的,而非键盘实时读取, 读取过程中会自动先跳过空白字符 (空格 、制表符 \t
、换行符 \n
等 ),直到遇到非空白字符才开始提取数据,之后遇到空白字符就停止读取。
概括一下流程:
跳过空白字符 → 从缓冲区提取匹配目标类型的字符 → 停止于第一个空白字符 → 残留未处理字符在缓冲区。
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
int a;
double b;
string s;
// 步骤1:读取整数
cout << "请输入一个整数: ";
cin >> a; // 从缓冲区读取整数
// 步骤2:读取浮点数
cout << "请输入一个浮点数: ";
cin >> b; // 从缓冲区读取浮点数
// 步骤3:读取字符串
cout << "请输入一个单词: ";
cin >> s; // 从缓冲区读取字符串
// 输出读取结果
cout << "你输入的整数: " << a << endl;
cout << "你输入的浮点数: " << b << endl;
cout << "你输入的单词: " << s << endl;
return 0;
}


📖小Tips:
若需读取单个数值 或不含空格的字符串 ,用 cin >>
更简洁.
若需读取包含空格的完整行 (如用户输入的句子、地址),必须用 getline。
📖例题:
cpp
#include <iostream>
using namespace std;
#include<string>
int main() {
string str;
getline(cin,str);
size_t pos=str.rfind(' ');
cout<<str.size()-(pos+1)<<endl;
return 0;
}
总结一下:
string最常用的是opereator[]、operator+=、遍历string以及迭代器。
完。
今天的分享就到这里,感谢各位大佬的关注,还请大家多多支持,你们的支持是我前进的最大动力!
