【编码艺术:掌握String类函数接口的妙用指南】

【本节目标】

  • 1. 为什么要学习string类

  • 2. 标准库中的string类

1. 为什么要学习string类

1.1 C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数, 但是这些库函数与字符串是分离开的,不太符合OOP(面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

2. 标准库中的string类

2.1 string类(了解)

string类的文档介绍

【编码艺术:掌握String类函数接口的妙用指南】_第1张图片

总结:

1. string是表示字符串的字符串类

2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。

3. string在底层实际是:basic_string模板类的别名,typedef basic_string string;

4. 不能操作多字节或者变长字符的序列。

在使用string类时,必须包含#include头文件以及using namespace std。

2.2 string类的常用接口说明

1. string类对象的常见构造

【编码艺术:掌握String类函数接口的妙用指南】_第2张图片

void Teststring()
{
	string s1; // 构造空的string类对象s1
	string s2("hello bit"); // 用C格式字符串构造string类对象s2
	string s3(s2); // 拷贝构造s3
	string s4 = s3;// 拷贝构造s4
}

我们再来介绍一下string其他相关的构造

【编码艺术:掌握String类函数接口的妙用指南】_第3张图片

(3)Copies the portion(一部分) of str that begins at the character position pos and spans(跨越) len characters (or until the end of str, if either str is too short or if len is string::npos).

我们来使用一下该函数:

string (const string& str, size_t pos, size_t len = npos);

【编码艺术:掌握String类函数接口的妙用指南】_第4张图片

我们再来看一下npos是什么?

【编码艺术:掌握String类函数接口的妙用指南】_第5张图片

所以我们就可以完全理解上面的函数:复制字符串 str 从字符位置 pos 开始,跨越 len 个字符(或者直到 str 的末尾,如果 str 太短或 lenstring::npos)的部分。

【编码艺术:掌握String类函数接口的妙用指南】_第6张图片

(5)Copies the first n characters from the array of characters pointed by s.

我们来使用一下该函数:

string (const char* s, size_t n);

【编码艺术:掌握String类函数接口的妙用指南】_第7张图片

这个构造函数比较简单,就是复制字符数组 s 指向的前 n 个字符,不过这里要注意该函数的第一个参数是字符串,而不是string类滴对象。

(5)Fills the string with n consecutive(连续的) copies of character c.

我们来使用一下该函数:

string (size_t n, char c);

【编码艺术:掌握String类函数接口的妙用指南】_第8张图片

这个构造函数就是用字符 c 的连续副本填充字符串,填充的数量为 n。

(7)Copies the sequence of characters in the range [first,last), in the same order.

这个构造函数涉及迭代器,我们后面再讲解。

2.string类对象的析构函数

【编码艺术:掌握String类函数接口的妙用指南】_第9张图片

析构函数会自动调用的,我们可以不用重点关注在这一块。

3. string类对象的赋值

【编码艺术:掌握String类函数接口的妙用指南】_第10张图片

这里赋值可以支持string类的对象常量字符串字符

【编码艺术:掌握String类函数接口的妙用指南】_第11张图片

4. string类对象的遍历和访问

【编码艺术:掌握String类函数接口的妙用指南】_第12张图片

  • 4.1.通过下标 + [ ]运算符重载实现
【编码艺术:掌握String类函数接口的妙用指南】_第13张图片
//遍历和访问
int main()
{
	string str("Test string");

	//下面两个函数结果相同,结果是不包含'\0'的
	cout << "size:" << str.size() << endl;//推荐使用
	cout << "length:" << str.length() << endl;

	for (size_t i = 0; i < str.size(); i++)
	{
		cout << str[i];
	}
	cout << endl;
	return 0;
}

运行结果:

我们再来写一个字符串的逆序

//字符串的逆序
int main()
{
	string str("Test string");

	int begin = 0, end = str.size() - 1;
	while (begin < end)
	{
		char tmp = str[begin];
		str[begin] = str[end];
		str[end] = tmp;
		++begin;
		--end;
	}
	for (size_t i = 0; i < str.size(); i++)
	{
		cout << str[i];
	}
	cout << endl;
	return 0;
}

上面的逆序字符串交换的这个代码很繁琐,C++为我们提供了Swap函数接口

【编码艺术:掌握String类函数接口的妙用指南】_第14张图片

于是我们根据上面的swap函数就可以这样写

//字符串的逆序
int main()
{
	string str("Test string");

	int begin = 0, end = str.size() - 1;
	while (begin < end)
	{
		swap(str[begin], str[end]);
		++begin;
		--end;
	}
	for (size_t i = 0; i < str.size(); i++)
	{
		cout << str[i];
	}
	cout << endl;
	return 0;
}

运行结果:

我们现在再来看一个细节,也就是我们之前提到的const的成员函数,这里的[ ]运算符重载实现了两个,这里是两个不同的函数,它们的参数是不同的,第二个隐形的this指针用了const修饰,这里我们要提一下参数匹配的问题。

【编码艺术:掌握String类函数接口的妙用指南】_第15张图片

#include 
#include 
using namespace std;

int main()
{
	string s1("hello world");
	const string s2("hello world");
	// 如果只实现了const char& operator[] (size_t pos) const;
	s1[0];//权限缩小
	s2[0];//权限平移
	// 但是此时返回值是const char& - 返回值不可修改
	//s2[0] = 'x';//error:error C3892: “s2”: 不能给常量赋值
	//所以就提供了char& operator[] (size_t pos);
	s1[0] = 'x';
	return 0;
}
  • 4.2.迭代器(Iterator)
//迭代器iterator:遍历和访问
int main()
{
	string str("Test string\0");
	//定义在类域里面,需要域作用限定符才可以访问
	//迭代器iterator是一个类型,用法像指针
	//[begin,end)左边右开区间
	string::iterator it = str.begin();
	while (it != str.end())
	{
		*it += 1;//访问修改
		cout << *it;//遍历
		++it;
	}
	cout << endl;
	return 0;
}

运行结果:

解析:

【编码艺术:掌握String类函数接口的妙用指南】_第16张图片

总结:虽然下标 + [ ]很方便,但是它仅仅适用于这些底层物理空间连续,比如string、vector等等。但是链式结构,树形和哈希结构,只能用迭代器,迭代器才是容器访问主流形态。

 所以我们上面的逆序就不用写上面的这么多代码,C++为我们提供了逆置的函数:reverse

【编码艺术:掌握String类函数接口的妙用指南】_第17张图片

所以就可以直接这样写:

//字符串的逆序
int main()
{
	string str("Test string");
	string::iterator it = str.begin();
	reverse(str.begin(), str.end());
	while (it != str.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;
	return 0;
}

运行结果:

上面通过函数模板实现,注意泛型编程不是针对某个容器的迭代器实现的,函数模板是针对各个容器迭代器实现。关于我们的迭代器,begin获取一个字符的迭代器,end获取最后一个字符下一个位置,普通的迭代器是可读可写的,但是这里也有一个细节,我们这里迭代器也实现了重载,const重载的只能进行可读

【编码艺术:掌握String类函数接口的妙用指南】_第18张图片

当我们对只读对象进行迭代器遍历的时候,就出现错误了。同时这里还有一个细节,我们发现上面是const_iterator,而不是const iterator,中间多了一个_,这里的const_iterator本质上是保护迭代器指向的数据"*it"不能被修改,而不是const iterator是迭代器本身不能被修改,也就"it"不能被修改,否则这样无法++,无法遍历,不符合我们的需求。

【编码艺术:掌握String类函数接口的妙用指南】_第19张图片

4.3.反向迭代器

我们也可以通过反向迭代器进行遍历和访问,但是此时的遍历是逆序的。

【编码艺术:掌握String类函数接口的妙用指南】_第20张图片

样例:

【编码艺术:掌握String类函数接口的妙用指南】_第21张图片

我们可以观察到反向迭代器也实现了两个版本,但是普通情况下我们基本上很少定义const对象,只有在传参的时候我们才最容易出现const对象。

//void fun(const string s1){}//调用拷贝构造,深拷贝,代价大
void fun(const string& s1)//引用,作为str的别名,开销小
{
	//error
	//string::reverse_iterator it = s1.rbegin();
	//const的对象调用rbegin应该返回const_reverse_iterator
	string::const_reverse_iterator it = s1.rbegin();
	while (it != s1.rend())
	{
		*it = 1;//const迭代器不可修改,error:“it”: 不能给常量赋值	
		cout << *it;
		++it;
	}
	cout << endl;
}

int main()
{
	string str("Test string");
	string::reverse_iterator it = str.rbegin();
	while (it != str.rend())
	{
		*it = 1;//普通迭代器不可修改
		cout << *it;
		++it;
	}
	cout << endl;
	fun(str);
	return 0;
}

不过通常我们都使用下标 + [ ]进行逆序遍历,但是反向迭代器也是非常有用的,对于链表这种没有下标的逆序遍历就需要使用反向迭代器。对于上面的迭代器,类型名都是非常长的,而且比较容易写错,这里我们可以使用之前我们讲到的auto关键字自动推至类型,但是这个写法对代码的可读性不好。

【编码艺术:掌握String类函数接口的妙用指南】_第22张图片

关于迭代器这里,之前的迭代器实现了两个版本,容易混淆,于是C++11就对const对象的迭代器进行了单独的处理,在普通迭代器前面加上了字符'c'表示此时的对象是cosnt的。

【编码艺术:掌握String类函数接口的妙用指南】_第23张图片

4.4.范围for
#include 
using namespace std;

int main()
{
	string str("Test string");

	for (auto e : str)
	{
		cout << e;
	}
	cout << endl;
	return 0;
}

范围for在我们这里不仅可以支持string,还能支持vertor,list等其他容器。我们现在再来介绍一下下面的元素存取相关函数。

我们先来看一个代码

【编码艺术:掌握String类函数接口的妙用指南】_第24张图片

当我们如果不小心访问了越界元素,此时程序就会报错,但是此时看右边我们知道是assert断言出错,如果我们不想出现这个报错界面,并且提示一下错误信息,就可以使用at,at也可以访问元素,它返回字符串中位置 pos 处的字符的引用,当访问越界的时候,此时提示信息"invalid string position"。

【编码艺术:掌握String类函数接口的妙用指南】_第25张图片

我们来验证一下。

【编码艺术:掌握String类函数接口的妙用指南】_第26张图片

再来看一下front和back,它们是访问字符串的头位置和尾位置的字符

【编码艺术:掌握String类函数接口的妙用指南】_第27张图片

int main()
{
	string s("hello world");
	cout << s.front() << endl;
	cout << s.back() << endl;

	cout << s[0] << endl;
	cout << s[s.size() - 1] << endl;
	return 0;
}

运行结果:

一般情况上我们很少用frong和back,因为我们可以通过pos为0和size-1位置访问头和尾元素。

5. string类对象的容量操作

【编码艺术:掌握String类函数接口的妙用指南】_第28张图片

5.1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()。
【编码艺术:掌握String类函数接口的妙用指南】_第29张图片5.2.max_size()返回字符串可以达到的最大长度,不同的机器这个最大长度是不一样的,下面的测试是32位机器下,

【编码艺术:掌握String类函数接口的妙用指南】_第30张图片

【编码艺术:掌握String类函数接口的妙用指南】_第31张图片

虽然上面给了我们这个字符串能到达的最大长度,但是这个值没有参考意义,实际上我们开辟不了这个最大长度的空间,使得字符串到达的最大长度。我们来验证一下

int main()
{
	try
	{
		string str1;
		string str2("Test string");

		cout << "str1:" << str1.max_size() << endl;
		cout << "str2:" << str2.max_size() << endl;
		//reverse:为字符串预留空间 - 相当于扩容
        //实践中没有参考的价值和意义		
        str1.reserve(str1.max_size());
	}
	catch(const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果:

5.3.capacity()返回空间总大小

【编码艺术:掌握String类函数接口的妙用指南】_第32张图片注意:这里的容量是有效字符的数量,而'\0'不属于有效字符,这里实际上开了16个字节的空间,其中一个用来存 '\0'。【编码艺术:掌握String类函数接口的妙用指南】_第33张图片

我们这里还能用来检测string的扩容机制

int main()
{
	try
	{
		string str1;

		size_t old = str1.capacity();
		cout << "str1:" << str1.capacity() << endl;
		//检测string的扩容机制
		for (size_t i = 0; i < 100; i++)
		{
			str1.push_back('x');
			if (old != str1.capacity())
			{
				cout << "str1:" << str1.capacity() << endl;
				old = str1.capacity();
			}
		}
	}
	catch(const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

vs下结果:P. J. 版本 - 1.5倍扩容

【编码艺术:掌握String类函数接口的妙用指南】_第34张图片

Linux(g++)下结果:SGI版本 - 2倍扩容

【编码艺术:掌握String类函数接口的妙用指南】_第35张图片

5.4.reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间capacity总大小时,reserver不会改变容量大小。
int main()
{
	try
	{
		string str1;

		str1.reserve(100);
		size_t old = str1.capacity();
		cout << "str1:" << str1.capacity() << endl;
		//检测string的扩容机制
		for (size_t i = 0; i < 100; i++)
		{
			str1.push_back('x');
			if (old != str1.capacity())
			{
				cout << "str1:" << str1.capacity() << endl;
				old = str1.capacity();
			}
		}
	}
	catch(const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

reserve可以和capaciy搭配使用,当使用reverse(100)的时候,此时就已经开辟了100空间,后面就不需要扩容了。如果我们确定需要多少空间时,提前开空间即可,此后就不需要扩容了。但是这里我们只需开辟100个空间,而程序却开辟了111个空间,因为vs下可能存在了内存对齐机制,会以它内部的规则去对齐,从而会开辟更大一点,Linux下直接开辟你确定所要的空间。

vs下运行结果:

Linux下的结果:

我们再来看一下当reserve的参数小于 string的底层空间capacity总大小时,reserver不会改变容量大小。

int main()
{
	string str2("hello world!xxxxxxxxxxx");
	str2.reserve(100);
	cout << "str2:" << str2.capacity() << endl;
	str2.reserve(5);
	cout << "str2:" << str2.capacity() << endl;
	return 0;
}

vs下运行结果:

Linux下的结果:                                                                                                                               

【编码艺术:掌握String类函数接口的妙用指南】_第36张图片

当reserve的参数小于 string的底层空间capacity总大小时,vs平台下reserver不会改变容量大小,Linux下reserver会改变容量大小,但是它缩小空间只会缩小到该字符串的有效字符数量size处,它是不会影响到已经存储的字符串的。

5.5. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用’\0'来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。

【编码艺术:掌握String类函数接口的妙用指南】_第37张图片

reserve只影响我们的容量,不会影响数据,而resize既影响容量,也会影响数据。我们会发现s1的大小和容量都增加了,那它是用什么填充这些空间的呢?

【编码艺术:掌握String类函数接口的妙用指南】_第38张图片

我们先来看一下大于当前容量的情况,我们可以通过监视窗口观察到,但是'\0'不一定是标识字符,有效字符属于0到size-1范围内。

【编码艺术:掌握String类函数接口的妙用指南】_第39张图片

我们再来看一下大于size但是小于capacity的情况

【编码艺术:掌握String类函数接口的妙用指南】_第40张图片

此时容量大小没有改变,只变化了数据,仅仅变化了size。如果小于size呢?

【编码艺术:掌握String类函数接口的妙用指南】_第41张图片

看一下监视窗口

【编码艺术:掌握String类函数接口的妙用指南】_第42张图片

此时删除了数据,只保留前resize个。resize应用的场景是开辟空间并指定字符初始化。注意:C++中new对应C语言中的malloc函数,而C++中没有C语言中对应的realloc扩容函数。所以C++中,我们如果使用字符数组,就会使用string,而string中提供了相应的接口,从而进行扩容。

5.6.clear()只是将string中有效字符清空,不改变底层空间大小。

【编码艺术:掌握String类函数接口的妙用指南】_第43张图片

6. string类对象的增删查改操作

【编码艺术:掌握String类函数接口的妙用指南】_第44张图片

6.1.增加

        在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。

【编码艺术:掌握String类函数接口的妙用指南】_第45张图片

这里有一个问题:string s1 = "hello world";//这个可行吗?可行,这里是一个单参数的构造函数,它会隐式类型转换,它先去构造,然后再拷贝构造,不过这里编译器可能会优化成直接构造。

【编码艺术:掌握String类函数接口的妙用指南】_第46张图片

我们来看一下append的使用。

int main()
{
	std::string str;
	std::string str2 = "Writing ";
	std::string str3 = "print 10 and then 5 more";

	// used in the same order as described above:
	str.append(str2);                       // "Writing "
	str.append(str3, 6, 3);                   // "10 "
    str.append("here: ");                   // "here: "
	str.append("dots are cool", 5);          // "dots "
	str.append(10u, '.');                    // ".........."
	str.append(str3.begin() + 8, str3.end());  // " and then 5 more"

	std::cout << str << '\n';
	return 0;
}

我们上面有了尾插,那我们有没有头插呢?或者在中间插入呢?

【编码艺术:掌握String类函数接口的妙用指南】_第47张图片

// inserting into a string
#include 
#include 

int main ()
{
  std::string str="to be question";
  std::string str2="the ";
  std::string str3="or not to be";
  std::string::iterator it;

  // used in the same order as described above:
  str.insert(6,str2);                 // to be (the )question
  str.insert(6,str3,3,4);             // to be (not )the question
  str.insert(10,"that is cool",8);    // to be not (that is )the question
  str.insert(10,"to be ");            // to be not (to be )that is the question
  str.insert(15,1,':');               // to be not to be(:) that is the question
  it = str.insert(str.begin()+5,','); // to be(,) not to be: that is the question
  str.insert (str.end(),3,'.');       // to be, not to be: that is the question(...)
  str.insert (it+2,str3.begin(),str3.begin()+3); // (or )

  std::cout << str << '\n';
  return 0;
}

实际中头插使用较少,因为要挪动数据,效率不高。那删除字符呢?

6.2.删除

【编码艺术:掌握String类函数接口的妙用指南】_第48张图片

// string::erase
#include 
#include 

int main ()
{
  std::string str ("This is an example sentence.");
  std::cout << str << '\n';
                                           // "This is an example sentence."
  str.erase (10,8);                        //            ^^^^^^^^
  std::cout << str << '\n';
                                           // "This is an sentence."
  str.erase (str.begin()+9);               //           ^
  std::cout << str << '\n';
                                           // "This is a sentence."
  str.erase (str.begin()+5, str.end()-9);  //       ^^^^^
  std::cout << str << '\n';
                                           // "This sentence."
  return 0;
}
6.3.查找

注意:

  • pos含义:搜索字符串中要查找的第一个字符的位置。
  • 函数返回值:第一个匹配项的第一个字符的位置。 如果未找到任何匹配项,则该函数返回 string::npos
【编码艺术:掌握String类函数接口的妙用指南】_第49张图片
// string::find
#include        // std::cout
#include          // std::string

int main ()
{
  std::string str ("There are two needles in this haystack with needles.");
  std::string str2 ("needle");

  // different member versions of find in the same order as above:
  std::size_t found = str.find(str2);
  if (found!=std::string::npos)
    std::cout << "first 'needle' found at: " << found << '\n';

  found=str.find("needles are small",found+1,6);
  if (found!=std::string::npos)
    std::cout << "second 'needle' found at: " << found << '\n';

  found=str.find("haystack");
  if (found!=std::string::npos)
    std::cout << "'haystack' also found at: " << found << '\n';

  found=str.find('.');
  if (found!=std::string::npos)
    std::cout << "Period found at: " << found << '\n';

  // let's replace the first needle:
  str.replace(str.find(str2),str2.length(),"preposition");
  std::cout << str << '\n';

  return 0;
}

现在我们来使用一下find,查找字符串的后缀是什么?这里我们会使用到substr。

int main()
{
	string s1("Test.cpp");
	string s2("Test.tar.zip");

	size_t pos1 = s1.find('.');
	if(pos1 != string::npos)
	{
		string suff1 = s1.substr(pos1, s1.size() - pos1);
		cout << suff1 << endl;//.cpp
		string suff2 = s1.substr(pos1);//默认取到结尾
		cout << suff2 << endl;//.cpp
	}

	size_t pos2 = s2.find('.');
	if (pos2 != string::npos)
	{
		string suff1 = s2.substr(pos2, s2.size() - pos2);
		cout << suff1 << endl;//.tar.zip
		string suff2 = s2.substr(pos2);//默认取到结尾
		cout << suff2 << endl;//.tar.zip
	}

	//搜索字符串中由参数指定的序列的最后一次出现
	size_t pos3 = s2.rfind('.');
	if (pos2 != string::npos)
	{
		string suff1 = s2.substr(pos3, s2.size() - pos3);
		cout << suff1 << endl;//.zip
		string suff2 = s2.substr(pos3);//默认取到结尾
		cout << suff2 << endl;//.zip
	}
	return 0;
}

运行结果:

【编码艺术:掌握String类函数接口的妙用指南】_第50张图片

如何将一个网址分开呢?

#include 
#include 
using namespace std;

int main()
{
	string str("https://cplusplus.com/reference/string/string/substr/");
	string sub1, sub2, sub3;
	size_t pos1 = str.find(':');
	//左闭右开区间
	sub1 = str.substr(0, pos1 - 0);
	cout << sub1 << endl;
	size_t pos2 = str.find('/', pos1 + 3);
	sub2 = str.substr(pos1 + 3, pos2 - (pos1 + 3));
	cout << sub2 << endl;
	sub3 = str.substr(pos2 + 1);
	cout << sub3 << endl;
	return 0;
}

运行结果:

【编码艺术:掌握String类函数接口的妙用指南】_第51张图片

6.4.修改
【编码艺术:掌握String类函数接口的妙用指南】_第52张图片
// replacing in a string
#include 
#include 

int main ()
{
  std::string base="this is a test string.";
  std::string str2="n example";
  std::string str3="sample phrase";
  std::string str4="useful.";

  // replace signatures used in the same order as described above:

  // Using positions:                 0123456789*123456789*12345
  std::string str=base;           // "this is a test string."
  str.replace(9,5,str2);          // "this is an example string." (1)
  str.replace(19,6,str3,7,6);     // "this is an example phrase." (2)
  str.replace(8,10,"just a");     // "this is just a phrase."     (3)
  str.replace(8,6,"a shorty",7);  // "this is a short phrase."    (4)
  str.replace(22,1,3,'!');        // "this is a short phrase!!!"  (5)

  // Using iterators:                                               0123456789*123456789*
  str.replace(str.begin(),str.end()-3,str3);                    // "sample phrase!!!"      (1)
  str.replace(str.begin(),str.begin()+6,"replace");             // "replace phrase!!!"     (3)
  str.replace(str.begin()+8,str.begin()+14,"is coolness",7);    // "replace is cool!!!"    (4)
  str.replace(str.begin()+12,str.end()-4,4,'o');                // "replace is cooool!!!"  (5)
  str.replace(str.begin()+11,str.end(),str4.begin(),str4.end());// "replace is useful."    (6)
  std::cout << str << '\n';
  return 0;
}

根据上面的查找和修改可以轻松解决一个我们曾经遇到的问题,将日期"2023-11-23"中的'-'修改为'/'。

int main()
{
	string str("2023-11-23");
	cout << str << endl;
	size_t pos = str.find('-');
	// '-'修改为'/'
	while (pos != string::npos)
	{
		str.replace(pos, 1, 1, '/');
		pos = str.find('-');
	}
	cout << str << endl;
	return 0;
}

我们看一下我们的代码有什么问题没?我们发现我们每次找'-'都是从字符串的其实位置开始找,那么这也效率比较低,其实我们第一找到'-'后,得到第一次出现'-'的位置,后面再找'-'就可以从上次找的位置+1即可,这样效率就提高很多,但是replace的效率比较低,我们下面替换一个字符还好,但是如果替换成'///'时,此时就要往后挪动数据才能插入,这样效率比较低,所以replace我们能少用尽量就少用。

int main()
{
	string str("2023-11-23");
	cout << str << endl;
	size_t pos = str.find('-', 0);
	// '-'修改为'/'
	if (pos != string::npos)
	{
		str.replace(pos, 1, 1, '/');
		pos = str.find('-', pos + 1);
	}
	cout << str << endl;
	return 0;
}

所以这里我们有更好的方法,使用范围for+赋值。

int main()
{
	string str("2023-11-23");
	cout << str << endl;
	string str1;
	for (auto ch : str)
	{
		if (ch == '-')
			str1 += '/';
		else
			str1 += ch;
	}
	cout << str1 << endl;
    str.swap(str1);
	return 0;
}

运行结果:

这里有一个细节问题:我们上面使用的是C++ 标准库中 std::string 类的成员函数。

string::swap 是 C++ 标准库中 std::string 类的成员函数,用于交换两个字符串的内容。它是在字符串对象上调用的函数,例如:

std::string str1 = "Hello";
std::string str2 = "World";

str1.swap(str2); // 交换 str1 和 str2 的内容

swap 是一个通用的 C++ 函数,用于交换两个对象的值。对于字符串来说,可以使用 std::swap 或直接使用 swap 来交换两个字符串的内容,例如:

std::string str1 = "Hello";
std::string str2 = "World";

std::swap(str1, str2); // 交换 str1 和 str2 的内容
// 或者直接使用 swap
swap(str1, str2);

主要区别在于调用方式和命名空间。string::swapstd::string 类的成员函数,而 swap 是一个通用的函数,可以在合适的作用域下直接使用或通过 std::swap 来调用。两者都可以用于交换字符串的内容。同时std::swap 参数是函数模板,使用的时候实例化,需要拷贝,消耗较大,而string::swap 是直接交换两个字符串的地址,后面实现string类的时候可以看到。

6.5.string::c_str:返回一个指向数组的指针,该数组包含表示字符串对象当前值的以 null 结尾的字符序列(即,一个 C 字符串)。
int main()
{
	string s1 = "Sample string";
	char* s2 = new char[s1.length() + 1];
	//char * strcpy ( char * destination, const char * source );
	//strcpy(s2, s1);//参数类型不匹配
	//const char* c_str() const;//返回值为const char*
	strcpy(s2, s1.c_str());
    delete[] s2;
	return 0;
}
7. string类非成员函数

【编码艺术:掌握String类函数接口的妙用指南】_第53张图片

【编码艺术:掌握String类函数接口的妙用指南】_第54张图片

这里为什么实现了两种参数不同的方式?

        这两个重载的 operator+ 函数是为了提供更大的灵活性,使得在字符串的拼接操作中更方便使用不同类型的参数。这是一种 C++ 中的重载技术,允许相同的操作符在不同的情境下具有不同的行为。

让我们分析这两个重载函数:

  1. string operator+ (const string& lhs, const char* rhs);

    • 允许将一个 std::string 对象和一个 C 字符串(以 const char* 表示)进行拼接。
    • 这使得你可以直接将一个 C 字符串连接到一个 std::string 对象的末尾。
  2. string operator+ (const char* lhs, const string& rhs);

    • 允许将一个 C 字符串和一个 std::string 对象进行拼接。
    • 这使得你可以直接将一个 std::string 对象连接到一个 C 字符串的末尾。

这两种方式的存在是为了方便用户在不同的场景下进行字符串的拼接,无论是从 std::string 到 C 字符串,还是从 C 字符串到 std::string

7.1.getline (string):从输入流 is 中提取字符并将它们存储到字符串 str 中,直到找到分隔字符 delim(或者对于 (2) 来说是换行符 '\n')。

流插入字符输入字符时默认以空格或者换行结束。

【编码艺术:掌握String类函数接口的妙用指南】_第55张图片

如果不想以空格作为字符输出结束的标志,就要使用getline().

【编码艺术:掌握String类函数接口的妙用指南】_第56张图片

上面的几个接口了解一下,下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的 操作,这里不一一列举,在需要用到时不明白了查文档即可。

7.1. vs和g++下string结构的说明

我们先来看一下下面的代码会输出上面呢?

int main()
{
	//sizeof计算的是一个类的大小
	//和空间上存放的数据没有关系
	string str1;
	cout << sizeof(str1) << endl;

	string str2("test string");
	cout << sizeof(str2) << endl;
	return 0;
}

我们先来分析一下

【编码艺术:掌握String类函数接口的妙用指南】_第57张图片

32位平台下大小应该是4+4+4=12字节。我们来看一下程序的输出结果

【编码艺术:掌握String类函数接口的妙用指南】_第58张图片

为什么是28呢?我们来解释一下。

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

  • vs下string的结构 string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义
  • string中字 符串的存储空间: 当字符串长度小于16时,使用内部固定的字符数组来存放 当字符串长度大于等于16时,从堆上开辟空间
  • 当字符串长度小于16时,使用内部固定的字符数组来存放 -  _Buf
  • 当字符串长度大于等于16时,从堆上开辟空间 - _Ptr
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字段保存从堆上开辟空间总的容量

最后:还有一个指针做一些其他事情。

故总共占16+4+4+4=28个字节。

【编码艺术:掌握String类函数接口的妙用指南】_第59张图片

Linux下面就只存放了一个指针,我们可以试一下Linux下的大小。

【编码艺术:掌握String类函数接口的妙用指南】_第60张图片

不是说存放的指针吗?为什么这里是8个字节,因为Linux下是64位平台,具体指针存放的内容是什么呢?

【编码艺术:掌握String类函数接口的妙用指南】_第61张图片

Linxu下指针指向的内容是字符串,指针向前减四字节就是引用计数,再向前减四个字节就是就是容量capacity,再减四个字节就是大小size。

【编码艺术:掌握String类函数接口的妙用指南】_第62张图片

7.1.1.引用计数

这里为什么要这样设置,我们先来看一下传值返回

【编码艺术:掌握String类函数接口的妙用指南】_第63张图片

如果让它指向ret就发生了浅拷贝。

【编码艺术:掌握String类函数接口的妙用指南】_第64张图片

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给 计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该 对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。这里就解决了析构函数的析构两次出错的问题,对于浅拷贝的另一个问题,需要写时拷贝解决。

7.1.2.写时拷贝

【编码艺术:掌握String类函数接口的妙用指南】_第65张图片

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的,这里的意义在于如果拷贝后不修改内容,那就赚啦。写时拷贝当引用计数为1时才会尝试修改数据时,才会进行实际的复制,以确保独立的修改。

写时拷贝

写时拷贝在读取是的缺陷

8.小试牛刀

仅仅反转字母

class Solution {
public:
	bool isLetter(char ch)
	{
		if (ch >= 'a' && ch <= 'z')
			return true;
		if (ch >= 'A' && ch <= 'Z')
			return true;
		return false;
	}
	string reverseOnlyLetters(string S)
	{
		if (S.empty())
			return S;

		size_t begin = 0, end = S.size() - 1;
		while (begin < end)
		{
			while (begin < end && !isLetter(S[begin]))
				++begin;

			while (begin < end && !isLetter(S[end]))
				--end;
			swap(S[begin], S[end]);
			++begin;
			--end;
		}
		return S;
	}
};

找字符串中第一个只出现一次的字符   

class Solution{
public:
 int firstUniqChar(string s) {

	 // 统计每个字符出现的次数
	 int count[256] = {0};
	 int size = s.size();
	 for (int i = 0; i < size; ++i)
	 count[s[i]] += 1;

	 // 按照字符次序从前往后找只出现一次的字符
	 for (int i = 0; i < size; ++i)
	 if (1 == count[s[i]])
	 return i;

	 return -1;
	 }
};

字符串里面最后一个单词的长度  

#include
#include
using namespace std;
int main()
{
	string line;
	// 不要使用cin>>line,因为会它遇到空格就结束了
	// while(cin>>line)
	while (getline(cin, line))
	{
		size_t pos = line.rfind(' ');
		cout << line.size() - pos - 1 << endl;
	}
	return 0;
}

验证一个字符串是否回文 

class Solution {
public:
bool isPalindrome(string s) {
    int begin = 0;
    int end = s.size() - 1;
    while (begin < end)
    {
        while (!isalpha(s[begin]) && !isdigit(s[begin]) && begin < end)
            begin++;
        while (!isalpha(s[end]) && !isdigit(s[end]) && begin < end)
            end--;

        if (s[begin] >= 'A' && s[begin] <= 'Z')
            s[begin] = tolower(s[begin]);
        if (s[end] >= 'A' && s[end] <= 'Z')
            s[end] = tolower(s[end]);

        if (s[begin] != s[end])
            return false;

        if (begin < end)
        {
            ++begin;
            --end;
        }
    }
    return true;
    }
};

字符串相加​​​​​​​

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        string str;
        //进位
        int next = 0;
        while(end1 >= 0 || end2 >= 0)
        {
            int value1 = 0,value2 = 0;
            if(end1 >= 0)
                value1 = num1[end1--] - '0';
            if(end2 >= 0)
                value2 = num2[end2--] - '0';

            int addvalue = value1 + value2 + next;
            next = addvalue/10;
            addvalue %= 10;
            str.insert(0,1,addvalue + '0');
        }
        if(next == 1)
        {
            str.insert(0,1,'1');
        }
        return str;
    }
};

【编码艺术:掌握String类函数接口的妙用指南】_第66张图片

你可能感兴趣的:(C++初阶,c++)