您现在的位置是:首页 >技术教程 >从C语言到C++_11(string类的常用函数)力扣58和415网站首页技术教程

从C语言到C++_11(string类的常用函数)力扣58和415

GR C 2024-06-17 10:48:56
简介从C语言到C++_11(string类的常用函数)力扣58和415

目录

1. 学习string的铺垫

1.1 什么是string类 

1.2 basic_string 模板类

1.3 编码表的由来

1.4 其它字符编码的string

2. string类对象的常见构造

3. sting类对象的容量操作

4. string类对象的访问及遍历操作

5. string类对象的修改操作

6. string类非成员函数

7. string的相关笔试题

8. string相关OJ题

字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)

代码解析:

58. 最后一个单词的长度 - 力扣(LeetCode)

代码解析:

力扣415. 字符串相加

代码解析:

本章完。


此篇算是STL的正式学习,string类的许多操作和以后很多的操作都是一样的,

所以此篇文章的接口函数讲得细一点,以后学习就会舒服很多。

1. 学习string的铺垫

语言中的字符串,是以  为结尾的一些字符的集合。 

为了方便操作,C标准库中提供了一些 str 系列的库函数。

但是这些库函数与字符串是分离开的,不太符合面向对象的思想。

而且底层空间需要用户自己管理,一不小心还会造成越界访问,很不方便。  

在工作中为了方便大多会使用 string 类,很少有人去使用 C 标准库中的字符串函数。

1.1 什么是string类 

简单来说,string 就是一个管理字符串的类。

上一篇说的查文档: string - C++ Reference(string类的文档介绍)

① 字符串是表示字符序列的类。

② 标准的字符串提供了对此类对象的支持,其接口类似于标准字符容器的接口,

     但添加了专门用于操作单字节字符串的设计特性。

③ string 类是使用 char,即作为它的字符类型,使用它的默认 char_traits 和分配器类型。

   (关于模板的更多信息,可以参阅 basic_string)

④ string类是 basic_sting 模板类的一个实例,它使用 char 来实例化 basic_string 模板类, 

     并用 char_traits 和 allocator 作为 basic_string 的默认参数。

    (关于更多的模板信息请参考 basic_sting )

⑤ 注意,这个类独立于所使用的编码来处理字节。

     如果用来处理多字节或变长字符(如UTF-8)的系列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:

① string 是表示字符串的字符串类。

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

③ string在底层上实际是:basic_string 模板类的别名:

typedef basic_string<char, char_traits, allocator>string;

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

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

1.2 basic_string 模板类

在正式开始讲解 string 之前,还要介绍一下刚才提到的 basic_string 模板类:

typedef basic_string<char, char_traits, allocator>string;

从文档中可以看出,string 的原生类并不是直接定义了一个 string 类,而是定义出了一个类模板。

而 string 是用 typedef 出来的,它其实是 basic_string<char> 

我们先用 C 格式字符串构造一个 string 类对象:

#include <iostream>
#include <string>
using namespace std;

int main()
{
	string str("hello world");

	return 0;
}

既然我们知道了它是  basic_string<char>  ,我们来猜想一下它在库里面是如何定义的:

template<class T>
class basic_string 
{
private:
	T* _str;
	// ...
};

这里为什么需要模板?管理字符串不就是一个 char 吗?搞一个模板出来干什么?

字符串还有其它类型?这和编码有关系,这里稍微补充一下关于编码的知识。

1.3 编码表的由来

我们知道,计算机是约翰·冯·诺依曼发明的,早期在计算机上是只需要显示英文的。

英文的显示非常简单,英文字母一组合就是英文单词了,

大写字母 + 小写字母,再加上一些标点符号,顶多也就一百多个字符,

所以出现了一套 ASCII 码。比如说你写了一个 'a' ,想在计算机中存储,因为计算机只有二进制,

虽然 0 和 1 只能表示两种状态,但是多个 0 和 1 一组合就可以表示出很多状态了。

为了能够记录这些字符,于是就建立了一个映射。值映射符号,于是就产生了编码表:

 ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)

是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。"

虽然英文是世界通用语言,但也不是所有人都懂英文啊。

为了能让计算机更好地普及,这时候就有人搞出了 Unicode——表示全世界文字的编码表。

"统一码(Unicode),也叫万国码、单一码,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。"

utf-8,常见的汉字都用2个字节去编,一些生僻的可以用3个或者4个……

(我们常说的 utf-8   utf-16   utf-32 )

你可以理解为,中文有了一套自己的规则,去找到值对应的汉字。

 存的时候存的是 utf-8 对应的值,你要用的时候它就会拿这个表去查。

要对应的上,如果对应不上就会出现我们熟知的乱码。

所以建议在Linux或服务器下统一把编码设置为 utf-8,不然出现乱码会很恶心。

我们已经知道,Unicode 是世界通用编码,而我们中国也有一套自己的编码方式。

GBK —— 中文自己量身定做的编码表。

"GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification)"

如果你打开一个文本,Windows 下一般默认的编码是GBK,Linux 下默认的就是 utf-8 。

GBK包括所有的汉字,包括简体和繁体。而这里显示的 GB2312 则只包括简体汉字。

(读音相同的字是编到一起的,打游戏时的C语言被屏蔽就能被体现)

回到刚才的问题 —— string 需要模板的原因:

刚才介绍了编码,现在大家应该能理解为什么 string 需要模板了。

string 不仅要存 char,还有 wchar_t (2个字节)char 16_t,char32 _t 等等,

这就是 string 需要 basic_sting 的原因。

1.4 其它字符编码的string

宽字节是一种扩展的存储方式,unicode 编码的字符一般以 wchar_t 类型存储。

wchar_t 是两个字节,是为了更好地表示字符。

如果涉及 Windows 编程,Windows 下的很多接口都是用的 Unicode,

这时它的字符串不是 char*,而是 wchar_t* ,这时候就涉及到转码,

如果想要存储 wchar_t* ,就最好用 wstring —— 专门处理宽字符的。

概念:wstring 就是每个字符都是一个 wchar_t 的:

不仅仅有 string 和 wstring!

还有 u16string(存16个比特位)、还有u32string(存32个比特位)

 总结:

本章主要学习 string,现阶段基本用的都是 string (里面存 char)

如果碰到有些地方是 wchar_t 就要使用与之对应的 wstring 了,

其他也一样,比如有些地方字符串编码是 utf-32,这时候你就可能要用 u32string 去存储了。

因为有的库或API只支持UTF-16编码的字符,

而且有的API使用UTF-16编码的字符时执行速度会快一些。

2. string类对象的常见构造

我们还是以查看文档的方式去学习:

#include <iostream>
#include <string>
using namespace std;

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

	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;

	string s4(s2, 6, 3);//从第6个位置开始向后面拷贝3个字母
	cout << s4 << endl;

	string s5(s2, 6, 15);// 第三个参数len大于后面字符长度,有多少拷贝多少拷贝到结尾
	cout << s5 << endl;
	string s6(s2, 6);// 第三个参数默认给npos,是整形的最大值,相当于取所有字符串
	cout << s6 << endl;

	string s7(10, 'x');// 用10个x构造s7
	cout << s7 << endl;
}
int main()
{
	test_string1();

	return 0;
}

3. sting类对象的容量操作

注意事项:

①  size() 和 length() 的计算不包含 。

     解释:它不包含最后作为结尾标识符的 ,告诉你的是有效的字符长度。

②  size() 和 length() 的功能都是返回字符串有效长度,功能上没有区别

     解释: 这是一个 "历史包袱" 问题。

因为 string 比 STL 出现的还要早一些,有了STL容器后,为了和树等对应上所以给出了 size() 。

而 length() 是代替传统的C字符串,所以针对C中的 strlen ,给出相应的函数 length() 。

C++中 string 成员函数 length() 等同于 size() ,功能没有任何区别。

不信可以看 C++标准库中的 string 中 size() 和 length() 的源代码:

size_type   __CLR_OR_THIS_CALL   length()   const
{   //   return   length   of   sequence
    return   (_Mysize);
}
 
size_type   __CLR_OR_THIS_CALL   size()   const
{   //   return   length   of   sequence
    return   (_Mysize);
} 

③ clear 只是把数据清了,但是容量还在。

④ reserve 开空间,影响容量。

    而 resize 是开空间,并对这些空间给一个初始值,进行初始化。(不指定默认用  初始化)

void test_string2()
{
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << s1.capacity() << endl;
	cout << s1.empty() << endl;

	cout << s1 << endl;
	s1.clear();
	cout << s1 << endl;;

	string s2;
	cout << s2.capacity() << endl;// 不同编译器的原始值不同,扩容也不同
	s2.reserve(1000);// 实际考虑内存对齐什么的,会多预留一点
	cout << s2.capacity() << endl;

	string s3("hello world");
	cout << s3.size() << endl;
	s3.resize(50, 'x');
	cout << s3 << endl;
}

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

reserve 为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserver不会改变容量大小,大于才会。

4. string类对象的访问及遍历操作

 这里 operator[] 是使用引用返回,是为了能够支持修改返回的变量。

我们就可以像数组一样操作string了。

迭代器是 STL 六大组件之一,是用来访问和修改容器的。

如果你是第一次接触 "迭代器的概念",不妨可以先把迭代器想象成 "像指针一样的类型"。

对于 string,无论是正着遍历,倒着遍历,下标 + [] 都足够好用,为什么还要迭代器呢?

当然,对于 string,下标和 [] 确实足够好用,我们在学习C语言的时候就先入为主地使用了,

确实可以不用迭代器。但是如果是其他容器(数据结构)呢?

比如 list、map / set  不支持 下标 + [] 遍历,迭代器就排上用场了,

这就是迭代器存在的意义。迭代器是通用的遍历方式。

对于 string,你得会用迭代器,但是一般我们还是喜欢用 下标 + [] 遍历。

迭代器有很多,此外还有反向迭代器、const 迭代器……

这些都可以通过看文档去了解和学习。对于迭代器后面还会详细讲解,

范围 for。

这个我们在讲 auto 关键字的时候讲过了,它是一个用起来是很甜的语法糖。

这个范围 for 虽然看起来和听上去都很强,又是自动迭代又是自动判断结束的,

但其实它底层也就是编译器在编译后把这段代码替换成了迭代器而已。

void test_string3()
{
	string s1("hello world");
	s1[0] = 'x';
	//s1[20];  内部会检查越界
	cout << s1 << endl;

	for (size_t i = 0; i < s1.size(); ++i)// 普通遍历string,每个字符+1
	{
		s1[i]++;
	}
	cout << s1 << endl;

	string::iterator it = s1.begin();// 迭代器遍历string,每个字符+1
	while (it != s1.end())
	{
		(*it)++;
		it++;
	}
	cout << s1 << endl;

	for (auto& e : s1)// 范围for遍历string,每个字符-1
	{
		e--;
	}
	cout << s1 << endl;

	string::reverse_iterator rit = s1.rbegin();// 迭代器逆置遍历string,每个字符-1
	while (rit != s1.rend())
	{
		cout << *rit;
		(*rit)--;
		++rit;// 注意这里也是++
	}
	cout << endl;
	
	*(s1.begin()) = 'h';// 把第一个字母改成h -> s1[0] = 'h';
	cout << s1 << endl;
}

5. string类对象的修改操作

 这个 c_str 有什么意义呢?

比如这里需要打开文件,fopen 第一个参数要求是 const char*,

所以这里怎么能直接放 string 是不行的,这时候可以用 .c_str()  就可以把字符串的地址返回出来。

简单接口演示:

void test_string4()
{
	string s("hello");
	s.push_back('-');
	s.push_back('-');
	s.append("world");
	cout << s << endl;

	string str("abcdefg");
	s += '@';
	s += str;
	s += "!!!";
	cout << s << endl;

	s.append(++str.begin(), --str.end());
	cout << s << endl;

	string copy(s.begin() + 3, s.end() - 3);
	cout << copy << endl;

	int ival = 2023;
	double dval = 3.14;
	cout << to_string(ival) << endl;
	cout << to_string(dval) << endl;

	string istr = "9999";
	string dstr = "9999.99";
	cout << stoi(istr) << endl;
	cout << stod(dstr) << endl;
}

 上面 += 是最好用且最常用的,看看 rfind 和 find 的使用场景:

void DealUrl(const string& url)
{
	// 取出协议
	size_t pos1 = url.find("://");
	if (pos1 == string::npos)
	{
		cout << "非法url" << endl;
		return;
	}
	string protocol = url.substr(0, pos1);
	cout << protocol << endl;

	// 取出域名
	size_t pos2 = url.find('/', pos1 + 3);// 冒号位置+3开始往后找
	if (pos2 == string::npos)
	{
		cout << "非法url" << endl;
		return;
	}
	string domain = url.substr(pos1 + 3, pos2 - pos1 - 3);
	cout << domain << endl;

	// 取出路径
	string uri = url.substr(pos2 + 1);
	cout << uri << endl << endl;
}

void test_string5()
{
	string filename("test.cpp.tar.zip");// 取后缀
	size_t pos = filename.rfind('.');// 反向找
	if (pos != string::npos)
	{
		//string suff = filename.substr(pos, filename.size() - pos);
		string suff = filename.substr(pos);//不用像上一行算长度,直接让默认值取到最后

		cout << suff << endl;
	}

	string url1 = "https://cplusplus.com/reference/string/string/";// 对一个网址进行操作,可以多放几个试试
	DealUrl(url1);
}

6. string类非成员函数

 

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

7. string的相关笔试题

1. 关于代码输出正确的结果是( )(vs2013 环境下编译运行)

int main()
{
	string a = "hello world";
	string b = a;
	if (a.c_str() == b.c_str())
	{
		cout << "true" << endl;
	}
	else cout << "false" << endl;
	string c = b;
	c = "";
	if (a.c_str() == b.c_str())
	{
		cout << "true" << endl;
	}
	else cout << "false" << endl;
	a = "";
	if (a.c_str() == b.c_str())
	{
		cout << "true" << endl;
	}
	else cout << "false" << endl;
	return 0;
}

A.false false false

B.true false false

C.true true true

D.true true false

2. 下面程序的输出结果正确的是( )

int main()
{
	string str("hello world");
	str.reserve(111);
	str.resize(5);
	str.reserve(50);
	cout << str.size() << " " << str.capacity() << endl;
	return 0;
}

A.10 50

B.5 50

C.5 111

D.10 111

3. 下面程序的输出结果正确的是( )

int main()
{
	string strText = "How are you?";
	string strSeparator = " ";
	string strResult;
	int size_pos = 0;
	int size_prev_pos = 0;
	while ((size_pos = strText.find_first_of(strSeparator, size_pos)) != string::npos)
	{
		strResult = strText.substr(size_prev_pos, size_pos - size_prev_pos);
		cout << strResult << " ";
		size_prev_pos = ++size_pos;
	}
	if (size_prev_pos != strText.size())
	{
		strResult = strText.substr(size_prev_pos, size_pos - size_prev_pos);
		cout << strResult << " ";
	}
	cout << endl;
	return 0;
}

A.Howareyou?

B.How Are You?

C.How are

D.How are you?

答案:

1. A

分析:a 和 b的值虽然相同,但是a.c_str()==b.c_str()比较的是存储字符串位置的地址,a和b是两个不同的对象,内部数据存储的位置也不相同,因此不相等,后面c="",a=""与b对象都没有任何的影响,所以都不相等

2. C

分析:

str.reserve(111); //调整容量为 111

str.resize(5);   //调整元素个数为 5

str.reserve(50);  //调整容量为 50,由于调整的容量小于已有空间容量,故容量不会减小

所以size=5 capacity=111

3. D

分析:程序的目的是以字符串strSeparator = " "作为分隔符,对字符串string strText = "How are you?";进行分割,每分割出一个单词就进行一次打印

8. string相关OJ题

现在关于OJ题建议刷《剑指offer》和力扣上string的题,

用牛客一道题演示下上面提到的geline函数:看看IO型和接口型C++的区别

字符串最后一个单词的长度_牛客题霸_牛客网 (nowcoder.com)

简单  通过率:35.00%  时间限制:1秒  空间限制:32M

描述

计算字符串最后一个单词的长度,单词以空格隔开,字符串长度小于5000。(注:字符串末尾不以空格为结尾)

输入描述:

输入一行,代表要计算的字符串,非空,长度小于5000。

输出描述:

输出一个整数,表示输入字符串最后一个单词的长度。

示例1

输入:

hello nowcoder

输出:

8

说明:

最后一个单词为nowcoder,长度为8  

#include <iostream>
using namespace std;

int main() {
    int a, b;
    while (cin >> a >> b) { // 注意 while 处理多个 case
        cout << a + b << endl;
    }
}
// 64 位输出请用 printf("%lld")

代码解析:

这题直接用 cin 就不行,因为有空格,所以我们用 getline 输入一行字符,查文档:

虽然不包string头文件有些编译器也能过,但还是包上好:

#include <iostream>
#include <string>
using namespace std;

int main()
{
    string str;
    getline(cin,str);
    size_t pos = str.rfind(' ');
    if(pos != str.npos)
    {
        cout << str.size() - pos - 1 << endl;// -1是减去空格
    }
    else //找不到直接输出size
    {
        cout << str.size();
    }
    return 0;
}

看看力扣是这么写的:(力扣就检查用例较多,且此题多加了最后有空格的情况)

58. 最后一个单词的长度 - 力扣(LeetCode)

难度简单

给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

示例 1:

输入:s = "Hello World"
输出:5
解释:最后一个单词是“World”,长度为5。

示例 2:

输入:s = "   fly me   to   the moon  "
输出:4
解释:最后一个单词是“moon”,长度为4。

示例 3:

输入:s = "luffy is still joyboy"
输出:6
解释:最后一个单词是长度为6的“joyboy”。

提示:

  • 1 <= s.length <= 10^4

  • s 仅有英文字母和空格 ' ' 组成

  • s 中至少存在一个单词

代码解析:

只是比牛客的那题多了一个条件,所以直接放代码:

class Solution {
public:
	int lengthOfLastWord(string s) {
        while(s[s.size()-1] == ' ')
        {
            s.erase(s.size()-1,1);
        }
		size_t pos = s.rfind(' ');
		if (pos != s.npos)
		{
			return s.size() - pos - 1;// -1是减去空格
		}
		else //找不到直接输出size
		{
			return s.size();
		}
	}
};

力扣415. 字符串相加

415. 字符串相加 - 力扣(LeetCode)

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

示例 1:

输入:num1 = "11", num2 = "123"
输出:"134"

示例 2:

输入:num1 = "456", num2 = "77"
输出:"533"

示例 3:

输入:num1 = "0", num2 = "0"
输出:"0"

提示:

  • 1 <= num1.length, num2.length <= 10^4

  • num1 和num2 都只包含数字 0-9

  • num1 和num2 都不包含任何前导零

class Solution {
public:
    string addStrings(string num1, string num2) {

    }
};

代码解析:

这题首先想到从字符串末尾开始加,加后考虑进位后头插到一个新的字符串。需要用到头插接口:

查文档发现string没有头插,只有任意位置插入删除,凑合用用:法一:头插

class Solution {
public:
	string addStrings(string num1, string num2) {
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		int next = 0;// 表示进位,这里只会是0或1
		string ret;// 把加后的字符弄到这
		while (end1 >= 0 || end2 >= 0)// 只要有一个没加完就继续,加完的看做数字0
		{
			int val1 = end1 >= 0 ? num1[end1] - '0' : 0;// -'0'转化为数字
			int val2 = end2 >= 0 ? num2[end2] - '0' : 0;
			int add = val1 + val2 + next;
			next = add > 9 ? 1 : 0;// 处理进位
			//写到这想到头插:查文档:
			ret.insert(0, 1, (add % 10) + '0');// 在ret第0个位置插入1个字符
			--end1;
			--end2;
		}
		if (next)// 提交后看测试用例有进位没处理到的处理一下
		{
			ret.insert(0, 1, '1');
		}
		return ret;
	}
};

提交通过后发现效率很低(虽然力扣效率不太准确,但是你网比较好且提交几次还是很慢就应该真是代码效率低了)哪里会低?string是连续的空间,所以任意位置插入需要挪动数据,效率就低了(O(N^2))。那能不能用效率高的尾插?而且 += 很方便。答案是可以的,头插和尾插最后的差别就是字符串反过来了,那我们以前写过逆置字符串就能派上用场,更方便的是C++已经提供了逆置字符串的函数,(string没提供,头文件algorithm提供了)在这个旧版网站搜一下:cplusplus.com - The C++ Resources Network

 或者在msdn应用搜一下:(能搜到就行)

 和我们以前写的差不多,左闭右开的区间,直接用,(法二,尾插+逆置):

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        int next = 0;// 表示进位,这里只会是0或1
        string ret;
        while(end1 >= 0 || end2 >= 0)// 只要有一个没加完就继续,加完的看做数字0
        {
            int val1 = end1 >= 0 ? num1[end1] - '0' : 0;// -'0'转化为数字
            int val2 = end2 >= 0 ? num2[end2] - '0' : 0;
            int add = val1 + val2 + next;
            next = add > 9 ? 1 : 0;// 处理进位

            //ret.insert(0,1,(add % 10) + '0');// 在ret第0个位置插入1个字符
            ret += (add % 10) + '0';// 尾插
            --end1;
            --end2;
        }
        if(next)// 提交后看测试用例有进位没处理到的处理一下
        {
            //ret.insert(0,1,'1');// 头插
            ret += '1';// 尾插
        }

        reverse(ret.begin(),ret.end());
        return ret;
    }
};

本章完。

学了这么多接口函数,又刷了两道OJ,后面也差不多就会刷了,

后面更一篇string相关的OJ题,再后一篇就模拟实现string,

后面的STL学习基本都是按照这个顺序走,只是内容少了点,因为接口函数都是类似的。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。