机械转码日记【18】C++的string类

发布于:2022-12-02 ⋅ 阅读:(470) ⋅ 点赞:(0)

目录

前言

1.为什么学习string类

1.1string其实不是类,而是类模板

1.2编码

2.string的使用

2.1string类的构造函数

2.2string的遍历 

2.2.1利用size()这个接口函数

2.2.2迭代器

2.2.3范围for

3.迭代器

3.1正向迭代器

3.2反向迭代器

3.3只读的正向迭代器

3.4只读的反向迭代器

4.string类中一些常用的接口函数

4.1 at()和[]的区别

4.2 size()和length()

4.3 capacity()

 4.4 push_back()和append()

4.5 insert()

4.6 erase()

4.7 reserve()

4.8 resize()

4.9 swap()

4.10 find()和rfind()

5.string的oj题

5.1仅仅反转字母

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

 5.3字符串中的第一个唯一字符

 5.4字符串相加


前言

前段时间因为种种原因又拖更了很久,今天这篇博客我们就来了解一下string类吧。

1.为什么学习string类

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数, 但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可 能还会越界访问。因此,C++发明了string类,在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。

1.1string其实不是类,而是类模板

下图是cplusplus网站中关于string类的定义,可以看到string是被typedef过的,它的原来的面目是basic_string<char>这个模板。

我们一起来看看string这个模板, 可以看到出了basic_string,还有别的string模板,比如u16string,u32string......那么为什么会出现这些东西呢?这就涉及到一个知识——编码。

1.2编码

ASCII码:要知道早期计算机是美国人发明的,所以一开始计算机只需要表示英文就行了,那么计算机的二进制代码只有0和1,是如何表示英文的呢,所以就产生了ASCII码表这个东西,它表示出了二进制代码和英文字母,字符之间的映射:

而渐渐的随着计算机的发展,单纯的只显示英文已经满足不了大家的需求了,随着全球化,计算机上要显示中文,俄文,阿拉伯文...;因此就出现了很多新的编码表;比如Unicode的utf8,utf16,utf32;gbk;gbk是国人针对我们中文发明的编码表,Unicode是针对全世界的文字发明的编码表。

可以看到上图我们储存的中文字符的编码确实是和ASCII不一样(ASCII没有负值),一般来讲,我们windows端的vs编译器采用的是gbk编码,一般常见汉字为2个字节,罕见的汉字文3个字节;而在Linux下的gcc和g++编译器默认采用的是Unicode编码。给大家看一个有意思的东西吧:

可以看到,相邻的gbk编码其实和我们中文的音是相关的,相邻的编码读音差不多相近或者相同。实际上,我们打游戏时,经常会出现敏感词汇被转化为“******”,这里就用到了编码,我们将那些铭感词汇的gbk编码弄成一个库,如果出现了,就转化为***。

就是因为有这么多的编码,我们才需要将string变成一个类模板,在接下来的学习中我们主要学习string这个类模板。

2.string的使用

首先要包含头文件#include<string>;注意不能是#include<string.h>;因为string.h在C语言中也有这个头文件,如果我们写string.h就是包含C语言的这个头文件了,而不是使用C++的string类。同时我们也要使用std命名空间,因为C++所有标准库都是包含在std空间里的。

2.1string类的构造函数

我们在Cplusplus网站下可以看到string类的构造函数,一共有7个,但这七个我们并不是每个都要重点去学,我们主要学我所标记的红框里面的,别的函数我们要使用的时候可以直接查一下文档。

函数名称
功能说明
string()
构造空的 string 类对象,即空字符串
string(const char* s)
C-string 来构造 string 类对象
string(const string&s)
拷贝构造函数
//string可以看作是一个管理字符动态增长的数组,同时为了兼容C语言,这个数组以\0结尾
int main()
{
	string s1; // 构造空的string类对象s1
	string s2("hello bit"); // 用C格式字符串构造string类对象s2
	s2 += "!!!!!";//这个字符数组可以动态增长
	string s3(s2); // 拷贝构造s3
    string s3 = s2;//拷贝构造s3
}

2.2string的遍历 

在C语言中,如果我们要实现一个字符串的遍历,是采用下面的方式的:

那么在C++中,我们如何去遍历一个字符串呢?一共有下面三种方法: 

2.2.1利用size()这个接口函数

size()是string类的一个接口函数,它返回的是字符串的长度,相当于我们在C语言中通过strlen()这个函数求出的长度。

//size()这个接口函数和下标+[]遍历
int main()
{
	string s1 = "hello world!";
	cout << s1.size() << endl;
	for (size_t i = 0; i < s1.size(); i++)
	{
		//s1.operator[](i)
		cout << s1[i] ;
	}
}

2.2.2迭代器

迭代器是我们接触到的一个新知识,它是一个和指针差不多的东西(或者可以说它就是指针),以我们的string类为例,它的使用方法是:

  1. 先定义迭代器的起始位置:string::iterator it = s1.begin();这里定义了名为it的迭代器,迭代器的起始位置为s1.begin(),是s1对象的第一个位置。
  2. 迭代器开始迭代,s1.end()是迭代器的结束位置,它指向最后一个数据的下一个位置,当迭代器的位置不为s1.end()的时候,迭代器自加,直到迭代到s1.end(),才结束迭代。
//迭代器
int main()
{
	string s1 = "hello world!";
	string::iterator it = s1.begin();
	//begin是第一个数据
	//end是字符串最后一个数据的下一个位置
	//左闭右开
	while (it != s1.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;
}

2.2.3范围for

范围for的语法我曾经在机械转码日记【11】提到过,在这里我们再温习一下吧,for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。auto是用来自动推导数据类型的。范围for它的底层原理其实也是迭代器。

//范围for,原理就是迭代器
int main()
{
	string s1 = "hello world!";
	for (auto ch : s1)
	{
		cout << ch;
	}
}

3.迭代器

迭代器有四种,正向迭代器,反向迭代器,只读的正向迭代器,只读的反向迭代器:

3.1正向迭代器

正向迭代器就是我们刚刚遍历string时使用的,它的开始位置是begin();结束位置是end()。

int main()
{
	string s1 = "hello world!";
	string::iterator it = s1.begin();
	//begin是第一个数据
	//end是字符串最后一个数据的下一个位置
	//左闭右开
	while (it != s1.end())
	{
		cout << *it<<" ";
		++it;
	}
	cout << endl;
}

3.2反向迭代器

反向迭代器,顾名思义,就是反向来迭代,它的开始位置为rbegin();结束位置为rend();

int main()
{
	string s1 = "hello world!";
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout <<*it<<" ";
		++rit;
	}
}

3.3只读的正向迭代器

只读的正向迭代器,我们可以把他看作一个普通的正向迭代器,但是他是只读的,不可写。比如下面的这种情况:

//注意看c++11做出的cbegin
//普通的正向迭代器,但是只读不可写
void Func(const string& rs)
{
	string::const_iterator it = rs.begin();
	//C++11中可以写成:
	//string::const_iterator it = rs.cbegin();
	//嫌麻烦可以直接用auto:
	//auto it = rs.begin();
	while (it != rs.end())
	{
		//*it += 1;   //err
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

int main()
{
	const string s1 = "hello world!";
	Func(s1);
	return 0;
}

 这个时候有几点需要注意:

  1. 在C++11中,对string类增加了两个新的接口,一个是cbegin(),一个是cend(),这两个接口用来表示只读的正向迭代器的开始位置和结束位置。
  2. 如果你觉得写起来特别麻烦,你可以使用auto语句让编译器自动判断你写的是什么迭代器,但是这种方式可能会使程序的可读性变差。
  3. 注意只能读,不能写,否则会报错:

3.4只读的反向迭代器

只读的反向迭代器,和上面类似,我们可以把他看作一个普通的反向迭代器,但是他是只读的,不可写。使用代码如下:

//普通的反向迭代器,但是只读不可写
void Func2(const string& rs)
{
	string::const_reverse_iterator rit = rs.rbegin();
	//C++11中可以写成:
	//string::const_iterator it = rs.crbegin();
	//auto rit = rs.rbegin();//嫌烦可以直接auto
	while (rit != rs.rend())
	{
		//(*rit) -= 1;  //err
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

和只读的正向迭代器类似,在C++11里,我们也可以用crbegin()来表示只读的反向迭代器的开始位置,用crend()来表示只读的反向迭代器的结束位置。

4.string类中一些常用的接口函数

在string类中有很多接口函数,但我们只需要记住一些比较常用的,剩下的用到再去查文档:

4.1 at()和[]的区别

[]和at()都支持对string类的读和写:

它们的不同之处就在于对越界的检查,对于[ ]来说,如果访问越界,会直接报断言错误,但at()对于越界的检验就会抛异常:

 

在现实生活中还是[ ]的使用场景多一些,因为它对越界的处理更加合适。

4.2 size()和length()

size()和length()的功能其实都是一样的,都是返回字符串的长度,或许你会问为什么会出现这两种功能一样但是不同名的函数呢?这就涉及到STL和string类的历史问题了,一开始string类是出现的比STL早的,所以刚开始采用的是length()这个接口区返回字符串长度,但随着STL的出现,为了和STL中其他的类采用同名的接口,就把size()的功能写成和length()一样了,简而言之,为了STL的使用方便性,所以才增加了size()接口。你这个时候可能又会问了,为什么不把length()接口删了,因为程序语言不是说删功能就删的,可能在size()出现之前,就有公司的代码使用了length(),删了你不可能让程序员全都改一遍吧!

4.3 capacity()

capacity(),中文翻译是容量,联想一下我们当初学习数据结构的栈的时候,当时是可以实现一个动态增长的过程,当时会对capacity和size进行比较,当capacity<=size的时候,就需要扩容了。同样的,在string类里面,也存在capacity()这样一个接口函数,它的功能是返回当前string类的容量,这个容量的大小总是大于size()的。

 4.4 push_back()和append()

push_back(),顾名思义,它的功能是尾插,即在字符串的尾部插入几个数据,如下面这样:

append(),这个函数也是尾插,但是和push_back()不一样,push_back()只能尾插一个字符,append()可以尾插一个字符串:

你觉得这样好用吗?其实我觉得有点麻烦,在string类中,其实对+=这个运算符进行了重载,我们可以直接使用+=这个运算符对字符串进行尾插操作,并且它的可读性和使用简单性比push_back()要高得多:

4.5 insert()

insert()顾名思义,就是插入,它可以在任意位置进行插入删除,同样的,他也有很多种形式,但我们只需要会用其中几种就行,其他的查手册就行了。

比如我们要在string开头的位置插入一个字符,这个时候应该这么做呢,这个时候就可以使用string& insert (size_t pos, size_t n, char c)这个函数:

当然也可以用iterator insert (iterator p, char c)这个函数,它是使用迭代器进行插入的,比如要实现头插:

4.6 erase()

前面讲了插入,那删除怎么删呢?那就要用到erase()这个函数了:

 他的具体用法就是:

4.7 reserve()

观察一下下面的这段代码: 

可以看到s的容量是频繁开辟的,这样势必会给系统带来消耗,影响程序运行速度,这个时候就可以使用reserve()了,reserve的作用是提前开辟好一段固定大小的内存,比如我先开1000个容量,就不用一直频繁开辟内存了:

4.8 resize()

resize()的作用其实是和reserve()差不多的,但是resize()在提前开辟内存时,还可以初始化这段内存,同时还可以改变size的大小比如下面这样:

4.9 swap()

说到string类里的swap函数,不得不要提到我们前面提到的全局的swap()函数,那么他们之间有什么区别呢?

说到string类里面的swap,它其实就是交换了两个string对象的指针,让s1的指针指向了s2,s2的指针指向了s1;但全局的swap()不一样,他是先拷贝构造了一个s1对象,然后把s2对象的值赋给了s1,把拷贝的那个对象的值赋给了s2,这是属于一种深拷贝,所以效率要比类里面的swap要低。

4.10 find()和rfind()

find()的功能就是在字符串中找到字符或者字符串的位置,如果找得到就会返回它的下标,如果找不到就会返回npos,也就是-1; 

 比如我们要实现取出文件的后缀的功能,那么以下代码就可以实现:

那么如果我们的文件名是string.cpp.c.zip这种有多个.的文件该怎么办呢?用上述的代码还能实现吗? 是不行的,这个时候我们要使用rfind()这个函数,就是反着找:

为了更加加深我们对find()的理解,我po出寻找网址的协议,域名和uri的一段代码:

//取出网址的域名
int main()
{
	string url = "https://blog.csdn.net/qq_52378490?type=blog";
	//协议  域名    uri
	//协议
	string protocol;
	size_t pos1 = url.find("://");
	if (pos1 != string::npos)
	{
		protocol = url.substr(0, pos1);
		cout << "protocol:" << protocol << endl;
	}
	else
	{
		cout << "非法url" << endl;
	}
	string domain;
	size_t pos2 = url.find('/', pos1 + 3);
	if (pos2 != string::npos)
	{
		domain = url.substr(pos1+3, pos2-(pos1+3));
		cout << "domain:" << domain << endl;
	}
	else
	{
		cout << "非法url" << endl;
	}
	string uri = url.substr(pos2 + 1);
	cout<< "uri:" << uri << endl;
}

5.string的oj题

5.1仅仅反转字母

仅仅反转字母

看到这种反转左右两端的数据,我们就知道要使用双指针法了,就是左边一个指针,右边一个指针,左指针++,右指针--,找到字母就交换,那么原理我们懂了,只需要转化为代码就行了:

class Solution {
public:
    bool isLetter(char ch)
    {
        if(ch>='a'&&ch<='z')
        {
            return true;
        }
        else if(ch>='A'&&ch<='Z')
        {
            return true;
        }
        else
        return false;
    }
    string reverseOnlyLetters(string s) {
        int left = 0;
        int right = s.size()-1;
        while(left<right)
        {
            //cout<<left<<":"<<right;
            while(left<right&&!isLetter(s[left]))//left<right防止越界,不是字母就++
            {
                ++left;
            }
            while(left<right&&!isLetter(s[right]))//left<right防止越界,不是字母就--
            {
                --right;
            }
            swap(s[left],s[right]);
            ++left;
            --right;//交换后继续++和--进行下一个判断,如果不++和--就会死循环。
        }
        return s;
    }
};

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

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

这道题的思路我拿到手就会啊,这不就是使用rfind()找到空格,也就是最后一个单词开始的位置,然后输出从空格开始到字符串结束的长度就行了呗,上代码!

#include <iostream>
using namespace std;

int main() {
    string str;
    cin>>str;
    size_t pos = str.rfind(' ');
    if(pos != string::npos)
    {
        cout<<str.size()-1-pos;
    }
    else
    {
        cout<<str.size();
    }
}

运行一下,欸,md怎么报错了呢?

我怎么也想不通啊,让我们拷贝代码到VS里调试一下:

通过调试我们发现原来我们输入的值没有被全部写入str里面,这是因为我们使用cin时,是自动以空格分隔的,所以只有hello被写入到str里面,而剩下的值都在缓冲区里面,(我在机械转码日记【2】——关于scanf的注意事项里面曾经提到过缓冲区的问题,感兴趣可以回去看看),这个时候 我们就要用到getline()这个函数了,它的作用就是让输入流里面的东西全部写入到字符串里(碰到换行‘\n’就结束):

我们改正之后: 

#include <iostream>
using namespace std;

int main() {
    string str;
    getline(cin,str);//把缓冲区的内容全部写入str
    size_t pos = str.rfind(' ');
    if(pos != string::npos)
    {
        cout<<str.size()-1-pos;
    }
    else
    {
        cout<<str.size();
    }
}

 5.3字符串中的第一个唯一字符

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

看到这道题我第一个想到的方法是哈希的映射方法,也是就是先统计每个字母出现的次数,然后找到那个只出现一次的字母:

class Solution {
public:
    int firstUniqChar(string s) {
        int count[26]={0};//创建一个大小为26个的数组,统计26个英文字母出现的次数
        for(auto e:s)
        {
            count[e - 'a']++;//统计次数
        }
        for(size_t i = 0;i<s.size();i++)
        {
            if(count[s[i]-'a']==1)//s[i]-'a'当前字母在count数组中的下标,count在这个下标的值为1,说明只出现了一次
            {
                return i;
            }
        }
        return -1;//如果走到这,说明前面什么也没返回,说明不存在只出现过一次的字母,返回-1
    }
};

当然我们也可以取巧,可以从两头找,也就是一个从头找,一个从尾找,如果尾和头找到的下标相同,那就是只出现过一次。

class Solution {
public:
    int firstUniqChar(string s) {
        for(auto e:s)
        {
            size_t pos1 = s.find(e);
		    size_t pos2 = s.rfind(e);
            if ((pos1!= string::npos)&&pos1 == pos2)
            return pos1;
        }
	return -1;
    }
};

 5.4字符串相加

字符串相加

 这一题可能你看到会没有说明思路,但是我们可以联想到我们列竖式的计算方法:

 竖式是从低位到高位运算,需要考虑三个量,两个加数和进位数,那么程序就有了:

 string addStrings(string num1, string num2) {
        int end1 = num1.size()-1;//加数1的最低位
        int end2 = num2.size()-1;//加数2的最低为
        int carry = 0;//进位数
        string retStr;//计算结果
        while(end1 >=0||end2>=0)//只要位数没有越界,就继续加
        {
            int val1 = end1>=0?num1[end1]-'0':0;//需要判断有没有越界,越界加数为0
            int val2 = end2>=0?num2[end2]-'0':0;//需要判断有没有越界,越界加数为0
            int ret = val1+val2+carry;//加数1+加数2+进位数=计算结果
            if(ret>9)//大于9说明要进位
            {
                ret-=10;
                carry = 1;//进位的话进位数为1
            }
            else
            {
                carry = 0;//不进位,进位数为0
            }
            retStr.insert(retStr.begin(),'0'+ret);//把当前位计算结果头插到上一步计算的那一位的前面
            --end1;//计算下一位
            --end2;//计算下一位
        }
}

程序编完,我们运行一下,发现有个算例没有过,当1+9的时候,由于--end使得进位数没有被加上,所以我们要在返回值的最后判断carry进位数是否为1,如果为1要加上去。

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size()-1;
        int end2 = num2.size()-1;
        int carry = 0;
        string retStr;
        while(end1 >=0||end2>=0)
        {
            int val1 = end1>=0?num1[end1]-'0':0;//需要判断有没有越界,越界为0
            int val2 = end2>=0?num2[end2]-'0':0;//需要判断有没有越界,越界为0
            int ret = val1+val2+carry;
            if(ret>9)
            {
                ret-=10;
                carry = 1;
            }
            else
            {
                carry = 0;
            }
            retStr.insert(retStr.begin(),'0'+ret);
            --end1;
            --end2;
        }
        if(carry == 1)
        {
            retStr.insert(retStr.begin(),'1');
        }
        return retStr;
    }
};

全部算例都通过了,但是为什么执行用时这么差呢?是因为insert头插的效率太低了,所以我们可以改进一下,我们可以每次尾插每一位运算的结果,最后把结果逆置一下就行: 

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size()-1;
        int end2 = num2.size()-1;
        int carry = 0;
        string retStr;
        while(end1 >=0||end2>=0)
        {
            int val1 = end1>=0?num1[end1]-'0':0;//需要判断有没有越界,越界为0
            int val2 = end2>=0?num2[end2]-'0':0;//需要判断有没有越界,越界为0
            int ret = val1+val2+carry;
            if(ret>9)
            {
                ret-=10;
                carry = 1;
            }
            else
            {
                carry = 0;
            }
            retStr += ('0'+ret);
            --end1;
            --end2;
        }
        if(carry == 1)
        {
            retStr += '1';
        }
        reverse(retStr.begin(),retStr.end());
        return retStr;
    }
};

 算法的时间复杂度也提高了,挺好!

本文含有隐藏内容,请 开通VIP 后查看