
🔥草莓熊Lotso:个人主页
❄️个人专栏:《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受。
🎬博主简介:

用char*处理字符串总踩内存、越界的坑?C++string类早帮你搞定这些 —— 自动管内存、接口丰富,开发和 OJ 题都高频用。这篇博客只讲string核心使用:从创建、遍历、修改,到容量优化、跨平台避坑,附实用代码,帮你快速上手解决需求。

--我们之前用C语言字符串时,大家应该多少都踩过下面这些坑
--那么我们学习string类,可以把这些痛点都变成过去式;
--在使用之前我们先需要对string有个基础的了解,并且不要忘记带上头文件以及using namespace std;后续的代码演示中就不一个个带这些东西了。
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;string的构造方式很多,这个在下面会有代码演示,当然这里string还重载了 = 也可以用。

最常用的4种:
// 1. 空字符串构造(默认构造)
string s1;  // s1是空串,底层已初始化,不用手动加'\0'
// 2. C字符串构造(最常用,把char*转成string)
string s2("hello Lotso");  // s2 = "hello Lotso"
// 3. 重复字符构造(创建n个相同字符的字符串)
string s3(5, 'a');  // s3 = "aaaaa"(5个'a')
// 4. 拷贝构造(用已有的string创建新对象)
string s4(s2);  // s4 = "hello C++"(和s2内容一样)代码演示:(注意看注释)
void test_string1()
{
	string s1;
	string s2("hello world");
	string s3(s2);
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	string s4(s2, 0, 5);//从s2下标为0的位置拷贝5个过去构造s4;
	cout << s4 << endl;
	//pos位置一直拷贝到结尾
	//1.写一个超过s2长度的
	string s5(s2, 6, 15);
	cout << s5 << endl;
	//2.直接不写,默认使用缺省值npos
	string s6(s2, 6);
	cout << s6 << endl;
	string s7("hello world", 6);//取前6个
	cout << s7 << endl;
	string s8(10, 'x');//用10个x
	cout << s8 << endl;
	s7 = "xxxxxx";//这样也可以
	cout << s7 << endl;
}
int main()
{
	test_string1();
}--至于析构这里了解下就可以了,不用我们自己调用

--在日常使用中,遍历是高频的操作,这里博主比较推荐大家使用前两种,更加简单直观
和数组下标访问逻辑类似,支持读和写,注意下标从0开始:

代码演示:(注意看注释)
//operate[]
void test_string2()
{
	string s1("hello world");
	cout << s1 << endl;
	s1[0]='x';//可以直接使用下标来访问修改,类似于上面那样
	cout << s1 << endl;
	cout << s1[0] << endl;
	//相比于数组这个越界有严格的检查
	//s1[12];//断言
	//s1.at(12);//抛异常,这里at的使用简单看看就行
	//size和length一样,但更推荐size
	cout << s1.size() << endl;
	cout << s1.length() << endl;
}
int main()
{
	try
	{
		test_string2();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}其实这里实现起来跟这个逻辑差不多;

不用管下标和长度,直接遍历每个字符。
void test_string5()
{
	string s1("hello world");
	cout << s1 << endl;
	//C++11
	//范围for,自动取容器数据赋值,自动迭代++,自动判断结束
	//其实底层还是迭代器,这个看反汇编可以发现
	
	//for (auto ch : s1)//其实可以直接使用&,可以修改
	for(auto& ch:s1)
	{
		ch -= 1;
	}
	for (const auto& ch : s1)
	{
		cout << ch << ' ';//只能读不能改
	}
	cout << endl;
	//支持迭代器的容器,都可以使用范围for
	//数组也支持,这里先使用一点C风格
	int a[10] = { 1,2,3 };
	for (auto e : a)
	{
		cout << e << ' ';
	}
	cout << endl;
}
int main()
{
	test_string5();
}--这里涉及到了一点auto,大家直接看看代码和注释来了解吧。
//迭代器对于函数的使用,auto
void test_string4()
{
	string s1("hello world");
	cout << s1 << endl;
	//string::iterator ret1 = find(s1.begin(), s1.end(), 'x');
	//利用auto进行优化
	auto ret1 = find(s1.begin(), s1.end(), 'x');
	if (ret1 != s1.end())
	{
		cout << "找到了x" << endl;
	}
	//list也是一样可以这样使用
	//C++11
	int i = 0;
	//通过初始化表达式值类型自动推荐对象类型
	auto j = i;
	auto k = 10;
	auto p1 = &i;
	//指定一定是指针
	auto* p2 = &i;
	cout << p1 << endl;
	cout << p2 << endl;
	//引用
	int& r1 = i;
	//r2不是int&引用,是int
	auto r2 = r1;
	//r3是int&引用
	auto& r3 = r1;
	cout << &r1 << endl;
	cout << &r2 << endl;
	cout << &i << endl;
	cout << &r3 << endl;
}
int main()
{
	test_string4();
}迭代器是STL容器的一个通用遍历方式,begin() 指向第一个字符,end() 指向最后一个字符的下一位
代码演示:(注意看注释)
void Print(const string& s)
{
	//2.const版本
	//const string::iterator it1=s.cbegin();
	//上面这样使用是不对的,const不应该用来修饰整个迭代器,这样都遍历不了了,而是修饰指向的对象
	string::const_iterator it1 = s.cbegin();//这里使用cbgin和普通的都可以
	while (it1 != s.cend())
	{
		//*it1 = 'x';不能修改
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;
	//3.reverse版本,加上const一起演示,逆序输出
	string::const_reverse_iterator it2 = s.rbegin();//这里使用rbegin
	while (it2 != s.rend())
	{
		//*it2 = 'x';//不能修改
		cout << *it2 << " ";
		++it2;
	}
	cout << endl;
}
//下标遍历,迭代器
void test_string3()
{
	string s1("hello world");
	cout << s1 << endl;
	//下标+【】
	//遍历or修改
	for (size_t i = 0; i < s1.size(); i++)
	{
		s1[i]++;
	}
	cout << s1 << endl;
	//迭代器
	//行为像指针一样的东西
	//1.常规使用
	string::iterator it1 = s1.begin();
	while (it1 != s1.end())
	{
		//(*it1)-;//修改
		cout << *it1 << " ";
		++it1;
	}
	//相对于下标+[]来说,迭代器更加通用,我们这里再来看看在链表中的使用
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	list<int>::iterator lit = lt.begin();
	while (lit != lt.end())
	{
		cout << *lit << " ";
		++lit;
	}
	cout << endl;
	//迭代器的其它使用形式
	Print(s1);
}
int main()
{
	test_string3();
}尾部追加是我们比较常见的修改场景,3中方式各有优势,大家可以按需选择:
--这里就不放参考文档了,大家可以自己去查阅一下,另外下面的代码中还涉及到了一些其它的接口使用,也可以看看。
代码演示:(注意看注释)
//push_back,append,+=,+;
//appear,=
void test_string7()
{
	string s1("hello world");
	s1.push_back('&');//尾插一个字符
	s1.append("hello bit");//尾插一个字符串
	cout << s1 << endl;
	s1.append(10, 'x');//尾插10个x
	cout << s1 << endl;
	//还可以配着迭代器使用
	string s3;
	string s2(" apple hello!");
	//我不想要空格和!
	s3.append(++s2.begin(), --s2.end());
	cout << s3 << endl;
	//其实我们直接使用+=更加方便
	string s4("hello world");
	s4 += ' ';
	s4 += "hello bit";
	cout << s4 << endl;
	//为什么不把 + 重载为成员的而是全局,因为这样可以不用一定把成员变量写在左边
	cout << s4 + "xxxx" << endl;
	cout << "xxxx" + s4 << endl;
	//assign,没有直接赋值好用
	s4 = "xxx";
	cout << s4 << endl;
	s4.assign("yyy");
	cout << s4 << endl;
}
int main()
{
	test_string7();
}代码演示:(注意看注释)
void test_string8()
{
	string s1("hello world");
	//上面都是尾插,这里实现一个头插
	s1.insert(0, "xxxxx");
	cout << s1 << endl;
	//但是头插一个必须这样写
	s1.insert(0, 1, '*');
	cout << s1 << endl;
	//第5个位置插入一个*
	s1.insert(5, 1, '*');
	cout << s1 << endl;
	//迭代器
	s1.insert(s1.begin(), '&');
	cout << s1 << endl<<endl;
	string s2("hello world");
	s2.erase(0, 1);//头删
	cout << s2 << endl;
	s2.erase(s2.begin());//头删
	cout << s2 << endl;
	s2.erase(5, 2);//指定位置开始删除2个
	cout << s2 << endl<<endl;
	//没给的话就全删掉
	s2.erase(5);//这里应该也是默认npos
	cout << s2 << endl;
}
int main()
{
	test_string8();
}代码演示:(注意看注释)
string s = "hello";
s.clear();  // s变成空串,但底层容量不变
cout << s.size();  // 输出0(有效字符数为0)replace() 能直接替换字符串中指定位置,指定长度的内容。可以替换为字符,字符串或者子串,有部分自己的使用场景。
代码演示:(注意看注释)
void test_string8()
{
	
	string s3("hello world");
	s3.replace(5, 1, "&&&");//把5这个位置的1个替换成&&&
	cout << s3 << endl;
	s3.replace(5, 3, "*");//从5开始的三个替换成*
	cout << s3 << endl;
	//我们再来看看怎么把所有空格都替换成%%
	string s4("hello            world hello wugongda");
	cout << s4 << endl;
	size_t pos = s4.find(' ');
	while (pos != string::npos)
	{
		s4.replace(pos, 1, "%%");
		//找到下一个空格
		pos = s4.find(' ', pos + 2);
	}
	cout << s4 << endl;
	//这样的话效率不是很高,我们换个思路优化一下
	string s5("hello            world hello wugongda");
	cout << s5 << endl;
	string s6;
	s6.reserve(s5.size());
	for (auto ch : s5)
	{
		if (ch != ' ')
		{
			s6 += ch;
		}
		else
		{
			s6 += "%%";
		}
	}
	cout << s6 << endl;
	//s5 = s6;
	
}
int main()
{
	test_string8();
}我们在使用string时,如果不注意容量,可能会导致频繁扩容,拖慢程序的效率。我们先了解一下下面会涉及的三个接口,再来学学优化技巧。
很多人会混淆这三个接口的作用,我们先通过表格对比看看吧。
| 接口 | 功能 | 日常用哪个? | 
|---|---|---|
| string::size - C++ Reference | 返回有效字符个数(比如 "hello" 返回 5) | 优先用,和其他 STL 容器接口一致 | 
| string::length - C++ Reference | 和size()功能完全一样(历史遗留接口) | 少用,不如size()通用 | 
| string::capacity - C++ Reference | 返回底层已分配的空间大小(能存多少字符,不含 '\0') | 优化时用 | 
--我们先来看看一个整体的代码,里面还涉及到了resize,shrink_to_fit等接口的使用。
代码演示:(注意看注释)
void TestCapacity()
{
	string s1;
	//s1.reserve(200);//确定要插入多少时,可以提前扩容
	size_t old = s1.capacity();
	cout << s1.capacity() << endl;
	for (size_t i = 0; i < 200; i++)
	{
		s1.push_back('x');//尾插
		if (s1.capacity() != old)
		{
			cout << s1.capacity() << endl;
			old = s1.capacity();
		}
	}
	cout << endl << endl;
}
void test_string6()
{
	string s1("hello world");
	cout << s1.max_size() << endl;//了解下即可
	cout << s1.size() << endl;//不包含结尾的\0
	cout << s1.capacity() << endl;//存储实际有效字符的个数,不包含结尾的\0
	s1.clear();//空间不会清理
	cout << s1.size() << endl;//不包含结尾的\0
	cout << s1.capacity() << endl<<endl;//存储实际有效字符的个数,不包含结尾的\0
	//测试空间增容
	TestCapacity();
	//reserve最好只用来增容
	string s2("hello world");
	cout << s2.size() << endl;
	cout << s2.capacity() << endl;
	s2.reserve(20);//会开的比20大
	cout << s2.size() << endl;
	cout << s2.capacity() << endl;
	//s2.reserve(5);//vs上不会缩容
	s2.shrink_to_fit();//这个可以实现缩容,但是一般不会用,代价比较大
	cout << s2.size() << endl;
	cout << s2.capacity() << endl;
	string s3(s2);
	cout << s3 << endl;
	// < 当前对象的size时,相当于保留前n个,删除后面的数据
	s3.resize(5);
	cout << s3 << endl;
	// > 当前对象的size时,插入数据
	s3.resize(10, 'x');
	cout << s3 << endl;
	s3.resize(30, 'y');
	cout << s3 << endl;
}
int main()
{
	test_string6();
}如果我们知道字符串最后大概有多长(比如上面的拼接200个字符),就可以先用 reserve() 预分配空间,避免扩容次数过多。
代码演示:(注意看注释)
string s;
// 已知要存1000个字符,提前预分配
s.reserve(1000);  
// 后续拼接1000个字符,不会频繁扩容
for (int i = 0; i < 1000; ++i) {
    s += 'a';
}resize(n) 用来修改有效字符的个数,分以下两种情况:
代码演示:(注意看注释)
string s = "hello";
// 1.n比size()大:补'x'到10个字符
s.resize(10, 'x');  
// 2.n比size()小:截断到3个字符
s.resize(3);       前面我们讲过 clear() 只会清理有效字符,但是底层空间不变。那我们如果想要进行缩容,就可以使用 shrink_to_fit() (但是这个只是一个建议,而且我们使用的也少,代价太大了)
前面的总体示例中有,这里就给大家看看图片吧!

不同编译器的string底层实现不一样,大家可以一起来看看差异。下述结构都是在32位平台下进行验证的,32位平台指针占4个字节
VS下string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义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;这种设计也是具有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量。
最后:还有一个指针做一些其它事情。
所以总共占28个字节。

g++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
struct _Rep_base
{
	size_type _M_length;
	size_type _M_capacity;
	_Atomic_word _M_refcount;
}--除了前面讲的一些以外,其实我们的string还有很多比较实用的接口,这里就再给大家分享一部分,如果有没分享到但是大家需要使用的话可以自己查阅参考文档去了解一下用法。
find() 从左往右找字符或者子串,返回第一次出现的下标;没有找到就返回 string :: npos(这个戴代表一个很大的数,代表“不存在”)
代码演示:(注意看注释)
nt main()
{
    string s = "hello Lotso";
    // 1. 找字符'w'
    size_t pos1 = s.find('L');
    if (pos1 != string::npos) {
        cout << "'L'在位置:" << pos1 << endl;// 输出6
    }
    // 2. 找子串"world"
    size_t pos2 = s.find("Lotso");
    if (pos2 != string::npos) {
        cout << "world在位置:" << pos2 << endl;// 输出6
    }
    // 3. 从下标3开始找字符'l'
    size_t pos3 = s.find('l', 3);  // 从第3位(0开始)往后找
    cout << "'l'在位置:" << pos3 << endl;// 输出3(s[3]是'l')
}在平常的使用中,如果使用 cin>>string 读取字符串时,遇到空格就会停止。而 getline() 能读取一整行的内容,包括空格。默认回车结束,也可以自己指定
比如我们下面这个题就必须使用getline,直接用cin是不行的

代码演示:(注意看注释)
#include <iostream>
#include <string>
using namespace std;
int main() {
	string str;
	// cin >> str;//这个不行
	getline(cin, str);
	//getline(cin, str, '#');//指定碰到#结束
	size_t pos = str.rfind(' ');
	if (pos != str.size())
	{
		cout << str.size() - (pos + 1) << endl;
	}
	else
	{
		cout << str.size() << endl;
	}
}代码演示:(注意看注释)
int main()
{
	string s = "hello world";
	// 1. 从位置6开始,取5个字符
	string sub1 = s.substr(6, 5);  // sub1 = "world"
	// 2. 从位置0开始,取5个字符
	string sub2 = s.substr(0, 5);  // sub2 = "hello"
	// 3. 从位置6开始,取到末尾
	string sub3 = s.substr(6);     // sub3 = "world"
}我们在一些特殊的场景下需要使用C语言的char*(比如printf输出),用c_str ()把string转换成const char*;
代码演示:(注意看注释)
#include <cstring>
int main()
{
	string s = "hello";
	// 1. printf输出(printf不直接支持string)
	printf("s = %s\n", s.c_str());  // 输出:s = hello
	// 2. 调用C库函数strlen(需要包含<cstring>)
	size_t len = strlen(s.c_str());  // len = 5
}判断字符串是否为空,优先用empty(),比size()==0更高效(empty()直接返回标志位,size()==0可能要计算):
代码演示:(注意看注释)
int main()
{
	string s;
	if (s.empty()) {  // 推荐
		cout << "s是空串";
	}
	// 不推荐:if (s.size() == 0)
}补充示例:(涉及到几个接口的使用,大家可以自己试试)
void SplitFilename(const std::string& str)
{
	std::cout << "Splitting: " << str << '\n';
	std::size_t found = str.find_last_of("/\\");
	std::cout << " path: " << str.substr(0, found) << '\n';
	std::cout << " file: " << str.substr(found + 1) << '\n';
}
void test_string7()
{
	string s("test.cpp.zip");
	size_t pos = s.rfind('.');
	string suffix = s.substr(pos);
	cout << suffix << endl;
	std::string str("Please, replace the vowels in this sentence by asterisks.");
	std::cout << str << '\n';
	std::size_t found = str.find_first_not_of("abcdef");
	while (found != std::string::npos)
	{
		str[found] = '*';
		found = str.find_first_not_of("abcdef", found + 1);
	}
	std::cout << str << '\n';
	std::string str1("/usr/bin/man");
	std::string str2("D:\\1-草莓熊Lotso\\1-课件\\4.C++课件\\C++进阶课件");
	SplitFilename(str1);
	SplitFilename(str2);
string url2("https://legacy.cplusplus.com/reference/string/string/substr/");
string url1("http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=65081411_1_oem_dg&wd=%E5%90%8E%E7%BC%80%20%E8%8B%B1%E6%96%87&fenlei=256&rsv_pq=0xc17a6c03003ede72&rsv_t=7f6eqaxivkivsW9Zwc41K2mIRleeNXjmiMjOgoAC0UgwLzPyVm%2FtSOeppDv%2F&rqlang=en&rsv_dl=ib&rsv_enter=1&rsv_sug3=4&rsv_sug1=3&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=1588&rsv_sug4=6786");
	string protocol, domain, uri;
	size_t i1 = url1.find(':');
	if (i1 != string::npos)
	{
		protocol = url1.substr(0, i1 - 0);
		cout << protocol << endl;
	}
	// strchar
	size_t i2 = url1.find('/', i1+3);
	if (i2 != string::npos)
	{
		domain = url1.substr(i1+3, i2-(i1+3));
		cout << domain << endl;
		uri = url1.substr(i2 + 1);
		cout << uri << endl;
	}
}
void test_string4()
	{
		string s("test.cpp.zip");
		size_t pos = s.find('.');
		string suffix = s.substr(pos);
		cout << suffix.c_str() << endl;
		string copy(s);
		cout << copy.c_str() << endl;
		s = suffix;
		cout << suffix.c_str() << endl;
		cout << s.c_str() << endl;
		s = s;
		cout << s.c_str() << endl;
	}
	void test_string5()
	{
		string s1("hello world");
		string s2("hello world");
		cout << (s1 < s2) << endl;
		cout << (s1 == s2) << endl;
		cout << ("hello world" < s2) << endl;
		cout << (s1 == "hello world") << endl;
		//cout << ("hello world" == "hello world") << endl;
		cout << s1 << s2 << endl;
		string s0;
		cin >> s0;
		cout << s0 << endl;
	}
	void test_string6()
	{
		string s1("hello world");
		string s2 = s1;
		cout << s1 << endl;
		cout << s2 << endl;
		string s3("xxxxxxxxxxxxxx");
		s1 = s3;
		cout << s1 << endl;
		cout << s3 << endl;
	}往期回顾:
《从崩溃到精通:C++ 内存管理避坑指南,详解自定义类型 new/delete 调用构造 / 析构的关键逻辑》
别再用函数重载堆代码了!C++ 模板初阶教程:原理 + 实例 + 避坑,新手也能秒懂
C++ 开发者必看!STL 库 + 字符编码一篇通,告别乱码与重复造轮子
结语:掌握string的创建、遍历、容量管理和跨平台注意事项,就够应对大部分日常开发和刷题场景。它的核心是 “省心高效”,不用纠结底层,专注逻辑即可。你常用string哪个接口?评论区聊聊~zhi
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど