【C++】string类的常见接口的使用

欢迎拜访Madison-No7个人主页
文章主题:string类的常见接口的使用
隶属专栏我的C++成长日志
写作日期:2025年9月27号

目录

一、初识string

二、string类的常见接口的使用

[2.1 string的构造函数](#2.1 string的构造函数)

[2.2 string 的析构函数](#2.2 string 的析构函数)

[2.3 string的常见容量接口](#2.3 string的常见容量接口)

[2.4 遍历string中的元素](#2.4 遍历string中的元素)

(1)operator[]

[(2)Iterator 迭代器](#(2)Iterator 迭代器)

(3)范围for(C++11支持):

补充知识:auto关键字

[2.5 与对象修改有关的操作](#2.5 与对象修改有关的操作)

[2.6 与查找有关的接口](#2.6 与查找有关的接口)

[2.7 string类的非成员函数](#2.7 string类的非成员函数)


一、初识string

  1. string是表示字符串的字符串类,可以理解为字符顺序表
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
  4. 不能操作多字节或者变长字符的序列。

注意:使用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) 是一种用于遍历容器(如**vectorlistmap** 等)中元素的对象,它提供了统一的访问接口,使得开发者可以不依赖容器的具体实现来操作元素。

迭代器的作用类似于指针,但比指针更通用 ------ 它可以适配不同的数据结构(数组、链表、树等),让遍历操作变得一致。

迭代器体现了封装的思想,因为它屏蔽了底层的实现细节,提供了统一的类似访问容器的方式,不需要关心容器底层结构和实现的细节。

***小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的特点:自动复制,自动迭代,自动判断结束

  1. 自动迭代:无需手动获取迭代器或控制索引,自动从容器第一个元素遍历到最后一个元素。
  2. **自动复制:**自动将遍历到的字符拷贝给迭代变量(上面是ch)。
  3. 自动判断结束:当遍历字符串的\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声明指针类型时,用autoauto*没有任何区别,但用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 语言库函数(如 strlenstrcmpprintf 等)或遵循 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类的非成员函数

(1)operator+(string)

返回一个新构造的字符串对象,其值是lhs中的字符与rhs中的字符的连接,即实现字符串加字符串。

此函数被重载成了全局函数,这样就能实现对象加字符串之间的顺序任意性。

(2)operator>>(string) operator<< (string)

istream& operator>> (istream& is, string& str);

重载了>>和<<才能用cin和cout对string类的输入和输出。

(3)relational operators (string)

大小比较:

(4)getline (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以及迭代器。


完。

今天的分享就到这里,感谢各位大佬的关注,还请大家多多支持,你们的支持是我前进的最大动力!

相关推荐
政沅同学2 小时前
C#系统日志
开发语言·c#
一只雄牧慕2 小时前
【C++】哈希表
开发语言·数据结构·c++·散列表
cici158743 小时前
在Ubuntu18.04安装兼容JDK 8的Eclipse集成开发环境
java·开发语言·eclipse
不枯石3 小时前
Matlab通过GUI实现点云的统计滤波(附最简版)
开发语言·图像处理·算法·计算机视觉·matlab
代码村新手3 小时前
C语言-操作符
开发语言·c++
老赵的博客3 小时前
c++ 之多态虚函数表
java·jvm·c++
liu****3 小时前
负载均衡式的在线OJ项目编写(四)
运维·c++·负载均衡·个人开发
天天进步20153 小时前
Python项目--交互式VR教育应用开发
开发语言·python·vr
@卞3 小时前
第十六届蓝桥杯软件赛C组省赛C++题解(京津冀)
c语言·c++·蓝桥杯