【C++初阶】一篇手撕string类

前言

在C语言中,字符串是以\0结尾的一些字符的集合,为了方便操作,C标准库中提供了一些str系列的库函数。但是这些库函数与字符串是分离开的,不太符合面向对象(OOP)的思想,而且底层空间需要用户自己去管理,稍不留神可能就会出现越界访问。为了解决上面的这些问题,C++中引入了string类,它给我们带来了极大的便利
在使用string类时,需要包含头文件#include,以及使用using namespace std展开命名空间

  • string是表示字符串的字符串类。
  • 该类的接口与常规容器(vector、list等)的接口基本相同,再添加了一些专门用来操作string的常规操作。
  • string在底层实际是:用basic_string模板类实例化出来的一个类,typedef basic_string string;。
  • 不能操作多字节或者变长字符的序列

一,常见的构造接口

📖string()
该类的默认构造函数,用于构造空的string类对象,即空字符串。

int main()
{
	string s1;
	cout << s1 << endl;
	return 0;
}

在这里插入图片描述
小Tips:string类对象支持流插入和流提取,下文将进行介绍,这里大家直接使用即可。

📖string(const char s)*
用C-string来构造string类对象,即用一个C的字符串(或字符数组)来构造一个string类的对象。

int main()
{
	string s1("Hello C++!");
	cout << s1 << endl;
	return 0;
}

在这里插入图片描述
📖string(size_t n, char c)
用n个字符c来构建一个string类对象。

int main()
{
	string s1(5, 'x');
	cout << s1 << endl;
	return 0;
}

在这里插入图片描述
📖string(const string& s)
string类的拷贝构造,用于构建一个和已存在的string类对象s一模一样的对象。

int main()
{
	string s1(5, 'x');
	string s2(s1);
	cout << s1 << endl;
	cout << s2 << endl;
	return 0;
}

在这里插入图片描述
📖string (const string& str, size_t pos, size_t len = npos)
复制str中从字符位置pos开始并跨越len字符的部分(如果 str 太短或 len 是字符串npos,则直到 str 的末尾)。简单来说就是使用一个已存在的string类对象的一部分来创建一个新的string类对象。

小Tips:nops是string类里面的一个静态成员变量,它是size_t类型,初始化为-1,即表示整型的最大值。此值如果在string的成员函数中作为形参len的缺省值,表示到字符串结束。如果作为string类中成员函数的返回值,一般表示没有匹配项。

int main()
{
	string s1("Hello C++!");
	string s2(s1, 0, 5);//用s1的部分来初始化创建s2
	cout << s1 << endl;
	cout << s2 << endl;
	return 0;
}

在这里插入图片描述
注意:对一个string类对象来说,它的第一个有效字符的下标是0。

📖string (const char s, size_t n)*
用s所指向字符串(或字符数组)的前n个来初始化创建一个string类对象。

int main()
{
	char str[] = "Hello C++!";
	string s1(str, 5);//用字符数组str的前5个字符来构建一个string类对象
	cout << str << endl;
	cout << s1 << endl;
	return 0;
}

在这里插入图片描述

二,与容量有关的接口

📖size()
返回字符串的有效字符长度。

int main()
{
	string s1("Hello C++!");
	string s2("Good morning!");
	cout << "s1的size:" << s1.size() << endl;
	cout << "s2的size:" << s2.size() << endl;
	return 0;
}

在这里插入图片描述
📖length()
返回字符串的有效字符长度。

int main()
{
	string s1("Hello C++!");
	string s2("Good morning!");
	cout << "s1的length:" << s1.length() << endl;
	cout << "s2的length:" << s2.length() << endl;
	return 0;
}

在这里插入图片描述
小Tips:从上面的打印结果可以看出,size()和length()接口的功能一模一样,甚至底层实现原理也完全相同,都是返回字符串的有效字符个数,最初只有length()接口,引入size()接口的原因是为了与其他容器的接口保持以致,一般情况下基本都使用size()接口。这也从侧面说明string诞生的时间比STL要早。
📖capacity()
返回一个string对象中空间的大小。

int main()
{
	string s1("Hello C++!");
	string s2("Good morning chunren!");
	cout << "s1的capacity:" << s1.capacity() << endl;
	cout << "s2的capacity:" << s2.capacity() << endl;
	return 0;
}

在这里插入图片描述
小Tips:同一个string对象,在不同平台下的capacity()(空间容量)可能不同,因为string在底层就是一个存储字符的动态顺序表,空间不够了要进行扩容,而不同平台底层的扩容机制可能有所不同,这就导致了最终capacity()的结果不同。将上面的代码放到Linux环境下使用g++编译器再来试试:
在这里插入图片描述
小Tips:capacity()返回一个string对象中空间的大小,这个空间指的是可以存储有效字符的空间,底层实际上的空间会多一个,因为还要存储\0。
📖VS下的扩容机制

int main()
{
	string s1("Hello!");
	size_t old = s1.capacity();
	cout << s1.capacity() << endl;
	for (size_t i = 0; i < 100; i++)
	{
		s1 += 'v';
		if (old != s1.capacity())
		{
			cout << "扩容:" << s1.capacity() << endl;
			old = s1.capacity();
		}
	}
	return 0;
}

在这里插入图片描述
VS下一上来会有15个空间用来存储数据(本质上是开16个空间,因为还要存\0),第一次扩容是2倍,后面都是以1.5倍的大小去扩容。
在这里插入图片描述
📖Linux下的扩容机制

int main()
{
	string s1("Hello!");
	size_t old = s1.capacity();
	cout << s1.capacity() << endl;
	for (size_t i = 0; i < 100; i++)
	{
		s1 += 'v';
		if (old != s1.capacity())
		{
			cout << "扩容:" << s1.capacity() << endl;
			old = s1.capacity();
		}
	}
	return 0;
}

将上面的代码放到Linux环境下使用g++编译器再来试试:
在这里插入图片描述
可见在Linux下,最初对象需要多少空间就开多少,后面一次按照2倍的大小进行扩容。

📖empty()
检测字符串是否为空串,是返回true,否则返回false。

int main()
{
	string s1;//
	if (s1.empty())
	{
		cout << "s1是一个空串" << endl;
	}
	return 0;
}

在这里插入图片描述
📖clear()
清空有效字符。

int main()
{
	string s1("Hello C++!");
	cout << "清空之前的size:" << s1.size() << endl;
	cout << "清空之前的capacity:" << s1.capacity() << endl;
	s1.clear();//清空
	cout << "清空之后的size:" << s1.size() << endl;
	cout << "清空之后的capacity:" << s1.capacity() << endl;
	return 0;
}

在这里插入图片描述
小Tips:从打印结果可以看出,clear()清空操作不会影响capacity()容量,也就是说string对象并不会主动的去缩容。
📖reserve (size_t n = 0)
为字符串预留空间。直接一次申请n个空间,可以用来存储n个有效字符,避免了每次都要去扩容。大部分的扩容都是异地扩容,扩容次数过多会影响效率。

int main()
{
	string s1;
	s1.reserve(100);//知道要尾插100个字符就先直接申请100个,避免后面再去扩容
	size_t old = s1.capacity();
	cout << s1.capacity() << endl;
	for (size_t i = 0; i < 100; i++)
	{
		s1 += 'v';
		if (old != s1.capacity())
		{
			cout << "扩容:" << s1.capacity() << endl;
			old = s1.capacity();
		}
	}
	s1.reserve(20);
	cout << "第二次执行reserve(20):" << s1.capacity() << endl;
	return 0;
}

在这里插入图片描述
小Tips:实际申请到的有效空间可能比我们需要的多,但是一定不可能比我们需要的少,如s1.reserve(100)去申请100个有效空间,但实际上申请了111个有效空间。当n小于当前对象的容量时,一般的编译器都不会执行缩容。(视具体情况而定)
在这里插入图片描述
将字符串大小调整为n个长度的大小,当n小于当前字符串的长度size(),会保留前n个字符,将第n个字符后面的所以字符删除;当n大于当前字符串的长度size(),先会进行扩容,以满足存储n个有效字符的需求,如果指定了字符c,会将新元素初始化为c的副本,否则全部初始化为空字符,即\0。

int main()
{
	string s1("Hello C++!");
	cout << "最初的s1.size():" << s1.size() << endl;
	cout << "最初的s1.capacity():" << s1.capacity() << endl;
	string s2 = s1;//将s1拷贝一份

	s1.reserve(100);
	cout << "reserve(100)后的s1.size():" << s1.size() << endl;
	cout << "reserve(100)后的s1.capacity():" << s1.capacity() << endl;

	s2.resize(100);
	cout << "resize(100)后的s1.size():" << s2.size() << endl;
	cout << "resize(100)后的s1.capacity():" << s2.capacity() << endl;
	return 0;
}

在这里插入图片描述
小Tips:reserve和resize的区别可总结为:前者只会影响容量,负责申请空间,不会改变字符串的有效字符个数,即会改变capacity,不会改变size;后者在申请空间的基础上还会对空间进行初始化,这样就会对有效字符的个数产生影响,所以它即会改变capacity也会改变size
📖shrink_to_fit()
将capacity容量缩至合适,一般不会缩小到和size一样大,可能会比size大一点。

int main()
{
	string s2("Hello C++!");
	cout << "最初的s2.size():" << s2.size() << endl;
	cout << "最初的s2.capacity():" << s2.capacity() << endl;

	s2.reserve(100);//扩容
	cout << "reserve(100)后的s2.size():" << s2.size() << endl;
	cout << "reserve(100)后的s2.capacity():" << s2.capacity() << endl;

	s2.shrink_to_fit();//缩容
	cout << "缩容后的s2.size():" << s2.size() << endl;
	cout << "缩容后的s2.capacity():" << s2.capacity() << endl;
	return 0;
}

在这里插入图片描述

三,与对象访问及遍历有关的操作

📖operator[ ]
在这里插入图片描述

int main()
{
	string s1("Hello C++!");//普通对象
	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i];
	}
	cout << endl << "修改后:";
	for (size_t i = 0; i < s1.size(); i++)
	{

		cout << ++s1[i];//因为返回值是引用所以可以用[]对其进行修改
	}
	cout << endl;
	const string s2("Hello World!");
	for (size_t i = 0; i < s2.size(); i++)
	{
		cout << s2[i];
	}
	cout << endl;
	return 0;
}

在这里插入图片描述
小Tips:因为operator[]的返回值是一个引用,所以可以通过[]加下标的方式去访问和修改string类对象,和内置类型的数组不同,这里的s1[i]本质上是去调用函数,而内置类型的数组使用[]本质是解引用。如果发生越界访问,程序会直接报错,at接口和operator[]接口的功能类似,只不过at接口在发生越界访问的时候会抛出异常。

📖迭代器
迭代器是一种抽象的设计概念,它提供一种方法,使之能够依序巡访某个容器所含的各种元素,而又无需暴露该容器的内部表述方式。迭代器是一种行为类似指针的对象,它是容器与算法的桥梁,算法需要去访问容器中的数据,但是容器的数据都是私有的,并且有多种容器,针对不同的容器,某一算法的具体实现可能不同,例如对链表逆置和对顺序表逆置,具体过程当然是不同的,但是迭代器的出现却将它们统一了起来,对于一个算法,无论是什么容器,只需要将它的迭代器区间传过来即可,算法只使用统一的逻辑。因此任何容器的迭代器类型都用iterator来表示,它是一个类的内置类型,通过typedef得到,关于迭代器更具体地内容我将在后续模拟实现的文章中为给大家分享,今天我们只需要知道如何使用即可。
📖begin、end
在这里插入图片描述

int main()
{
	string s1("Hello C++!");//普通对象
	string::iterator it = s1.begin();
	while (it < s1.end())
	{
		cout << *it;//解引用迭代器
		it++;//++迭代器,让迭代器向后走
	}
	cout << endl;
	it = s1.begin();
	while (it < s1.end())
	{
		++(*it);//通过迭代器去修改
		it++;
	}
	it = s1.begin();
	while (it < s1.end())
	{
		cout << *it;
		it++;
	}
	cout << endl;
	return 0;
}

在这里插入图片描述
从上面的代码可以看出,一个迭代器对象和一个指针类型的变量十分相似,都可以通过*解引用,并且都可以++,还可以解引用后去修改。但本质上对迭代器的这些操作都是通过运算符重载来实现的,具体实现我将在后续文章中为大家介绍。

小Tips:迭代器区间永远都是左闭右开,迭代器类型作为类的内置类型可以直接通过类名::iterator访问,例如:string::iterator就表示string类里面的迭代器类型。普通迭代器可读可写,const迭代器限制的是其指向的内容,只能读不能写,而const迭代器本身可以修改。
在这里插入图片描述

📖rbegin、rend
在这里插入图片描述
在这里插入图片描述

int main()
{
	string s1("Hello C++!");//普通对象
	string::reverse_iterator it = s1.rbegin();
	while (it < s1.rend())
	{
		cout << *it;//解引用迭代器
		it++;//++迭代器,让迭代器向后走
	}
	cout << endl;
	return 0;
}

在这里插入图片描述
小Tips:reverse_iterator一般被叫做反向迭代器,因为它可以倒着去遍历。
📖范围for

int main()
{
	string s1("Hello C++!");//普通对象
	for (auto it : s1)
	{
		cout << it;
	}
	cout << endl;
	for (auto& it : s1)//用引用就可以进行修改
	{
		it--;
	}
	for (auto it : s1)
	{
		cout << it;
	}
	cout << endl;
	return 0;
}

在这里插入图片描述
小Tips:范围for就是基于迭代器实现的,在底层范围for会转化成正向迭代器。换言之,一个容器如果不支持迭代器,那它必定也不支持范围for。
📖使用at访问对象中的元素
因为at函数也是使用的引用返回,所以我们也可以通过at函数修改对应位置的元素。

int main()
{
	string s("CSDN");
	for (size_t i = 0; i < s.size(); i++)
	{
		//at(pos)访问pos位置的元素
		cout << s.at(i);
	}
	cout << endl;

	for (size_t i = 0; i < s.size(); i++)
	{
		//at(pos)访问pos位置的元素,并对其进行修改
		s.at(i) = 'x';
	}
	cout << s << endl; //xxxx
	return 0;
}

四,与对象修改有关的操作

📖operator=
赋值运算符重载是string类的一个默认成员函数,该函数有三个重载形式,如下图所示:
在这里插入图片描述

int main()
{
	string s1("Hello C++!");
	string s2("你好,C++!");
	cout << s2 << endl;//原始的s2
	s2 = s1;//string类对象
	cout << s2 << endl;//第一次赋值后的s2
	s2 = "green";
	cout << s2 << endl;//第二次赋值后的s2
	s2 = 'a';
	cout << s2 << endl;//第三次赋值后的s2
	return 0;
}

在这里插入图片描述
📖void push_back (char c)
将一个字符c追加到string类对象的末尾,它的长度增加1。

int main()
{
	string s1("Hello C++!");
	cout << "追加前:" << s1 << endl;
	s1.push_back('s');
	cout << "追加后:" << s1 << endl;
	return 0;
}

在这里插入图片描述
📖append
append是在源字符串的后面进行追加操作的成员函数,它有七种重载实现形式,如下图所示:
在这里插入图片描述

int main()
{
	string s1("Hello C++!");
	string s2("aaaa");
	cout << "追加前:" << s2 << endl;
	s2.append(s1);
	cout << "追加一个string对象:" << s2 << endl;
	s2 = "aaaa";
	s2.append(s1, 6, 3);
	cout << "追加一个string对象的一部分:" << s2 << endl;
	s2 = "aaaa";
	s2.append("你好");
	cout << "追加一个C类型的字符串:" << s2 << endl;
	s2 = "aaaa";
	s2.append("Hello!", 2);
	cout << "追加一个C类型字符串的前两个字符:" << s2 << endl;
	s2 = "aaaa";
	s2.append(5, 'b');
	cout << "追加五个字符b:" << s2 << endl;
	s2 = "aaaa";
	s2.append(s1.begin()+2, s1.begin()+4);
	cout << "追加一个迭代器区间:" << s2 << endl;
	return 0;
}

在这里插入图片描述
📖operator+=
通过重载运算符+=实现追加,该运算符重载有三种重载实现形式,如下图所示:
在这里插入图片描述

int main()
{
	string s1("Hello C++!");
	string s2("aaaa");
	cout << "追加前:" << s2 << endl;
	s2 += s1;
	cout << "追加一个string类对象:" << s2 << endl;
	s2 = "aaaa";
	s2 += "bcde";
	cout << "追加一个C类型的字符串:" << s2 << endl;
	s2 = "aaaa";
	s2 += 'o';
	cout << "追加一个字符:" << s2 << endl;
	return 0;
}

在这里插入图片描述
小Tips:除了上面介绍的一些常用的字符串修改接口外,还有一些不太常用的,例如:assign(内容替换)、insert(指定位置插入)、erase(删除)、replace(部分替换)、swap(交换两个字符串)。它们的使用方法都大同小异。

五,与查找有关的接口

📖c_str()
该接口的返回值类型是const char*,即返回一个C格式的字符串,该接口起到桥梁作用。

int main()
{
	string s2("Hello C++!");
	const char* str = s2.c_str();
	cout << str << endl;
	return 0;
}

在这里插入图片描述
📖find
从字符串的pos位置开始往后查找字符或字符串,返回其在当前字符串中的位置。
在这里插入图片描述

int main()
{
	string s1("https://www.csdn.net/?spm=1011.2124.3001.4476");
	size_t pos1 = s1.find("csdn");
	cout << pos1 << endl;
	size_t pos2 = s1.find("www.csdn.net", 7, 3);
	cout << pos2 << endl;
	return 0;
}

在这里插入图片描述
小Tips:一般在没有找到的情况下会返回npos,即整型最大值。
📖rfind
在字符串pos位置开始往前查找字符或字符串,返回其在当前字符串中的位置。
在这里插入图片描述

int main()
{
    std::string str("The sixth sick sheik's sixth sheep's sick.");
    std::string key("sixth");

    std::size_t found = str.rfind(key);
    if (found != std::string::npos)
        str.replace(found, key.length(), "seventh");

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

    return 0;
}

在这里插入图片描述
📖substr (size_t pos = 0, size_t len = npos)
在源字符串中,从pos位置开始,截取n个字符,以string的形式返回。

int main()
{
	string s1("https://www.csdn.net/?spm=1011.2124.3001.4476");
	size_t pos1 = s1.find("csdn");
	string s2 = s1.substr(pos1, 8);
	cout << s2 << endl;
	return 0;
}

在这里插入图片描述
小Tips:除了上面介绍的一些常用接口,还有一些不常用的,比如:find_first_of(在字符串中搜索与其参数中指定的任何字符匹配的第一个字符)、find_last_of(查找最后一个匹配的)、find_first_not_of(查找第一个不匹配的)、find_last_not_of(查找最后一个不匹配的)。

六,string类的非成员函数

有些运算符重载函数存在竞争左操作数的问题,所以它们写在string类的外面,是类的非成员函数,主要有下面几种:
在这里插入图片描述
小Tips:operator>>和getline的区别在于,前者遇到空格’ '和换行\n会截止,而后者默认只有遇到换行\n才截止,因此当我们需要从键盘读取一个含有空格的字符串是,只能用getline。

七,与类型转换有关的接口

📖string类型转成其他内置类型
在这里插入图片描述
📖to_string将内置类型转成string类型
在这里插入图片描述

八,编码问题

我们平时使用的 string 本质上是通过对类模板 basic_string 用字符型 char 实例化得到的。
在这里插入图片描述
搞成模板的原因是为了兼容其他字符类型字符串的管理。即字符不只有 char 类型,还有 wchar_t 、char16_t、char32_t等,它们的区别在于存储一个字符所需要的字节数不同。这些不同类型的字符本质上和编码有关,我们目前C/C++中接触最多的是 ASCII 编码,它是用一个字节来存储一个字符。编码产生的本质是,计算机底层只认识二进制01序列,并不认识这些字符,因此要将字符存到计算机里面就需要建立一个字符与二进制01的对应关系,这个这关关系也被叫做映射表,也就是我们所说的编码表,而 ASCII 表是老美搞出来表示它们国家常用字符的映射表,大多是一些字母和符号。为了表示我们国家的文字,我们国家出台了 gbk 编码,此外国际上还有 unicode 编码(万国码)。

💘不知不觉,【C++初阶】一篇手撕string类 学习告一段落。通读全文的你肯定收获满满,让我们继续为C++学习共同奋进!!!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hallelujah...

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值