天天看点

string类·基本使用

string类·基本使用
你好,我是安然无虞。

文章目录

  • ​​自学网站​​
  • ​​写在前面​​
  • ​​为什么要学习string类?​​
  • ​​什么是string类?​​
  • ​​string类的底层​​
  • ​​string类的常用接口​​
  • ​​构造函数​​
  • ​​遍历访问操作​​
  • ​​迭代器​​
  • ​​练习题​​
  • ​​容量相关操作​​
  • ​​增删查改操作​​
  • ​​非成员函数​​
  • ​​课堂练习​​
  • ​​字符串中的第一个唯一字符​​
  • ​​字符串里面最后一个单词的长度​​
  • ​​验证回文串​​

自学网站

推荐给老铁们两款学习网站:

面试利器&算法学习:​​​牛客网​​​ 风趣幽默的学人工智能:​​人工智能学习​​ ​

写在前面

从今天开始我们就慢慢进入C++的STL部分咯,我会讲解的比较深入,不过请铁子们放心,还是不太难理解的,跟上我的步伐即可。

不过需要注意的是,string是在C++标准库中的,不是在STL中,因为string的产生早于STL。

为什么要学习string类?

首先我们为什么要学习string类呢,学习它有什么作用呢?我们知道,在C语言中,字符串是以’\0’为结尾的一些字符的集合,为了操作方便,C标准库提供了一套str系列的库函数,比如strlen(), strstr(), strcpy()等等,但是这些库函数与字符串是分离开的,不符合OOP的思想,而且底层空间需要用户自己管理,稍不留神会导致越界访问出错。

所以在常规的工作中,为了简单、方便、快捷,基本上都会使用string类,很少会有人去使用C标准库中的字符串操作函数。

什么是string类?

​​关于string类的文档介绍​​

string类·基本使用

对了,有一点需要说明一下,从现在开始大家要尝试读英文文档,不需要一个单词一个单词去翻译,所表示的大致意思知道即可。

就像上面这段描述string类的,你只需要知道:

string是表示字符顺序序列的类,该类的接口与常规容器的接口基本一致,再添加一些专门用来操作string的常规接口。string类是basic_string模板类的一个实例,并且使用char作为它的字符类型(想深入了解,需具备一定编码相关的知识),它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数。

typedef basic_string<char> string;
//string是被typedef出来的,底层是basic_string      

因为string是在C++标准库中定义的,所以我们在使用string类的时候,必须包含#include < string> 头文件以及using namespace std;

还有一个问题就是为什么不降string类的头文件定义成#include<string.h>,而是#include< string>呢?其实很简单,我们知道,C++是兼容C语言的,而C标准库中有string.h这个头文件,所以C++标准库为了避免命名冲突,所以才这样做的。

string类的底层

下面我们大致说说string类的底层结构:

template<class T>
class basic_string
{
public:
  basic_string(const char* str = "")
    :_size(strlen(str))
    , _capacity(_size)
  {
    _str = new char[capacity + 1];
    strcpy(_str, str);
  }

  //之前我们常说传引用返回是为了减少拷贝
  //但是注意哦,这里传引用返回是为了支持修改
  T& operator[](size_t pos)
  {
    assert(pos < _size);
    return _str[pos];
  }
private:
  const T* _str;
  size_t _size;
  size_t _capacity;
};      

好的,这里我就简单介绍一下,大家了解一下即可,下一篇我们会详细模拟实现string的底层。

string类的常用接口

构造函数

string类·基本使用

虽然string类中给我们提供了这么多接口,但是在实际使用当中用的最多的无非两种:构造空的string类对象以及用C字符串来构造string类对象。接下来我们只操作4种,剩下的感兴趣的老铁可以下去自行操作:

string s1;//构造空的string类对象s1
string s2("hello string");//用C格式的字符串构造string类对象s2
string s3(s2);//拷贝构造string类对象s3
string s4(10, 'c');//string类对象s4中包含10个字符'c'      
string类·基本使用

遍历访问操作

一共有三种方式可以遍历式访问string类对象,其中用的最多的就是方式一。

string s("hello cplusplus");
//共有三种遍历方式,其中第一种使用的最多
//1.下标+[]
for (int i = 0; i < s.size(); i++)
{
  cout << s[i];
}
cout << endl;

//2.迭代器
string::iterator it = s.begin();
while (it != s.end())
{
  cout << *it;
  it++;
}
cout << endl;

//其中还可以通过迭代器反向遍历
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
  cout << *rit;
  rit++;
}
cout << endl;

//3.范围for
for (auto ch : s)
{
  cout << ch;
}
cout << endl;      

迭代器

1. 正向迭代器

begin() + end(): begin()获取的是第一个字符的迭代器,end()获取的是最后一个有效字符的下一个位置的迭代器
string类·基本使用
string s("hello world");
string::iterator it = s.begin();
while (it != s.end())
{
  cout << *it << " ";
  it++;//正向迭代器++,向正向走
}      

结果:

string类·基本使用

2. 反向迭代器

rbegin() + rend(): rbegin()获取的是最后一个有效字符的迭代器,rend()获取的是第一个字符的上一个位置的迭代器
string类·基本使用
string s("hello world");
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
  cout << *rit << " ";
  rit++;
}      

结果:

string类·基本使用

对于反向迭代器,我们需要格外注意一下,反向迭代器里的rbegin()获取的是最后一个有效字符位置的迭代器,rend()获取的是第一个字符的前一个位置的迭代器,而且反向迭代器++,是反向走的,正如上面所运行出来的结果一样。

3. 正向只读迭代器

对于正向只读迭代器呢,只能读,不能写。

void Func(const string& rs)
{
  string::const_iterator it = rs.begin();
  while (it != rs.end())
  {
    cout << *it << " ";
    //(*it) += 1;编译不通过
    it++;
  }
}

int main()
{
  string s("hello string");
  Func(s);
  return 0;
}      

4. 反向只读迭代器

对于反向只读迭代器,也是只能读不能写。

void Func(const string& rs)
{
  //string::const_reverse_iterator rit = rs.rbegin();
  //上面rit的类型也太长了吧,这就体现出auto的作用了
  auto rit = rs.rbegin();
  while (rit != rs.rend())
  {
    cout << *rit << " ";
    //(*it) += 1;//编译不通过
    rit++;
  }
}

int main()
{
  string s("hello string");
  Func(s);
  return 0;
}      

OK,关于string类的迭代器,也就是上面的这四种。说到这里,大家可能大致明白迭代器的使用,但是对于迭代器到底是什么或许还是一知半解,下面我们就来说说迭代器到底是何方神圣!

下面对这三种遍历的方式进行解剖:

方式一:下标+[ ]

string s("hello cplusplus");

for (int i = 0; i < s.size(); i++)
{
  cout << s[i];
  //s是自定义类型,所以会找[]运算符重载去调用
  //s.operator[](i);
}
cout << endl;      

我们知道上面的s是自定义类型对象,所以 s[i] 表示调用[ ]运算符重载:s.operator[] (i); 但是对于内置类型来说的话,会直接转化称指针的解引用,就像下面这样:

const char* s2 = "world";
cout << s2[i];//s2为内置类型,直接转化为*(s2+i);      

方式二:迭代器

string s("hello cplusplus");

string::iterator it = s.begin();
//注意哦,这里的!=不建议写成<,后面说为什么
while (it != s.end())
{
  cout << *it;
  it++;
}
cout << endl;      
string类·基本使用

这里的迭代器像不像指针一样的东西,下面我们看看官方库是怎么定义它的:

string类·基本使用

所以,迭代器是什么?

我们可以认为迭代器是像指针一样的东西或者就是指针。

这里也就解释了,为什么我说循环控制语句最好不要写成下面这样子:

while(it < s.end())      

因为如果是链表结构,那么底层就不是按照顺序存储的了,这样地址的大小是不确定的,所以这样写是错误的,改成 != 更严谨些。

方式三:范围for

//自动取s里面的字符赋给ch,自动++,自动判断结束
for (auto ch : s)
{
  cout << ch;
}
cout << endl;      

我们知道,上面的范围for自动取s里面的字符赋给ch,自动++,自动判断结束,看起来是不是很高大上,不过终究只是看起来,其实范围for的底层原理是被替换成了迭代器。

下面我们通过汇编代码验证:

这是方式二迭代器的汇编代码:

string类·基本使用

这是方式三范围for的汇编代码:

string类·基本使用

OK,关于遍历访问操作和迭代器的相关问题到此就结束了,不过老铁们请放心,后面经常会使用到迭代器,咱们慢慢去感受它的奥秘吧。

下面我们做一道练习题强化一下吧。

练习题

原题链接:​​仅仅反转字母​​

题目描述:

string类·基本使用

示例:

string类·基本使用

解题思路:

这道题目其实很简单,跟我们之前学习的快排单趟很相似。

题解代码:

class Solution {
public:
    //判断字母
    bool isChar(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)
        {
            while(left < right && !isChar(s[left]))
                left++;
            while(left < right && !isChar(s[right]))
                right--;
            swap(s[left], s[right]);
            left++;
            right--;
        }
        return s;
    }
};      

容量相关操作

函数名称 功能说明
size 返回字符串有效字符长度
length 返回字符串有效字符长度
capacity 返回空间总大小
empty 检测是否为空串,是则返回true
clear 清空有效字符
reserve 为字符串预留空间(扩空间)
resize 扩空间 + 初始化

好,下面我们来对应练习:

void Teststring1()
{
  string s("hello string");
  cout << s.size() << endl;
  cout << s.length() << endl;
  cout << s.capacity() << endl;
  cout << s << endl;
}      
string类·基本使用

今天这篇博客主要是教大家使用string类的一些常用接口,至于它们底层是如何实现的,咱们在下一篇博客中进行讲解。

下面如果我们加上这行代码呢:

//将s中的字符串清空,注意哦,只是将size置为0,capacity不变
s.clear();      
string类·基本使用

下面我们继续操作:

//将s中的有效字符个数增加到10个,多出位置用'a'初始化
s.resize(10, 'a');
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
cout << endl;

//将s中的有效字符个数增加到15个,多出位置用'\0'初始化
s.resize(15);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
cout << endl;

//将s中的有效字符个数减少至5个,但是容量不会变
s.resize(5);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
cout << endl;      

上面这段代码需要大家自己去敲去感受resize()这个函数。下面我们来感受一下reserve()函数:

void Teststring2()
{
  string s;
  //测试reserve()是否会改变string中有效元素的个数
  s.reserve(100);
  cout << s.size() << endl;
  cout << s.capacity() << endl;
  //以上,很明显reserve()不会改变string中有效元素的个数
  cout << endl;


  //测试reserve()参数小于string的底层空间大小时,是否会将空间缩容
  s.reserve(50);
  cout << s.size() << endl;
  cout << s.capacity() << endl;
  //以上,证明reserve()不会将空间缩小
  //事实上VS下的reserve()和resize()不会缩空间,但是resize()会将有效数据个数减少,空间不会
  cout << endl;
}      

其实细心的你会发现,reserve()函数可以提高插入数据时的效率,因为当你事先知道要开辟多少空间的时候,使用reserve()可以避免频繁增容,减小开销。

注意哦:

  1. size()和length()方法底层实现原理完全相同,string中引入size()的原因是为了与其它容器的接口保持一致,一般情况下基本都是使用size();
  2. clear()只是将string中有效字符清空,不改变底层空间大小;
  3. resize(size_t n)和resize(size_t n, char c)都是将字符串中有效元素个数改变到n个,不同的是当字符个数增多时,resize(size_t n)用’\0’来填充多出的空间,resize(size_t n, char c)用字符’c’来填充多出的空间;
  4. reserve()为string预留空间,不改变有效元素个数。

增删查改操作

函数名称 功能说明
push_back 尾插字符c
append 尾部追加一个字符串
operator+= 尾部追加一个字符串或者字符
find 从pos位置向后找字符c,返回其位置
rfind 从从pos位置向前找字符c,返回其位置
substr 从pos位置开始截取n个字符,然后将其返回
insert 随机插入
erase 随机删除
swap 交换

下面我们来练习以上操作:

void Teststring1()
{
  string str;
  str.push_back('h');//尾插字符'h'
  str.append("ello");//尾部追加字符串"ello"
  str += ' ';//尾部追加一个空格
  str += "world";//尾部追加字符串"world"
  cout << str << endl;
  cout << str.c_str() << endl;//以C语言的方式打印字符串
  cout << endl;
}      

获取文件后缀:

void Teststring2()
{
  //获取文件后缀
  string file("string.cpp");
  //从前向后找字符'.'如果找到了,返回其位置
  size_t pos = file.find('.');
  //判断pos位置是否存在
  if (pos != string::npos)//如果没找到,返回npos(后面说)
  {
    string suffix(file.substr(pos, file.size() - pos));
    cout << suffix << endl;
  }

  //npos是string类里面的一个静态的成员变量
  //static const size_t npos = -1;

  size_t pos = file.rfind('.');
  //从后向前找字符'.'如果找到了,返回其位置
  if (pos != string::npos)
  {
    string suffix(file.substr(pos, file.size() - pos));
    cout << suffix << endl;
  }

  //可能你会觉得rfind()跟find()相比好像没啥特殊功能鸭,那你看看下面:
  string file2("string.c.tar.zip");
  //这个时候如果还是用find()找到的就是.c.tar.zip
  size_t pos = file2.rfind('.');
  if (pos != string::npos)
  {
    string suffix = file2.substr(pos, file2.size() - pos);
    cout << suffix << endl;
  }
}      

取出URL中的域名:

string类·基本使用
void Teststring3()
{
  //取出URL中的域名:www.cplusplus.com
  string url("https://www.cplusplus.com/reference/string/string/find/");
  cout << url << endl;
  size_t start = url.find("://");
  if (start != string::npos)
  {
    start += 3;
    size_t finish = url.find('/', start);
    string address(url.substr(start, finish - start));
    cout << address << endl;
  }
}      

下面说说swap()函数,我们知道,在algorithm头文件下有一个swap()函数,它是这样实现的:

string类·基本使用
template <class T> 
void swap(T& a, T& b)
{
    T c(a); 
    a=b; 
    b=c;
}      

而string类里面也实现了一个自己的swap()函数:

string类·基本使用

那给出如下代码,你猜猜哪个效率更高?

void Teststring4()
{
  string s1("hello cplusplus");
  string s2("string");
  s1.swap(s2);//string类的swap(),效率高,直接交换指针
  swap(s1, s2);//algorithm头文件下的swap(),效率低,深拷贝交换
}      

关于底层实现会在下一篇详细讲解哦。

注意:

  • 在string尾部追加字符时,s.push_back(‘c’) / s.append(‘c’) / s += 'c’这三种实现方式差不多,不过一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串;
  • 对string操作时,如果能事先知道大概存放多少字符,可以使用reserve()将空间预留好,避免多次增容导致性能消耗。

非成员函数

以上说的都是string类的成员函数,下面我们来说说string类的非成员函数。

函数名称 功能说明
operator+ 传值返回,尽量少用
operator>> 输入运算符重载
operator<< 输出运算符重载
getline 获取一行字符串

上面提供的几个接口,自行了解即可,还有很多接口大家可以查阅文档学习哦,是时候培养查阅文档的习惯了。下面我们给出几道OJ题,来使用前面所说的这些接口。

课堂练习

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

原题链接:​​字符串中的第一个唯一字符​​

题目描述:

string类·基本使用

示例:

string类·基本使用

注意:

string类·基本使用

解题思路:

本题可以借用哈希的思想。

class Solution {
public:
    int firstUniqChar(string s) {
        //统计次数
        int count[26] = {0};
        for(auto ch : s)
        {
            //字符在内存中都是以ASCII存着的
            count[ch - 'a']++;
        }

        for(int i = 0; i < s.size(); i++)
        {
            if(count[s[i] - 'a'] == 1)
                return i;
        }
        return -1;
    }
};      

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

原理链接:​​字符串里面最后一个单词的长度​​

题目描述:

string类·基本使用

示例:

string类·基本使用

解题思路:

本题很简单,只需要注意一点即可:不能使用cin和scanf(),因为它们遇到空格就结束了,所以如果想正确输入字符串中的空格,需要使用getline()函数。

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

int main() {
    string str;
    //注意哦,不能使用cin和scanf(),因为它们遇到空格就结束了
    getline(cin, str);
    size_t pos = str.rfind(' ');//获取字符串最后一个空格的位置
    cout << str.size() - pos - 1 << endl;

   return 0;
}      

验证回文串

原题链接:​​验证回文串​​

题目描述:

string类·基本使用

示例:

string类·基本使用
class Solution {
public:
    //判断是否为小写字母或者数字字符
    bool isLetterOrNumber(char ch){
        if(ch >= 'a' && ch <= 'z') return true;
        else if(ch >= '0' && ch <= '9') return true;
        else return false;
    }
    bool isPalindrome(string s) {
        //将所有的大写字母转化为小写字母
        for(auto& ch : s){
            if(ch >= 'A' && ch <= 'Z')
                ch += 32;
        }
        int begin = 0;
        int 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]){
                begin++;
                end--;
            }
            else{
                return false;
            }
        }
        return true;
    }
};