【C++】第九节—string类(中)——详解+代码示例

发布于:2025-04-07 ⋅ 阅读:(27) ⋅ 点赞:(0)

hello! 

云边有个稻草人-CSDN博客

C++_云边有个稻草人的博客-CSDN博客

菜鸡进化中。。。 

目录

【补充】

二、string类里面的常用接口

1.resize

2.insert

3.assign

4.erase

5.replace+find

6.c_str

7.rfind+substr

8.find_first_of、find_last_of、find_first_not_of、find_last_not_of

9.算数类型和string之间的转换

总结

三、例题

1.字符串中的第一个唯一字符

2.字符串最后一个单词的长度+getline

3.验证回文串

四、string的模拟实现

Q1:无参的构造函数把_str初始化为nullptr可以吗?

模拟实现

string.h

string.cpp

test.cpp

《Right Now (Na Na Na)》


回顾上节【C++】第八节—string类(上)——详解+代码示例-CSDN博客

正文开始——

string - C++ Reference】英文文档,全程观看,理解效果更佳!

【补充】

什么是迭代器?Iterator就是迭代器的意思

概念:迭代器是一种检查容器内元素并遍历元素的数据类型,通常用于对C++中各种容器内元素的访问,但不同的容器有不同的迭代器,初学者可以将迭代器理解为指针

begin()和end()

顾名思义,begin()就是指向容器第一个元素的迭代器 如果你是初学者,你可能会猜到 end()是指向容器最后一个元素的迭代器, 但事实并非如此,实际上,end()是指向容器最后一个元素的下一个位置的迭代器。

上节课我们已经介绍了如何使用迭代器。

续上节


二、string类里面的常用接口

1.resize

#include<iostream>
#include<string>

using namespace std;

int main()
{
	string s1("eeeeeeeeeeeeeeee");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;

	//s1.resize(12);
	//cout << s1 << endl;
	//cout << s1.size() << endl;
	//cout << s1.capacity() << endl << endl;

	//s1.resize(20);
	//cout << s1 << endl;
	//cout << s1.size() << endl;
	//cout << s1.capacity() << endl << endl;

	//s1.resize(40);
	//cout << s1 << endl;
	//cout << s1.size() << endl;
	//cout << s1.capacity() << endl << endl;

	s1.resize(40,'x');
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;

	return 0;
}

2.insert

string::insert - C++ Reference

这个接口使用的不是很多,对于插入是要挪动数据效率就比较低,我们了解一下即可,需要的时候可以查看文档使用


3.assign

单词不认识就先搜一下看看大概意思猜猜它的大概功能是什么

assign在功能上是一种赋值,operator=比assign要简单一点,operator=是将对象赋值给另外一个对象,进行对象之间的拷贝,assign支持的更复杂一点,见下:(该接口了解即可,使用的时候查看文档)

int main()
{
	string s1("hello world");
	string s2("OOOOOOO");
	s2.assign(s1, 0, 4);
	cout << s2 << endl;

	return 0;
}

4.erase

我们根据上面的介绍来体验一下这些功能,见下:

int main()
{
	//不给第二个参数默认将后面的全部删除
	string s1("QQQQQQQQQQQ");
	s1.erase(2);
	cout << s1 << endl;

	//删除中间的部分
	string s2("hello world hello earth");
	s2.erase(5, 6);
	cout << s2 << endl;

	//实现头删
	string s3("hello world hello earth");
	s3.erase(0, 1);
	cout << s3 << endl;

	//同样可以利用迭代器来实现头删
	string s4("hello world hello earth");
	s4.erase(s4.begin());
	cout << s4 << endl;

	//当后面一个参数大于后面的长度时,默认是将后面全部删除
	string s5("hello world hello earth");
	s5.erase(5, 900);
	cout << s5 << endl;

	return 0;
}

5.replace+find

这个函数也建议少用,如果不是平替的话就涉及了代码的整体后移,效率低下,所以这个接口建议在平替的时候使用效果最好

int main()
{
	string s1("hello world");
	cout << s1 << endl;

	s1.replace(5, 1, "%%");//第五个位置开始的一个字符替换成"%%"
	cout << s1 << endl;

	return 0;
}

int main()
{
	string s1("hello world");
	cout << s1 << endl;

	s1.replace(5, 2, "%%");//第五个位置开始的一个字符替换成"%%"
	cout << s1 << endl;

	return 0;
}

这让我们想起来以前有个将字符串里面的空格替换成别的字符的问题,我们再来看一下用现在学的知识如何来解决

这时候就需要另外一个接口来进行搭配使用——find

int main()
{
	string s1("hello world hello lrq");
	cout << s1 << endl;

	size_t i = s1.find(' ');
	while (i != string::npos)//npos被视为无效下标,使用类里面的用静态成员变量要声明类域
	{
		s1.replace(i, 1, "%%");
		i = s1.find(' ', i + 2);//简化了,可以直接从指定位置开始找
	}
	cout << s1 << endl;
	return 0;
}

int main()
{
	string s1("hello world hello lrq");
	cout << s1 << endl;

	size_t i = s1.find(' ');
	while (i != string::npos)//npos被视为无效下标,使用类里面的用静态成员变量要声明类域
	{
		s1.replace(i, 1, "%%");
		i = s1.find(' ', i + 2);
	}
	cout << s1 << endl;

	//还可以使用另一种玩法
	string s2;
	for (auto ch : s1)
	{
		if (ch != ' ')
			s2 += ch;
		else
			s2 += "%%";
	}
	cout << s2 << endl;

	return 0;
}

但是还是尽量少用replace,效率太低了,平替不用后移,但是其他的情况就比较差了。


6.c_str

返回存储的字符串的首元素的地址 _str

重载了流插入为什么还要用c_str呢,是为了跟C更好的兼容(根据下面的代码了解一下即可)

int main()
{
	string s1("hello world");
	cout << s1 << endl;
	cout << s1.c_str() << endl;

	string s2("Test.cpp");
	FILE* fout = fopen(s2.c_str(), "r");
	char ch = fgetc(fout);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(fout);
	}

	return 0;
}

7.rfind+substr

rfind是从最后一个有效元素倒着找目标字符,substr取部分字符串也就是取子串,现在我们的目标是取字符串里面的后缀名

int main()
{
	//将rfind和substr结合起来用,见下:此时我们要取后缀名
	string s1("test.cpp.zip");
	size_t pos = s1.rfind('.');
	if (pos != string::npos)
	{
		cout << s1.substr(pos) << endl;
	}

	return 0;
}


8.find_first_of、find_last_of、find_first_not_of、find_last_not_of

string - C++ Reference

自己结合文档代码实例思考一下是什么功能


9.算数类型和string之间的转换

to_string 和 stoi( string转成int ),to_string - C++ Reference,简单演示一下,自己看看文档理解

string s3 = to_string(11.11);

总结


三、例题

1.字符串中的第一个唯一字符

387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

class Solution {
public:
    int firstUniqChar(string s) {
        //开空间来存放出现次数的数组
        int count[26] = {0};

        //统计次数
        for(auto ch : s)
        {
            count[ch-'a']++;//映射到count里面
        }
        
        //找出字符串里面第一个只出现一次的字符-->再遍历一遍s
        //利用s里面的每个字符映射到count数组里面找为1的,找不到就返回-1
        for(int i = 0;i < s.size();i++)
        {
            if(count[s[i]-'a'] == 1)
                return i;
        }

        return -1;
    }
};

2.字符串最后一个单词的长度+getline

字符串最后一个单词的长度_牛客题霸_牛客网

根据上面的思路我们实现了下面的代码,

这是怎么回事呢?我们光看瞎捉摸肯定很难看出问题出在哪里,这时候我们可以启用VS去调试一下,看看内部运行结果是否符合我们的预期,我们前面学的VS调试技巧派上用场喽O(∩_∩)O哈哈~

无论是scanf还是cin都支持连续的从流里面去提取数据,我们在终端输入的数据会先输入到缓冲区(也就是数组),读取数据的时候是到缓冲区里面去取数据,这样可以提高效率。在缓冲区里面我们可能会输入多个值,默认空格和换行是多个值之间的分割。这也就意味着如果我们输入一行,一行里面有空格的话我们就无法取到后面的数据存储到同一个变量里面

C++标准库里面专门有那么一个函数来解决这个问题——getline

修改一下题目里面的代码,看看结果如何 

#include <iostream>
using namespace std;

int main()
{
    string s1;
    getline(cin,s1);
    
    size_t pos = s1.rfind(' ');//找到了倒数第一个空格的位置
    cout<<s1.size()-pos-1<<endl;

    return 0;
}

还有一点,getline还可以自定义结束符

总结:cin默认换行或者空格是分隔符,getline默认换行是分隔符,可以解决我们上面遇到的问题,而且getline可以自定义分隔符。

3.验证回文串

125. 验证回文串 - 力扣(LeetCode)

class Solution {
public:
    bool isLetterOrNumber(char ch)
    {
        if((ch >= 'a' && ch <= 'z')||(ch >= 48 && ch <= 57))
            return true;
        return false;
    }

    bool isPalindrome(string s) {
        //将所有大写字母转换为小写字母
        for(auto& ch : s)
        {
            if(ch >= 'A' && ch <='Z')
                ch += 32;
        }

        //利用双指针来判断
        int begin = 0,end = s.size()-1;
        while(begin < end)
        {
            while(begin < end && !isLetterOrNumber(s[begin]))
                begin++;
            while(begin < end && !isLetterOrNumber(s[end]))
                end--;
            if(s[begin++] != s[end--])
                return false;
        }
        return true;
    }
};

四、string的模拟实现

Q1:无参的构造函数把_str初始化为nullptr可以吗?

tip:我们自己实现的string会与库里面的string命名冲突,我们可以将我们自己实现的string放在一个命名空间里面就ok了,下面的lrq就是我自己命名空间的名称。

无参的构造函数
string::string()
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{}

模拟实现

我们这节课先实现一小部分,剩下的明天继续!

string.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

// 我们将自己写的string类封装在lrq这个命名空间里面,
// 这样我们在调用库里面的string函数就不会冲突了
namespace lrq
{
	class string
	{
	public:
		//typedef char* iterator;
		using iterator = char*;
		using const_iterator = const char*;

		//构造函数
		//string();
		string(const char* str = "");//这里给一个缺省值直接代替无参的,常量字符串结尾默认有一个\0,也不需要再给\0
		~string();                  //同时给缺省值的话,只在声明给,不能声明和定义同时给

		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);

		// 此函数是返回成员变量里面的_str,不期待被改变,这里形参权限被缩小了,这前面我们也讲过,
		// 加上const,无论是普通对象还是const对象都能使用该函数,首先是该函数不改变成员变量
		//比较短小的函数直接写成内联函数
		const char* c_str()const
		{
			return _str;
		}

		char& operator[](size_t i)
		{
			assert(i < _size);
			return _str[i];
		}

		const char& operator[](size_t i)const
		{
			assert(i < _size);
			return _str[i];
		}

		//迭代器
		iterator begin()
		{
			return _str;
		}

		iterator end()//指向最后一个有效数据的下一个位置
		{
			return _str+_size;
		}

		const_iterator begin()const
		{
			return _str;
		}

		const_iterator end()const
		{
			return _str + _size;
		}


	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
}

string.cpp

#include"string.h"

namespace lrq
{
	//构造函数
   //无参
	//string::string()
	//	:_str(new char[1] {'\0'})
	//	, _size(0)
	//	, _capacity(0)
	//{}

	//strlen是在运行时计算的,遇到\0才终止,sizeof是在编译时计算的
	string::string(const char* str)
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
	}

	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}

	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//永远比n多开一个空间来存放\0,比_capacity多1
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		_str[_size] = ch;
		_size++;
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			//扩容
			size_t newCapacity = 2 * _capacity;
			//扩2倍不够直接需要多少扩多少
			if (newCapacity < _size + len)
				newCapacity = _size + len;
			reserve(newCapacity);
		}
		strcpy(_str + _size, str);//注意开始拷贝的位置
		_size += len;
	}

	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
}

test.cpp

#include"string.h"

void test_string1()
{
	lrq::string s2;
	cout << s2.c_str() << endl;

	// 迭代器--像指针一样的东西
	lrq::string::iterator it1 = s2.begin();
	while (it1 != s2.end())
	{
		(*it1)--;// 修改
		cout << *it1 << " ";
		it1++;
	}
	cout << endl;

	while (it1 != s2.end())
	{
		cout << *it1 << " ";
		it1++;
	}
	cout << endl;

	// 范围for
	// 底层是迭代器的支持
	// 意味着支持迭代器就支持范围for
	for (auto& ch : s2)
	{
		ch++;//修改
	}

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

}

void test_string2()
{
	lrq::string s1("hello world");
	cout << s1.c_str() << endl;

	s1 += ' ';
	s1 += "hello lrq";
	cout << s1.c_str() << endl;
}


int main()
{
	test_string2();

	return 0;
}

运行成功! 

今天就先到这里了,明天继续干!

完——


《Right Now (Na Na Na)》

至此结束——

我是云边有个稻草人

期待与你的下一次相遇!