当前位置: 首页 > article >正文

String的认识和使用

string我会结合英文文档来进行剖析

网站是cplusplus.com

string是一个管理字符数组的顺序表 ,string的本质也是模板,只是库给你typedef了

首先我们来看string的构造函数

void test1()
{
	string s1;
	string s2("yeah");
	cin >> s1;
	cout << s1 << endl;
	cout << s2 << endl;
	char str[16];
}
int main()
{
	test1();
	return 0;
}

 C语言的不能做到很好的按需申请空间,而string就可以根据动态申请来很好的管理空间

那我们想把两个字符串拼接到一起怎么办

string重载了运算符+

string ret = s1 + s2;
cout <<ret << endl;
string ret2 = s1 +"好快乐";
cout << ret2 << endl;

这是类比C语言中strcat的知识,strcat只管把那个字符串拷贝到另一个字符后面,空间不够得自己开好,而且找'\0'也会耗时间,string就很好的解决了这个问题

前面我们都是浅浅的看一下,接下来我们来看string的访问

void test2()
{
	string s1("yeah");
	string s2 = "yeah";
}

上面这个是单参数的构造支持隐式类型的转换

接下来我们来看string的遍历

这里包括可读和可写两个版本的

那么我们如何知道它的大小呢

	for (size_t i = 0; i < s1.size(); i++)
	{
		//读
		cout << s1[i] << " ";
	}
	cout << endl;
	for (size_t i = 0; i < s1.size(); i++)
	{
		//写
		s1[i]++;
	}
	cout << s1 << endl;

这是第一种遍历方式

第二种遍历方式叫作迭代器

 迭代器可以想象成像指针一样的东西

每个容器的迭代器都是在它里面定义的,直接用是用不了的,得去到类域里面找

begins是开始的位置,end是最后一个数据的下一个位置

这是一个左闭右开的区间 

	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	it = s1.begin();
	while (it != s1.end())
	{
		*it = 'a';
		++it;
	}

一个是读一个是写

那我们上面的不等于可不可以改成小于呢,这里是可以的但是不建议,迭代器作为一种主流的容器遍历方式,当我在链表的时候就会发现这个小于是用不了的,因为物理空间不是连续的

这两个其实是反向迭代器的意思 

	string s1("yeah");
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;

 它的++就是倒着走的

那我们想简化一下还可以用auto来识别类型

auto rit = s1.rbegin();

那我们再用范围for来进行一个遍历,这个其实是最舒服的

for (auto ch:s1)
{
	cout << ch << " ";
}

 这个就可以自动判断结束,自动++了,这个原理其实就是迭代器,跟上面的代码差不多

 

也包含了begin和end 

但是范围for不支持倒着遍历和修改

	for (auto ch:s1)
	{
		ch++;
	}
	cout << s1 << endl;

因为本质是赋值给给它,ch的改变并不会影响s1

for (auto&ch:s1)
{
	ch++;
}
cout << s1 << endl;

这样子就可以了

void func(string s)
{
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
}
string s1("yeah");
func(s1);

这里我们这样子传值会涉及到深拷贝的问题

我们又不期望修改所以

void func(const string& s)

但是这样子编译就通过不了了

因为我迭代器是支持可读可写的,但是我这里加上了const就不能写了

这里是const对象,const对象返回的是const的begin,所以我们应该用const迭代器

void func(const string& s)
{
	string::const_iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
}

 这里也能体现的出auto的重要性

我们再回头看一下拷贝构造函数

比如说这个就是从第几个位置往后去拷贝几个字符

	string s2(s1, 2, 1);
	cout << s2 << endl;

 那假如说这个后面很长但是我们又想拷贝所有字符怎么办,我们可以看到上面有给一个缺省值npos

类型结合它的长度来看,无符号整型的-1就是整型的最大值,42亿9千万这个铁铁的是造成越界 

 它会取到这个字符串的结束

string s1("yeahxxxxxxxxxxxxxxxxxxxxxxxxxx");
string s2(s1, 2);
cout << s2 << endl;

所有我们不给就好了

那我们其实也不需要去数去算就好了

string s3(s1, 2,s1.size()-2);
cout << s3 << endl;

初始化也是有好几种方式

	string s4(10,'a');
	cout << s4 << endl;
	string s5(++s4.begin(), --s4.end());
	cout << s5 << endl;

第二个就是第一个和最后一个不要了

这两个其实是没有区别的,因为string其实出现的比stl要早,最早开始就叫length,但是后面stl出来了,为了迎合大众需求,因为大家都用size,所以string也添加了size 

这个是不会释放空间的

string s1("yeah");
s1.clear();
s1 += "mmm";
cout << s1.size() << endl;
cout << s1.capacity() << endl;

 

这个其实是不准的,因为它是写死了,假如我没有剩那么多空间它也不会跟着改变 

那么我们可以想出一个问题,为什么不把’\0‘删去呢,因为C++需要向下兼容C语言

我们再来看一下它的扩容机制,这个大概是1.5倍的扩容

void test6()
{
	string s;
	size_t old = s.capacity();
	cout << "初始化" << s.capacity() << endl;
	for (size_t i = 0; i < 100; i++)
	{
		s.push_back('x');
		if (s.capacity() != old)
		{
			cout << "扩容" << s.capacity() << endl;
			old = s.capacity();
	  }
	}
}

这主要是和编译器所运行的环境来看

这也是涉及空间,reserve这个单词涉及保留的意思,reserve一般会去申请扩容 

s.reserve(100);

 不过vs编译器下一般会开的比100大

reserve的意义在哪里呢,确定大概知道要多少空间,提前开好,减少扩容,提高效率

那reserve会不会缩呢

	s.reserve(10);
	cout<< s.capacity() << endl;

一般的编译器都是不会缩了,有些编译器可能会缩,那要是缩是怎么缩呢,比如我有一块100的空间,内容是50,那么我一般都会缩到50,这个一般都是异地缩

	s1.resize(7,'x');
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	s1.resize(12, 'x');
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	s1.resize(3);
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

 这里是resize的三种情况

 

 at就是越界了以后它会抛异常,它和operator[]的区别就是,前者会给你提醒,后者直接就进行报错

但是日常还是operator[]用的比较多

	s1[0]++;
	s1.at(0)++;
	cout << s1 << endl;

还是第一行的看的最符合我们日常需求

这个是插入字符串所需的

	string s1("hello world");
	string s;
	s.push_back('*');
	s.append("hello");
	s.append(s1);
	cout << s << endl;

 第二行就是插入一个对象的一部分,但是我们最好用的还是+=

	s += '*';
	s += "hello";
	s += s1;
	cout << s << endl;

所以string的设计是非常冗余的

有+=自然也有+,+其实是全局函数

	string ret1 = s1 + '*';
	string ret2 = s1 + "hello";
	string ret3 = s1 + s;

 但是+是一个传值返回就是要构造一个对象,至少都有两次拷贝

 assign有赋值的意思

	string s1("hello world");
	string s2;
	s2.assign(s1);
	cout << s2 << endl;

那么我已经有一些数据呢,就会把它覆盖掉


	string s1("hello world");
	string s2("xxx");
	s2.assign(s1);
	cout << s2 << endl;

string s1("hello world");
string s2("xxx");
s2.assign(s1,2,3);
cout << s2 << endl;

也可以指定某些位置多少值,这里就是第二个位置往后三个值

那如果我们想要头插的话就需要用到insert

string s1("hello world");
s1.insert(0, 1, 'x');
s1.insert(s1.begin(), 'x');
cout << s1 << endl;

 这是插入一个字符下的场景,具体我们还得看对应的接口

	s1.erase(5);
	cout << s1 << endl;

 npos就是后面有多少删多少

insert和erase能不用就不用,因为他们都涉及挪动数据,效率不高

replace有替换的意思

s1.replace(5, 1, "****");
cout << s1 << endl;

 这段代码的意思就是把第五个位置的一个字符替换成上面这串

这个其实也跟上面一样能不用就不用,因为接口设计复杂繁多,需要时查一下即可

	string s1("hello world daily room");
	string s2;
	for  (auto e:s1)
	{
		if (e !=' ')
		{
			s2 += e;
		}
		else
		{
			s2 += "%20";
		}
	}
	s1.assign(s2);
	cout << s1 << endl;
	s1 = s2;
	cout << s1 << endl;
	swap(s1, s2);
	cout << s1 << endl;

这个是库里面的swap

会经历3次深拷贝

string s1("hello world daily room");
string s2;
for  (auto e:s1)
{
	if (e !=' ')
	{
		s2 += e;
	}
	else
	{
		s2 += "%20";
	}
}
s1.assign(s2);
cout << s1 << endl;
s1 = s2;
cout << s1 << endl;
swap(s1, s2);
cout << s1 << endl;

那我们string里面的这个交换时怎么实现的呢

把它们两个指针一交换就ok了

我们验证一下发现也确实是如此

	printf("s1:%p\n", s1.c_str());
	printf("s2:%p\n", s2.c_str());
	s1.swap(s2);
	printf("s1:%p\n", s1.c_str());
	printf("s2:%p\n", s2.c_str());

但是编译器就怕你用到上面那个深拷贝的swap

所以编译器提前弄好了这个,在有现成的和模板之间它就会先选择现成的

printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());
swap(s1,s2);
printf("s1:%p\n", s1.c_str());
printf("s2:%p\n", s2.c_str());

所以改成这个也是一样的

string s("test.cpp");
size_t i = s.find('.');

 那我们怎么把后面的给取出来呢

string s1 = s.substr(i);
cout << s1 << endl;

 

string s("test.cpp.tar.zip");
size_t i = s.rfind('.');
string s1 = s.substr(i);
cout << s1 << endl;

像这样我们要找到它最后一个.的位置就需要用到rfind

 就是在子字符串中找相匹配的任意一个字符

这里我们直接借用官网来看一下

	std::string str("Please, replace the vowels in this sentence by asterisks.");
	std::size_t found = str.find_first_of("aeiou");
	while (found != std::string::npos)
	{
		str[found] = '*';
		found = str.find_first_of("aeiou", found + 1);
	}

	std::cout << str << '\n';

 这个是倒着找的

这个是找不是子字符串里面的

我们平时cin输入字符串的时候中间难免会出现空格换行等字符,所以跟scanf一样都只能识别到一部分,所以我们需要用到getline

string的大部分接口我就介绍到这里了,接下来是string的模拟实现,后面是各种容器,但是其实这些容器都有很强的相似性,所以后面会显得越来越简短,主要还是放在模拟实现上面,有什么不好的地方欢迎大家指出 


http://www.kler.cn/a/456552.html

相关文章:

  • Qt从入门到入土(七)-实现炫酷的登录注册界面(下)
  • 【每日学点鸿蒙知识】Shape描述、全局loading组件、checkbox样式、H5监听键盘收起、弹窗不关闭
  • 021-spring-springmvc
  • Navicat 连接 SQL Server 详尽指南
  • 计算机网络-L2TP Over IPSec基础实验
  • 【Rust自学】7.4. use关键字 Pt.1:use的使用与as关键字
  • 【RK3568笔记】Android适配红外遥控器
  • 虚拟机配置网络(nat)
  • Windows 安装 Jenkins 教程
  • 敏捷开发Scrum的深入理解和实践
  • 开源轮子 - EasyExcel02(深入实践)
  • .net core 的文件操作
  • HTML 标签页(Tabs)详细讲解
  • ISDP010_基于DDD架构实现收银用例主成功场景
  • 探索 Java 微服务的新趋势:现代工具与最佳实践
  • 【elementplus】中文模式
  • 【微信小程序】4plus|搜索框-历史搜索 | 我的咖啡店-综合实训
  • yarn install 安装报错:Workspaces can only be enabled in private projects.
  • 用 Unity 引擎,了解其核心概念、组件、资源、脚本、编辑器等功能,能够独立开发多平台的游戏或应用
  • 一种基于XC7V690T的在轨抗单粒子翻转系统(一)
  • IDEA2020的一些有用的功能
  • Java 溯本求源之基础(三十)——封装,继承与多态
  • STM32开发笔记123:使用STM32CubeProgrammer下载程序
  • 存储快照与拓扑调度
  • 软件工程三 需求获取与结构化分析方法(需求分析、功能建模、数据建模、行为建模、数据字典等)
  • 嵌入式AI STM32部署卷积神经网络的魔法棒