天天看點

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;
    }
};