
你好,我是安然無虞。 |
文章目錄
- 自學網站
- 寫在前面
- 為什麼要學習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類是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類對象以及用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 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 s("hello world");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;//正向疊代器++,向正向走
}
結果:
2. 反向疊代器
rbegin() + rend(): rbegin()擷取的是最後一個有效字元的疊代器,rend()擷取的是第一個字元的上一個位置的疊代器
string s("hello world");
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
結果:
對于反向疊代器,我們需要格外注意一下,反向疊代器裡的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;
這裡的疊代器像不像指針一樣的東西,下面我們看看官方庫是怎麼定義它的:
是以,疊代器是什麼?
我們可以認為疊代器是像指針一樣的東西或者就是指針。
這裡也就解釋了,為什麼我說循環控制語句最好不要寫成下面這樣子:
while(it < s.end())
因為如果是連結清單結構,那麼底層就不是按照順序存儲的了,這樣位址的大小是不确定的,是以這樣寫是錯誤的,改成 != 更嚴謹些。
方式三:範圍for
//自動取s裡面的字元賦給ch,自動++,自動判斷結束
for (auto ch : s)
{
cout << ch;
}
cout << endl;
我們知道,上面的範圍for自動取s裡面的字元賦給ch,自動++,自動判斷結束,看起來是不是很高大上,不過終究隻是看起來,其實範圍for的底層原理是被替換成了疊代器。
下面我們通過彙編代碼驗證:
這是方式二疊代器的彙編代碼:
這是方式三範圍for的彙編代碼:
OK,關于周遊通路操作和疊代器的相關問題到此就結束了,不過老鐵們請放心,後面經常會使用到疊代器,咱們慢慢去感受它的奧秘吧。
下面我們做一道練習題強化一下吧。
練習題
原題連結:僅僅反轉字母
題目描述:
示例:
解題思路:
這道題目其實很簡單,跟我們之前學習的快排單趟很相似。
題解代碼:
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類的一些常用接口,至于它們底層是如何實作的,咱們在下一篇部落格中進行講解。
下面如果我們加上這行代碼呢:
//将s中的字元串清空,注意哦,隻是将size置為0,capacity不變
s.clear();
下面我們繼續操作:
//将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()可以避免頻繁增容,減小開銷。
注意哦:
- size()和length()方法底層實作原理完全相同,string中引入size()的原因是為了與其它容器的接口保持一緻,一般情況下基本都是使用size();
- clear()隻是将string中有效字元清空,不改變底層空間大小;
- resize(size_t n)和resize(size_t n, char c)都是将字元串中有效元素個數改變到n個,不同的是當字元個數增多時,resize(size_t n)用’\0’來填充多出的空間,resize(size_t n, char c)用字元’c’來填充多出的空間;
- 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中的域名:
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()函數,它是這樣實作的:
template <class T>
void swap(T& a, T& b)
{
T c(a);
a=b;
b=c;
}
而string類裡面也實作了一個自己的swap()函數:
那給出如下代碼,你猜猜哪個效率更高?
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題,來使用前面所說的這些接口。
課堂練習
字元串中的第一個唯一字元
原題連結:字元串中的第一個唯一字元
題目描述:
示例:
注意:
解題思路:
本題可以借用哈希的思想。
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;
}
};
字元串裡面最後一個單詞的長度
原理連結:字元串裡面最後一個單詞的長度
題目描述:
示例:
解題思路:
本題很簡單,隻需要注意一點即可:不能使用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;
}
驗證回文串
原題連結:驗證回文串
題目描述:
示例:
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;
}
};